AgentOS Content Pipeline
Authoritative guide for writing and shipping lessons / bonuses on <your-agentos-domain>.
Read this before touching anything in apps/web/app/intensive/lesson/[id]/page.tsx or
the intensive_bonuses Supabase table.
When to use
- Adding a new bonus (текстовый гайд, без видео)
- Rewriting an existing lesson (e.g., changing structure, adding steps)
- Updating prompts / repos / outcome / skills inside an existing bonus
- Shipping any content change to production
Не использовать для
- Изменений auth / payments / API endpoints (это отдельный стек)
- Бекенд миграций (Supabase schema changes — отдельный workflow)
- Изменений рендерера блоков (
UnifiedBlockRenderer.tsx) без понимания всех уроков
Workflow at a glance
1. Author Python source-of-truth file → /tmp/lessonN_steps.py
2. Generate TS mock from source → python3 /tmp/update_mock_lN.py
3. Patch Supabase DB from same source → ssh thrall + python3 patch_db_lN.py
4. Build Next.js standalone → npm run build
5. Stage tarball + scp to prod server → /tmp/web-stage-vN/ → /tmp/web-deploy.tgz
6. Unpack on prod + PM2 restart → cd /var/www/<your-project> && tar -xzf
7. Verify HTTP 200 + visible content → curl https://<your-agentos-domain>/intensive/lesson/N
8. **Visual verify (MANDATORY)** → agent-browser → screenshot mobile → проверить глазами
9. Commit + push → <your-github-org>/<your-platform-repo> main
Critical rules (read before any change)
Source-of-truth is Python, not TS or JSON. One Python file drives both mock and DB — so they NEVER diverge. See
references/02-source-of-truth.md.re.subreplacement strings interpret\\nas newline. This will silently break your generated TypeScript with «Unterminated string constant». Always wrap repl inlambda m: repl_str. Seereferences/06-pitfalls.mdPitfall #1.Stale
/tmp/lessonN_steps.pyon Thrall.patch_db.pydoessys.path.insert(0, '/tmp'). If an old version sits in/tmp(from a previous session), it will load that one instead of yours and silently patch the WRONG content. Alwayscp /root/lessonN_steps.py /tmp/lessonN_steps.pybefore running patch_db. See Pitfall #2.Bonus pages don't show video or timecodes.
bonusN !== nullinapps/web/app/intensive/lesson/[id]/page.tsxcontrols this. Don't accidentally re-enable them.No ID buttons on any block. Earlier ID copy was visual noise — removed from step / code / prompt / mcp_config / agent_config. Only main «Копировать» (content) button remains.
Production deploy goes to prod server (), NOT Thrall. DNS for
<your-agentos-domain>points at prod server. There IS a shadow PM2<your-pm2-name>on Thrall :3020 — deploying there looks green on internal curl but prince sees old code. Alwaysdig +short <your-agentos-domain>before deploy.Service-role-key NEVER in chat output.
bot/.envon Thrall contains SUPABASE_SERVICE_ROLE_KEY which bypasses RLS. Nevercat bot/.env, nevertr "\0" "\n" < /proc/PID/environwithout| grep -v ROLE_KEY. See Pitfall #5.Visual verify after deploy is MANDATORY (HARD RULE). После любого UI-deploy НЕЛЬЗЯ рапортовать «Готово» только на основе HTTP 200 + grep на ключевые строки.
grepловит наличие нужного, но не отсутствие лишних элементов от прошлой версии. Обязательный шаг:- открыть страницу через
agent-browser(CDP, headless Chrome) - mobile viewport (375px width)
- screenshot full-page (проскроллить полностью включая footer)
- глазами сверить с ожиданием: positive (новое есть) + negative (старого нет)
- только потом «Готово»
Если visual verify не сделана (agent-browser недоступен) — явно сказать user: «визуально не проверила, могут быть остаточные UI-элементы». См. Pitfall #9 в
references/06-pitfalls.md. Применяется ко ВСЕМ UI-задачам: EdgeLab, AgentOS Intensive, workshop, любая страница на*.<your-domain>.- открыть страницу через
READ README перед build/deploy (HARD RULE, 2026-05-13). Перед
npm run buildлибо правкой env / deploy скриптов / infra — обязательноRead README.mdрепо целиком (илиGrepпо «deploy», «env», «host», «prod», «production»). Не доверяй.env.example(часто staging-значения) и устаревшим skill-файлам. README на HEAD — canonical. Эту ошибку уже один раз получили (NEXT_PUBLIC_API_BASE_URL из staging значения сломал prod). См. Pitfall #11 +core/rules.mdглавное правило.«Материалы» эфира = поле
materials, неblocks(HARD RULE). На странице эфира вкладка-счётчик «Материалы · N» считает изintensive_eth_details.materials(массив{name, desc, url, filename}), НЕ из link-блоков внутриblocks. После добавления link-блоков в описание шага обязательно параллельно PATCHmaterialsтем же набором. См. Pitfall #15.Cabinet счётчики материалов — хардкод-мок (HARD RULE).
apps/web/app/intensive/cabinet/page.tsxимеет массивBROADCASTSс полямиmaterials/lessons/openзашитыми статикой. Ничего не читает из БД. При добавлении материалов к эфиру N — параллельно правитьBROADCASTS[N-1].materialsи делать полный rebuild + redeploy. См. Pitfall #16.Hot-patch скомпилированных chunks с тем же hash — антипаттерн (HARD RULE). Браузер кеширует chunk по hash в имени файла как immutable. Правка содержимого без смены hash → у существующих посетителей старый код, у новых — новый. Всегда полный
npm run build(получит новый hash) → tarball → scp →pm2 restart. См. Pitfall #12.Статика только вне
/intensive/*префикса. Next.js app router имеет роуты/intensive/cabinet,/intensive/eth/[id],/intensive/lesson/[id]и т.д. Файлы вpublic/intensive/<anything>/...→ HTTP 404 из-за конфликта. Паттерн в проекте:public/efir-N/...→/efir-N/.... После добавления статики в public/ — обязательныйpm2 restart(Next.js standalone кеширует listing при старте, см. Pitfall #14).Контент урока живёт в 4 местах, не в одном (HARD RULE). После любого изменения контента урока обязательно обновить ВСЕ 4 источника, иначе listing-страницы покажут старое:
- DB
intensive_lessons.blocks— детальная страница BONUS_LESSONS/ONBOARDING_LESSONSmock вapp/intensive/lesson/[id]/page.tsx— SSR fallbackLESSONS[]hardcoded вapp/intensive/cabinet/onboarding/page.tsx— список предобученияFEED_ITEMS[]hardcoded вapp/intensive/materials/page.tsx— агрегация материалов
Обязательный grep-чеклист после изменения:
grep -rn "ONBOARDING_LESSONS\|BONUS_LESSONS\|FEED_ITEMS\|LESSONS\[" apps/web/app/Visual verify (#8) ловит автоматически — если делать скриншот listing-страниц, а не только детальной. См. Pitfall #10.
- DB
Detailed references
Open ONLY the reference you need (don't load all on every task):
references/01-architecture.md— bonus vs lesson, ID mapping (lesson 10 ↔ bonus 1), full block-types table, where each piece of content lives.references/02-source-of-truth.md— Python source file structure, helper conventions, step / prompt / timecode / repo formats. Includestemplates/lesson_steps.py.tmpl.references/03-mock-and-db.md— howupdate_mock.pyrewritespage.tsx, howpatch_db.pyPATCHes Supabase, where service-role-key lives, env path on Thrall.references/04-deploy.md— exact tarball layout, scp steps, PM2 commands, verify curl, rollback via.next.bak.*directories.references/05-frontend-rules.md— hide rules (video / timecodes / ID buttons), block-types white-list, mockToBlocks adapter, render order (timecodes → description → rest).references/06-pitfalls.md— 17 silent failure modes that will waste your hour. Особо критичны для эфир-страниц: #11.env.exampleломает prod API URL, #12 hot-patch chunk не инвалидирует кеш, #13public/intensive/*404 из-за app router конфликта, #14 Next.js standalone кеширует public/ listing (нужен PM2 restart), #15 «Материалы» эфира — отдельное поле БДmaterials, не blocks, #16 cabinet counters — хардкод-мок, #17 material card был<div>без href (баг репо, починен 2026-05-13).references/07-video-pipeline.md— видео-уроки end-to-end: Kinescope upload через TUS (X-Video-Title quirk), CSP frame-src для iframe, тайм-коды через AssemblyAI universal-2 + Sonnet по word-level транскрипту, транскрипты какpublic/transcripts/lesson-N.md.
Video lessons pipeline (NEW, 2026-05-08)
Для уроков с видео-эфиром нужны три связанные сущности — все три в скилле автоматизированы.
1. Kinescope embed на странице урока
- Frontend:
app/intensive/lesson/[id]/page.tsxрендерит iframehttps://kinescope.io/embed/{kinescope_video_id}?playsinline=1через padding-top 56.25% хак (НЕ через Tailwindaspect-video+absolute— на iOS Safari лазерчатый. См. Pitfall #11 в 07). - Source of truth для video_id:
intensive_lessons.kinescope_video_id(DB) ИЛИLessonMock.kinescopeId(mock fallback). Backend/api/platform/intensive/lessonsотдаётkinescope_video_idв DTO — фронт preferит DTO, mock — fallback. - CSP (HARD RULE):
next.config.tsДОЛЖЕН содержатьframe-src 'self' https://kinescope.io https://*.kinescope.io+media-srcдля CDN; Caddy/etc/caddy/CaddyfileДОЛЖЕН отдавать тот же CSP (он перезаписывает Next-заголовок). Без этого iframe чёрный без сообщения. См. 07-video-pipeline.md секция «CSP и Caddy». - Upload через API:
POST https://uploader.kinescope.io/v2/videoсо всем файлом в body +X-Video-Title(b64 от ASCII-плейсхолдера, потому что header'ы ASCII-only) +X-Parent-Id. После — PATCH/v1/videos/{id}с настоящим title (cyrillic). См. 07 секция «Upload».
2. Тайм-коды по реальным переходам темы
Двухэтапный пайплайн вместо ручной разметки или равномерных интервалов:
- Транскрибация в AssemblyAI:
language_code=ru,speech_models=["universal-2"](устаревшийspeech_modelстрокой больше не работает — fallback ругается). Получаем words сstart/endв ms. - Сегментация в окна по 30s, отправка в Sonnet через Agent tool с просьбой найти 5-7 точек где явно меняется тема. Sonnet возвращает
{ts: int_seconds, label: "..."}. НЕ генерируй тайм-коды через OpenRouter — только черезAgent(subagent_type=general-purpose, model=sonnet)per rules.md. - Запись в DB как блоки
type=timecodeс форматом{id, ts: "MM:SS", type, label}, вставленные после meta/outcome/skills и перед steps. Параллельно те же тайм-коды в mockLessonMock.timecodes[]для SSR.
3. Транскрипт-файлы для скачивания
- Файлы лежат в
apps/web/public/transcripts/lesson-{N}.md— markdown с заголовками## MM:SS — labelпо тайм-кодам и абзацами по паузам (gap > 800ms). - Кнопка «Скачать транскрипт» на странице урока показывается между видео и тайм-кодами для
lessonN ∈ [1..5](расширять список по мере появления новых транскриптов). Атрибутdownload="agentos-lesson-{N}-transcript.md". - Транскрипт = бонус для AI-репетитор-флоу из L1: ученик скачивает .md, вставляет в Claude/GPT, расспрашивает.
См. полный пайплайн в references/07-video-pipeline.md.
Templates
In templates/:
lesson_steps.py.tmpl— copy to/tmp/lessonN_steps.py, fill in TITLE / DURATION / DESCRIPTION / OUTCOME / SKILLS / PROMPTS / REPOS / TIMECODES / STEPS.update_mock.py.tmpl— copy to/tmp/update_mock_lN.py, change bonus_number, block boundary identifiers ('11': {/'12': {).patch_db.py.tmpl— copy to/tmp/patch_db_lN.py, changebonus_number=eq.N.
Quickstart: add a new bonus in 5 minutes
# 1. Copy templates, fill in content
cp .claude/skills/agentos-content/templates/lesson_steps.py.tmpl /tmp/lesson12_steps.py
cp .claude/skills/agentos-content/templates/update_mock.py.tmpl /tmp/update_mock_l12.py
cp .claude/skills/agentos-content/templates/patch_db.py.tmpl /tmp/patch_db_l12.py
# Edit lesson12_steps.py — add TITLE, DESCRIPTION, STEPS, etc.
# Edit update_mock_l12.py — change "'11'" → "'12'", "'12'" → "'13'" (block boundary)
# Edit patch_db_l12.py — change "bonus_number=eq.2" → "bonus_number=eq.3"
# 2. Update mock + DB
python3 /tmp/update_mock_l12.py
scp /tmp/lesson12_steps.py /tmp/patch_db_l12.py root@<your-staging-server-ip>:/root/
ssh root@<your-staging-server-ip> 'cp /root/lesson12_steps.py /tmp/lesson12_steps.py && \
set -a && source /home/<your-user>/intensive-agentos/bot/.env && set +a && \
cd /root && python3 patch_db_l12.py'
# 3. Build + deploy (see references/04-deploy.md for full steps)
cd apps/web && npm run build
# ... stage tarball, scp, unpack on prod server, PM2 restart, verify
# 4. Commit + push
git add apps/web/app/intensive/lesson/\[id\]/page.tsx
git commit -m "feat(lesson 12): новый бонус про X"
git push origin main
Where content lives
| Layer | Location |
|---|---|
| Python source-of-truth (current session) | /tmp/lessonN_steps.py |
| Templates (this skill) | .claude/skills/agentos-content/templates/ |
| TS mock (SSR fallback) | apps/web/app/intensive/lesson/[id]/page.tsx (BONUS_LESSONS / ONBOARDING_LESSONS) |
| Block renderer | apps/web/components/blocks/UnifiedBlockRenderer.tsx |
| Block types schema | apps/web/lib/unified-block-types.ts |
| DB (live source for client fetch) | Supabase intensive_bonuses table, column blocks (JSON) |
| Backend (FastAPI gunicorn :8095) | /home/<your-user>/intensive-agentos/api/ on Thrall |
| Production frontend | prod server → /var/www/<your-project>/ (PM2 :3020) |
| Staging frontend | Thrall → /home/<your-user>/intensive-<your-pm2-staging-name>/ (PM2 :3010) |
| Repo | <your-github-org>/<your-platform-repo> (private) |