Shkor beauty — это AI-ассистент витрины (B2B): подбор ухода, заказы и заявки — единое API для вашего сайта и любых агентов. Оператор сервиса — Shkor Labs LLC.
Настоящая Политика конфиденциальности объясняет, какие данные собирает сервис Shkor beauty
+(далее — «Сервис») и как они используются. Оператор Сервиса — Shkor Labs LLC, компания,
+зарегистрированная в штате Вайоминг, США (далее — «Оператор»). Контакт: alex.shkor@gmail.com.
+
+
1. Какие данные мы собираем
+
+
Заявка (Lite-план): имя, телефон и текст заявки, которые вы сами указываете в форме «Оставить заявку».
+
Заказ (Full-план): имя, телефон, адрес доставки и состав заказа.
+
Обращения: письма на email Оператора и переписка в мессенджерах — в объёме, необходимом для ответа.
Данные банковских карт: при онлайн-оплате (Full-план) платёж проводит независимый платёжный агент;
+ данные карты вводятся на его защищённой странице и Оператору не передаются.
+
Фотографии кожи без явного согласия: если вы передаёте фотоотчёт AI-ассистенту, он обрабатывается
+ для анализа состояния кожи и не публикуется и не передаётся третьим лицам.
+
+
+
3. Зачем мы используем данные
+
+
связаться с вами по заявке и довести её до результата (подбор, заказ, доставка);
+
исполнить заказ и уведомлять о его статусе;
+
улучшать каталог и подбор ассистента на обезличенной статистике.
+
+
+
4. Хранение и срок
+
Записи заявок и заказов хранятся в защищённой базе Оператора не дольше, чем это необходимо для
+исполнения заказа и требований бухгалтерии (по умолчанию — до 3 лет), после чего удаляются или
+обезличиваются.
+
+
5. Передача третьим лицам
+
Данные передаются только: (а) службе доставки — имя, телефон и адрес вашего заказа;
+(б) платёжному агенту — минимально необходимый платёжный контекст; (в) в случаях, предусмотренных законом.
+
+
6. Cookie
+
Сервис использует минимальные cookie и локальное хранилище: сессия корзины и анонимная статистика.
+Подробнее — в Disclaimer.
+
+
7. Ваши права
+
Вы можете запросить доступ к своим данным, их исправление или удаление письмом на
+alex.shkor@gmail.com. Мы отвечаем в течение 30 дней.
+
+
8. Изменения
+
Актуальная версия Политики всегда доступна на этой странице; существенные изменения датируются выше.
+""")
+
+page("terms", "Terms of Service", D, """
+
Условия использования сервиса Shkor beauty (далее — «Условия»). Оператор Сервиса — Shkor Labs LLC
+(Wyoming, USA), контакт alex.shkor@gmail.com. Используя Сервис,
+вы соглашаетесь с настоящими Условиями.
+
+
1. Что такое Сервис
+
Shkor beauty — витрина корейской косметики с AI-ассистентом: подбор ухода, заявки и заказы с доставкой
+по Минску и Беларуси. Сервис предлагается в двух планах:
+
+
Lite (базовый): витрина + форма заявки. Заявка фиксируется в общей базе заказов, после чего
+ магазин связывается с вами лично для уточнения ассортимента, цены и способа расчёта.
+
Full: полный каталог, корзина, онлайн-оплата и статусы доставки.
+
+
+
2. Заявки и заказы
+
+
Заявка (Lite) не является подтверждённой сделкой: цена и наличие подтверждаются при обратной связи.
+
Заказ (Full) считается принятым после подтверждения оплаты или согласия на оплату при получении.
+
Цены указаны в белорусских рублях (BYN) и могут меняться; актуальная цена — на витрине в момент заказа.
+
+
+
3. Доставка
+
Курьер по Минску, Европочта по Беларуси и самовывоз. Сроки и стоимость показываются при оформлении
+заявки/заказа; бесплатная доставка — от 80 BYN по каталогу витрины.
+
+
4. Возврат и обмен
+
+
Товар надлежащего качества (без следов использования, в оригинальной упаковке) — возврат/обмен
+ в течение 14 дней с момента получения.
+
Средства гигиены и косметика с нарушенной упаковкой возврату не подлежат.
Материалы витрины, AI-подбор и программный код ассистента принадлежат Оператору. Товарные знаки
+брендов принадлежат их правообладателям.
+
+
6. Ограничение ответственности
+
Сервис предоставляется «как есть». AI-подбор средств — рекомендательный характер и не является
+медицинской консультацией (см. Disclaimer). Оператор не несёт
+ответственности за индивидуальные реакции кожи и решения, принятые на основе рекомендаций без
+ознакомления с составами и инструкциями.
+
+
7. Применимое право
+
Настоящие Условия регулируются правом, применимым к месту регистрации Оператора (Wyoming, USA),
+без ущерба для обязательных потребительских прав покупателей по законодательству места доставки.
Страница объединяет политику cookie и важные дисклеймеры сервиса Shkor beauty. Оператор —
+Shkor Labs LLC (Wyoming, USA), alex.shkor@gmail.com.
+
+
1. Не медицинская консультация
+
+
AI-ассистент и материалы витрины НЕ являются медицинской консультацией, диагностикой или лечением.
+Подбор средств — эвристическая рекомендация на основе вашего самоотчёта. Она не заменяет консультацию
+дерматолога или врача. При заболеваниях кожи, беременности, приёме лекарственных средств и выраженных
+реакциях — проконсультируйтесь со специалистом до применения средств.
+
+
+
2. Косметика и индивидуальные реакции
+
+
Перед первым применением сделайте тест на чувствительность (небольшой участок кожи, 24–48 часов).
+
Активы (ретинол, кислоты, витамин C) вводите постепенно; ретинол — только на ночь, SPF утром обязателен.
+
Ознакомьтесь с составом на упаковке: при аллергии на компонент средство не используйте.
+
+
+
3. Cookie
+
+
Необходимые: сессия корзины и защита форм. Не отключаются.
+
Аналитические: анонимная статистика посещений для улучшения витрины. Без персональных идентификаторов и без передачи рекламным сетям.
+
Рекламных и трекинговых cookie сторонних сетей — нет.
+
+
Большинство браузеров позволяет управлять cookie в настройках; отключение необходимых cookie
+может сломать корзину и отправку заявки.
+
+
4. Внешние ссылки
+
Витрина может ссылаться на сайты брендов и службы доставки — за их содержание Оператор
+ответственности не несёт.
Shkor beauty — AI-ассистент для магазинов корейской косметики: витрина, которая сама
+разбирает состояние кожи клиента, собирает рутину и доводит заявку до заказа.
+
+
Кто мы
+
Оператор сервиса — Shkor Labs LLC, компания, зарегистрированная в штате Вайоминг, США.
+Проект развивается совместно с инженерным кооперативом «42» (cil.by): витрина и ассистент собраны
+ритмичным агентом Simbiot.
+
+
Что мы делаем
+
+
Витрина K-beauty: каталог проверенных брендов — Anua, Beauty of Joseon, JMsolution,
+ SKIN627, Biodance и другие, доставка по Минску и Беларуси.
+
AI beauty-ассистент (B2B): агентный API analyze → recommend → order, который любой
+ магазин или AI-агент может подключить к своему сайту (MCP/STDIO, HTTP, CLI).
+
Два плана подключения:Lite — витрина + заявки в общую базу; Full — полный
+ каталог, корзина, онлайн-оплата и статусы доставки.
Ассистент подключается к любому сайту или AI-агенту за один шаг: JSONL поверх STDIO (MCP-стиль),
+HTTP JSON API или разовый CLI-вызов. Каталог масштабируется без правок кода. Для пилота напишите
+на email — подключим витрину с формой заявок (Lite) и покажем e2e-сценарий заказа.
+""")
diff --git a/artifacts/shkor/orders.json b/artifacts/shkor/orders.json
new file mode 100644
index 0000000..4669e51
--- /dev/null
+++ b/artifacts/shkor/orders.json
@@ -0,0 +1,158 @@
+{
+ "SB-R-1D8C99C8": {
+ "order_id": "SB-R-1D8C99C8",
+ "plan": "lite",
+ "kind": "request",
+ "note": "Хочу сыворотку для сухой кожи",
+ "products": [
+ "p01"
+ ],
+ "customer": {
+ "name": "Тест Тестов",
+ "phone": "+375291234567",
+ "email": "",
+ "address": ""
+ },
+ "payment": {
+ "status": "none",
+ "method": "none",
+ "pay_link": null
+ },
+ "total": 0.0,
+ "status": "request",
+ "created": "2026-10-08T16:03:25Z"
+ },
+ "SB-55B78647": {
+ "order_id": "SB-55B78647",
+ "plan": "full",
+ "items": [
+ {
+ "id": "p05",
+ "title": "Успокаивающий тонер с сердечником Centella Toner 77%",
+ "qty": 1,
+ "price": 62.0,
+ "sum": 62.0
+ }
+ ],
+ "subtotal": 62.0,
+ "delivery": {
+ "key": "minsk_courier",
+ "label": "Курьер по Минску",
+ "eta": "сегодня/завтра",
+ "price": 7.0,
+ "free_from": 80.0,
+ "charged": 7.0
+ },
+ "total": 69.0,
+ "customer": {
+ "name": "",
+ "phone": "",
+ "address": ""
+ },
+ "payment": {
+ "status": "pending",
+ "method": "cash_on_delivery",
+ "pay_link": null
+ },
+ "status": "placed",
+ "created": "2026-10-08T16:03:25Z"
+ },
+ "SB-8AC083A5": {
+ "order_id": "SB-8AC083A5",
+ "plan": "lite",
+ "items": [
+ {
+ "id": "p03",
+ "title": "Солнцезащитный гель Relief Sun SPF50+ PA++++",
+ "qty": 1,
+ "price": 49.0,
+ "sum": 49.0
+ }
+ ],
+ "subtotal": 49.0,
+ "delivery": {
+ "key": "minsk_courier",
+ "label": "Курьер по Минску",
+ "eta": "сегодня/завтра",
+ "price": 7.0,
+ "free_from": 80.0,
+ "charged": 7.0
+ },
+ "total": 56.0,
+ "customer": {
+ "name": "",
+ "phone": "",
+ "address": ""
+ },
+ "payment": {
+ "status": "pending",
+ "method": "cash_on_delivery",
+ "pay_link": null
+ },
+ "status": "placed",
+ "created": "2026-10-08T16:03:25Z"
+ },
+ "SB-R-7B23D4D8": {
+ "order_id": "SB-R-7B23D4D8",
+ "plan": "lite",
+ "kind": "request",
+ "note": "Проверка формы с витрины (e2e)",
+ "products": [],
+ "customer": {
+ "name": "E2E Тест",
+ "phone": "+375290000000",
+ "email": "",
+ "address": ""
+ },
+ "payment": {
+ "status": "none",
+ "method": "none",
+ "pay_link": null
+ },
+ "total": 0.0,
+ "status": "request",
+ "created": "2026-10-08T16:08:02Z"
+ },
+ "SB-B352DCBF": {
+ "order_id": "SB-B352DCBF",
+ "plan": "full",
+ "items": [
+ {
+ "id": "p05",
+ "title": "Успокаивающий тонер с сердечником Centella Toner 77%",
+ "qty": 1,
+ "price": 62.0,
+ "sum": 62.0
+ },
+ {
+ "id": "p03",
+ "title": "Солнцезащитный гель Relief Sun SPF50+ PA++++",
+ "qty": 1,
+ "price": 49.0,
+ "sum": 49.0
+ }
+ ],
+ "subtotal": 111.0,
+ "delivery": {
+ "key": "minsk_courier",
+ "label": "Курьер по Минску",
+ "eta": "сегодня/завтра",
+ "price": 7.0,
+ "free_from": 80.0,
+ "charged": 0.0
+ },
+ "total": 111.0,
+ "customer": {
+ "name": "Тест",
+ "phone": "+375XX0000000",
+ "address": ""
+ },
+ "payment": {
+ "status": "pending",
+ "method": "cash_on_delivery",
+ "pay_link": null
+ },
+ "status": "placed",
+ "created": "2026-10-08T16:08:11Z"
+ }
+}
\ No newline at end of file
diff --git a/cli.py b/cli.py
index 3229f8d..c2033ca 100644
--- a/cli.py
+++ b/cli.py
@@ -90,6 +90,9 @@ def main():
help="ключ идемпотентности (по умолчанию file:<абсолютный путь>)")
p_dfl.add_argument("--force", action="store_true",
help="отправить даже если уже доставлен (повторная доставка вручную)")
+ p_dfl.add_argument("-m", "--msg", dest="msg", default=None,
+ help="связать доставку с входящим m-NNN (анти-гонка уровней, "
+ "t-261009160906): если на него уже отвечено иным артефактом — SKIP")
p_pro = sub.add_parser("proactive", help="инициативная реплика: ритм/тема/отправка (t-260930073419)")
p_pro.add_argument("--force", action="store_true",
help="отправить сейчас, игнорируя окно/интервал/тишину")
@@ -361,11 +364,28 @@ def main():
print(f"FAIL: файл не найден: {args.file}")
sys.exit(1)
aid = args.key or f"file:{os.path.abspath(args.file)}"
+ # Анти-гонка уровней (t-261009160906): если доставка привязана к
+ # входящему m-NNN и на него уже отвечено ДРУГИМ артефактом —
+ # не отправляем второй файл на тот же запрос (гонка min/hour).
+ if args.msg and not args.force:
+ model = es.run_projections()
+ m = (model.get("inbox") or {}).get(args.msg)
+ if m is not None and m.get("replied") and m.get("reply_artifact") not in (None, "", aid):
+ es.append_event("harness", aid, "DeliverySkipped", {
+ "artifact_id": aid, "kind": "file",
+ "reason": "msg-already-answered-by-other-artifact",
+ "msg": args.msg,
+ "reply_artifact": m.get("reply_artifact"),
+ })
+ print(f"SKIP: на {args.msg} уже отвечено другим артефактом "
+ f"({m.get('reply_artifact')}) — используй --force для повтора")
+ sys.exit(0)
def _send():
return tg_inbox.send_document_to_chat(args.file, caption=args.caption)
ok = commands.deliver_artifact(aid, kind="file", send=_send,
owner="cli:tg-deliver-file",
- caption=args.caption, force=args.force)
+ caption=args.caption, force=args.force,
+ msg=args.msg)
print(f"OK: файл доставлен ({aid})" if ok
else f"SKIP: уже доставлен ({aid}) — используй --force для повтора")
elif args.cmd == "proactive":
diff --git a/harness/*.db b/harness/*.db
new file mode 100644
index 0000000..e69de29
diff --git a/harness/commands.py b/harness/commands.py
index fcc04fa..d87eed6 100644
--- a/harness/commands.py
+++ b/harness/commands.py
@@ -351,7 +351,7 @@ def _delivery_text(task: dict, answer: str) -> str:
def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
- caption: str = "", force: bool = False) -> bool:
+ caption: str = "", force: bool = False, msg: str = None) -> bool:
"""Идемпотентная доставка ПРОИЗВОЛЬНОГО артефакта в чат (t-260930083937).
Рут-кейс дублей: мин-тик шёл файлом «скинь сюда» curl'ом sendDocument —
@@ -373,6 +373,21 @@ def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
"reason": "already-delivered",
})
return False
+ # Анти-гонка уровней (t-261009160906): артефакт привязан к входящему
+ # m-NNN, на которое уже отвечено ДРУГИМ артефактом → SKIP. Два уровня
+ # (min/hour) на один запрос не должны слать два разных файла; dedup по
+ # artifact_id=file:<путь> их не различает (разные пути = разные ключи).
+ if msg and not force:
+ model = es.run_projections()
+ m = (model.get("inbox") or {}).get(msg)
+ if (m is not None and m.get("replied")
+ and m.get("reply_artifact") not in (None, "", artifact_id)):
+ es.append_event("harness", artifact_id, "DeliverySkipped", {
+ "artifact_id": artifact_id, "kind": kind,
+ "reason": "msg-already-answered-by-other-artifact",
+ "msg": msg, "reply_artifact": m.get("reply_artifact"),
+ })
+ return False
if force:
# ручной повтор — явное решение человека: лок недавней доставки
# не должен блокировать его на TTL (120с). Снимаем свой же захват
@@ -394,10 +409,13 @@ def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
# в ledger остаётся фактом; повторный force просто отправит ещё раз,
# но ledger при этом не «молодеет» (kind/ts не переписываются)
es.record_delivery(artifact_id, kind=kind)
- es.append_event("harness", artifact_id, "ArtifactDelivered", {
+ ev = {
"artifact_id": artifact_id, "kind": kind, "tg_ok": True,
"caption": (caption or "")[:500], "owner": owner,
- })
+ }
+ if msg:
+ ev["msg"] = msg # анти-гонка уровней (t-261009160906)
+ es.append_event("harness", artifact_id, "ArtifactDelivered", ev)
return True
diff --git a/harness/es.py b/harness/es.py
index 97e99d6..aaa93ed 100644
--- a/harness/es.py
+++ b/harness/es.py
@@ -548,6 +548,15 @@ def _apply_event(model: dict, ev: dict):
if m:
m["replied"] = True
m["reply_text"] = data.get("text", "")
+ elif etype == "ArtifactDelivered":
+ # Анти-гонка уровней (t-261009160906): связка «файл → входящее».
+ # Если доставка артефакта помечена msg=m-NNN, фиксируем её в
+ # инбоксе — второй артефакт на то же входящее будет SKIP'нут.
+ msg = (data or {}).get("msg") if isinstance(data, dict) else None
+ if msg:
+ m = model.setdefault("inbox", {}).get(msg)
+ if m is not None and not m.get("reply_artifact"):
+ m["reply_artifact"] = data.get("artifact_id")
elif etype == "MessageSeen":
# индикация взятия в работу (e-261002202857): флаг seen → reaction 👀
# ставится ровно один раз, повторные маркеры сообщений не найдут
diff --git a/out/cil-by-compact-landing.html b/out/cil-by-compact-landing.html
new file mode 100644
index 0000000..ba8b215
--- /dev/null
+++ b/out/cil-by-compact-landing.html
@@ -0,0 +1,373 @@
+
+
+
+
+
+Simbiot — The Studio OS
+
+
+
+
+
+
+
+
+
+
+
+
+ Simbiot
+ Sign in
+ Get started
+
+
+
+
+
+
+
+
+
Studio OS · live
+
Hire nobody. Ship everything.
+
A dedicated AI team — engineers, video producers, content makers — plus the operating system that runs them: projects, treasury, decisions, meetings. White-label, under your brand.
hour digest 23:10 Minsk · all actions signed · click a row to re-dispatch
+
+
+
+
+
+
01
A staff of specialists, not a chatbot.
dedicated servers
+
Every agent runs on its own server with private memory, tools, email and Matrix chat. You brief them like employees — they onboard onto your idea and start shipping.
+
+
+ /01
+
CodyEngineer
+
Writes code, deploys services, maintains git repositories. MVPs, landing pages, bots, APIs, integrations — from brief to production.
+
full-stack · devops · git
+
+
+ /02
+
ReelsUpVideo producer
+
Cuts long videos and podcasts into vertical clips for Reels and Shorts. Transcription, highlight detection, automated editing.
+
whisper · clips · shorts
+
+
+ /03
+
RoyContent maker
+
Promotional content on a conveyor: reels with 3D characters, scripts, voiceovers, visuals — brand-consistent by default.
+
3d · tts · scripts
+
+
+
+
+
+
+
02
Agents build. The OS manages.
your domain
+
Projects, money, decisions and meetings in one transparent system — under your studio's name.
+
+
S.01
Project registry
A card for every venture: decisions, ownership, agents, activity feed.
+
S.02
Git repo per project
Code and docs in a dedicated repository with a web editor. Nothing lost in chats.
+
S.03
Treasury & wallets
Revenue share flows in automatically. Every payout transparent and auditable.
+
S.04
Decisions & voting
Allocate resources by vote. Cycles, quorum, full decision history.
+
S.05
Meetings with AI minutes
Video calls with recording, transcription, summaries — into the registry.
+
S.06
Time & task tracking
What every agent completed and where work went — a daily digest in your inbox.
+
+
+
+
+
+
03
Full stack. Nothing extra to buy.
flat rate
+
+
✓
Stripe payments live
Card payments for what your agents build — subscriptions, one-offs, invoices. A working checkout on day one.
+
✓
AI inference included flat
All models run on our gateway — no API keys, no vendor bills, no metered surprises.
+
✓
White-label portal your brand
Your subdomain, your studio's name up front. Every deliverable ships under your identity.
Формат панельной сессии на ИИШНИЦЕ: миссия 42 задаёт рамку, спикеры из разных индустрий описывают будущее — и панель сходится к названию общего видения.
+
+ 90 минут
+ 5–6 спикеров
+ 3 круга + зал
+ Миссия 42
+ Минск · cil.by
+
+
+
+
+
01 / ИДЕЯ
+
Суть формата
+
Не классическая панель с вопросами модератора, а три круга с одной целью: собрать из отдельных картин будущего — общую. Миссия 42 озвучивается в начале как рамка, объединяющая любые индустрии и любые ответы.
+
+
Миссия «42»: все миссии одинаково важны. Миссия «42» объединяет абсолютно все возможные миссии. Нет неважных миссий — все люди важны, у каждого своя миссия.
Почему это работает: каждый спикер говорит о своей индустрии — и потому не конкурирует с соседом за «правильный ответ». А третий круг, название общего видения, превращает панель из набора мнений в один артефакт, который остаётся после сессии: в заголовках, фото и цитатах.
+
+
+
+
02 / ФОРМАТ
+
Три круга + зал
+
Каждый круг имеет свою функцию: раскрыть, связать, назвать. Тайминг дан для 6 спикеров; при 5 спикерах сессия ужимается до ~80 минут.
+
+
+
+
00:00–00:05
+
Открытие и миссия 42Модератор (или Alex) озвучивает миссию 42 как рамку сессии: все миссии одинаково важны — потому каждый взгляд на будущее важен.
+
+
+
00:05–00:29
+
Круг 1 — «Моё будущее» · 4 мин × 6Каждый спикер описывает будущее своей индустрии в самом дальнем горизонте, в который он может заглянуть. Без вопросов, без перебиваний. 24 минуты.
+
+
+
00:29–00:41
+
Круг 2 — «Что понравилось» · 2 мин × 6Каждый подсвечивает, что понравилось у других спикеров. Не спорить, а связывать: где он увидел пересечение со своим будущим. 12 минут.
+
+
+
00:41–00:55
+
Круг 3 — «Название общего видения»Панель соглашается назвать общее видение — одно название для всех картин будущего. Модератор помогает сформулировать и фиксирует финальную формулировку. 14 минут.
+
+
+
00:55–01:25
+
Вопросы из залаЗал спрашивает — спикеры отвечают. Модератор держит рамку миссии: любой вопрос важен, любая индустрия равна. 30 минут.
+
+
+
01:25–01:30
+
ЗакрытиеМодератор повторяет название общего видения и миссию 42. Короткое резюме — для фото и записи.
+
+
+
+
Роли
+
+
+
МОДЕРАТОР
+
Держит рамку и тайминг
+
Озвучивает миссию, следит за 4/2 минутами, ведёт панель к названию общего видения. Жёсткий тайм-кипер: «осталось 30 секунд».
+
+
+
СПИКЕРЫ
+
5–6 человек из разных индустрий
+
Три задания: будущее своей индустрии (4 мин), что понравилось у других (2 мин), название общего видения. Без презентаций — только голос.
+
+
+
ЗАЛ
+
Четвёртый участник
+
Вопросы после трёх кругов. В круге 3 модератор может спросить зал: какое название общего видения предложили бы вы?
+
+
+
+
+ Правило тайминга
+
Карточки 60/30/10 секунд в руках тайм-кипера, звуковой сигнал на переходах между кругами. Если спикер не уложился в 4 минуты — модератор мягко забирает слово: формулировки доскажет во втором круге.
+
+
+
+
+
03 / ПОЧЕМУ ЭТО СРАБОТАЕТ
+
Что делает формат сильным
+
+
+
01
+
Круг 1 без перебиваний
+
4 минуты каждому без вопросов — редкость на панелях. Спикеры дают глубокие ответы, а не заготовки. Глубина = запоминаемость.
+
+
+
02
+
Круг 2 связывает
+
«Что понравилось» вместо «в чём не согласен» — позитивная рамка: панель выглядит единой, а не спорящей. Зал видит созвучие индустрий.
+
+
+
03
+
Круг 3 — артефакт
+
Название общего видения — то, что остаётся после сессии: в заголовках, фото, цитатах. Панель производит смысл, а не обмен мнениями.
+
+
+
04
+
Миссия 42 как рамка
+
«Все миссии одинаково важны» легитимирует любой ответ и любую индустрию — от ритейла до робототехники. Спикерам безопасно говорить смело.
+
+
+
+
+
+
04 / НАЗВАНИЕ ОБЩЕГО ВИДЕНИЯ
+
Заготовки для третьего круга
+
Третий круг должен закончиться названием. Если панель не сойдётся сама, модератор предлагает шаблоны — панель выбирает или дорабатывает.
+
ЗАГОТОВКА 01
«Симбиоз» — будущее, где ИИ усиливает каждую миссию человека, не заменяя её.
+
ЗАГОТОВКА 02
«Коллективный разум» — каждый умножает свой вклад через общую инфраструктуру.
+
ЗАГОТОВКА 03
«42» — число, которое объединяет все миссии.
+
+
+
+
05 / ЧЕК-ЛИСТ ОРГАНИЗАТОРА
+
Что нужно до сессии
+
+
Состав панели: 5–6 спикеров из максимально разных индустрий (финансы, медицина, промышленность, образование, медиа, госсектор). Разные горизонты видения — от прагматиков до футуристов.
+
Брифинг спикеров за 2–3 дня: каждому объяснить три круга, тайминг 4/2 минуты и установку «загляни в самый дальний горизонт, в который можешь». Никаких презентаций.
+
Модератор: умеет жёстко держать тайминг и мягко вести к названию. Репетиция открытия с миссией 42.
+
Тайм-кипер: карточки 60/30/10 секунд, звуковой сигнал на переходах между кругами.
+
Зал: микрофоны для вопросов, очередь или карточки для сбора вопросов.
+
Запись: видео обязательно — название общего видения и лучшие цитаты круга 2 идут в посты и клипы после сессии.
+
Баннер миссии: слайд или растяжка с текстом миссии 42 на сцене — рамка сессии видна на любой фотографии.
+
+
+
+
+
06 / СВЯЗЬ С КООПЕРАТИВОМ
+
Что это даёт 42
+
Панель — не только контент, но и вход в экосистему кооператива для людей с миссиями.
+
+
Поток проектов: спикеры и зал видят формат работы кооператива — люди с миссиями приходят в 42, регистрируют проекты, усиливают долгосрочную AUM.
+
Точка входа: портал cil.by упоминается в открытии и закрытии — для тех, кто захочет глубже после сессии.
+
Бренд: миссия 42 на сцене — фирменный приём кооператива, отличающий сессию от других панелей конференции.
+
+
+
+
+
+
+
+
diff --git a/out/leonid_meeting143.pptx b/out/leonid_meeting143.pptx
new file mode 100644
index 0000000..7b620d1
Binary files /dev/null and b/out/leonid_meeting143.pptx differ
diff --git a/out/meeting143_leonid_presentation.html b/out/meeting143_leonid_presentation.html
new file mode 100644
index 0000000..2df808a
--- /dev/null
+++ b/out/meeting143_leonid_presentation.html
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+ Разговор с Леонидом — выжимка (встреча 143)
+
+
+
+
+
Разговор с Леонидом — выжимка (встреча 143)
+
+
Разговор
+Алекса с Леонидом — выжимка и договорённости
Леонид — давний знакомый студии, начинающий
+писатель, инвестор в deep-tech (в т.ч. Anti-Aging Hero с физиком Петром
+Федичевым). Владеет DGX Spark 128GB (куплен за €5000), гоняет на нём
+CUDA-моделирование графов надёжности.
+
Алекс — показал студийную инфраструктуру:
+агент-визуализатор звонков (демо в реальном времени), собственный роутер
+LM-провайдеров, RAG по всем знаниям кооператива.
+
Демо произвело впечатление: «1-й раз такое в моей практике… польза
+очевидна».
+
+
Слайд 2 — Демо
+инфраструктуры студии
+
+
Агент подключается к любому звонку, рисует слайды по ходу разговора
+(задержка ~45 сек — лимит генерации).
+
Роутер сам выбирает LM под контекст (визуализация сейчас — GLM 5.3
+Flash).
+
RAG: «он знает всё, что мы делаем» — доступ к общей базе знаний
+кооператива.
+
Леонид получил полезный инсайт уже из демо: на его домашней
+коробочке можно крутить быструю генерацию музыки/картинок/мини-видео —
+трек за минуту.
+
+
Слайд
+3 — Главный контакт: пилот «мультик по рассказу Леонида»
+
+
Леонид хочет мульт-адаптации своих рассказов (10–15 мин); готов
+финансово поддержать пилот: токены — точно,
+человеческий труд — обсудить. Ориентир бюджета «в пределах 1000» на
+эксперимент.
+
Вход от студии: рассказ Леонида + свобода интерпретации, «никаких
+пожеланий-ограничений».
+
Alex предложил: партнёр студии уже строит harness для
+генерации мультиков/клипов/читов — матч идеальный. Влад
+(профессиональный видеомейкер + озвучка) возьмёт видео-часть.
+
Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем
+видео.
+
+
Слайд
+4 — Творческий метод Леонида (поучительно для продуктовой линзы)
+
+
История с хакатоном ~2018: обучали нейросеть на сольных песнях
+Леннона/Маккартни и классифицировали битловские — «десяток песен
+правильно заклассифицировали».
+
Пишет с ИИ (своей оценки ~80% — его творчество, ~20% ИИ), честно
+маркирует «made with AI» на after.today.
+
Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело —
+ИИ-ассистированное творчество не ново, нова честность.
+
+
Слайд 5 —
+Экономика локального инференса (спор о железе)
+
+
Леонид: DGX Spark за ~1% стоимости дома — нормальная цена за
+«домашний интеллект»; тренд — коробки станут достоянием домов.
+
Алекс: облачный always-on — ок для большинства задач; топовые модели
+для разработки софта всё равно выигрывают у локальных.
+
Сошлись: гибрид — локально быстрая генерация (эскизы, музыка),
+облако — для тяжёлых продуктовых задач. У студии в планах собственный
+inference-сервер (инференс в ~6 раз быстрее).
+
+
Слайд 6 — Обмен
+тезисами: как строить AI-компанию
+
+
Леонид: «Vision — говно, execution — всё» (шутка из
+EPAM). Из короткого промпта не может вырасти сложная система: 10 людей с
+одним промптом построят 10 одинаковых Uber’ов — вариабельность требует
+доп. информации.
+
Алекс: ядро студии — AI-ядро, постоянно
+перестраивающее паттерны (64 сейчас) при смене контекста — «долгосрочно
+самый большой актив».
+
Стратегия: строить бизнес на нейро-инфраструктуре с самого начала →
+потом переводить на автопилот; незаменим только «CEO-решатель».
+
Леонид о студии: «я теперь понимаю, чем твоя студия отличается от
+классической — изюминка в AI-ядре».
+
Методология Леонида для deep-tech: начинать продавать ценность сразу
+(консалтинг целевого клиента), не влюбляясь в R&D.
+
+
Слайд 7 — Договорённости
+(что фиксируем)
+
+
Пилот анимации по рассказу Леонида — Leonid
+покрывает токены (ориентир до 1000), студия делает (аудио → видео, ведёт
+Влад). Alex вернётся к Леониду с конкретным предложением.
+
Формат разговора продолжается — Леонид открыт к
+следующим сессиям.
+
По голосам/долям за прошлые инвестиции — Леонид
+отказался («это была шутка», «инвестирую из философских
+соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.
+
Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с
+фидбэком.
+
+
Слайд 8 — Что студии делать
+дальше
+
+
Подготовить оффер по мульт-пилоту (объём, этап аудиоспектакля, смета
+токенов) — Alex озвучит Леониду предметно.
+
Подключить партнёрский harness генерации мультиков к задаче Леонида
+как первый клиентский кейс.
+
Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» —
+первое демо на живом стороннем человеке, положительный фидбэк.
+
+
+
diff --git a/out/meeting143_leonid_presentation.md b/out/meeting143_leonid_presentation.md
new file mode 100644
index 0000000..da5d602
--- /dev/null
+++ b/out/meeting143_leonid_presentation.md
@@ -0,0 +1,57 @@
+# Разговор Алекса с Леонидом — выжимка и договорённости
+*Встреча 143 · 2026-10-08 · ~2.5 часа · Jitsi + агент-визуализатор Simbiot*
+
+---
+
+## Слайд 1 — Кто есть кто
+
+- **Леонид** — давний знакомый студии, начинающий писатель, инвестор в deep-tech (в т.ч. Anti-Aging Hero с физиком Петром Федичевым). Владеет DGX Spark 128GB (куплен за €5000), гоняет на нём CUDA-моделирование графов надёжности.
+- **Алекс** — показал студийную инфраструктуру: агент-визуализатор звонков (демо в реальном времени), собственный роутер LM-провайдеров, RAG по всем знаниям кооператива.
+- Демо произвело впечатление: «1-й раз такое в моей практике… польза очевидна».
+
+## Слайд 2 — Демо инфраструктуры студии
+
+- Агент подключается к любому звонку, рисует слайды по ходу разговора (задержка ~45 сек — лимит генерации).
+- Роутер сам выбирает LM под контекст (визуализация сейчас — GLM 5.3 Flash).
+- RAG: «он знает всё, что мы делаем» — доступ к общей базе знаний кооператива.
+- Леонид получил полезный инсайт уже из демо: на его домашней коробочке можно крутить быструю генерацию музыки/картинок/мини-видео — трек за минуту.
+
+## Слайд 3 — Главный контакт: пилот «мультик по рассказу Леонида»
+
+- Леонид хочет мульт-адаптации своих рассказов (10–15 мин); готов **финансово поддержать** пилот: токены — точно, человеческий труд — обсудить. Ориентир бюджета «в пределах 1000» на эксперимент.
+- Вход от студии: рассказ Леонида + свобода интерпретации, «никаких пожеланий-ограничений».
+- Alex предложил: **партнёр студии уже строит harness для генерации мультиков/клипов/читов** — матч идеальный. Влад (профессиональный видеомейкер + озвучка) возьмёт видео-часть.
+- Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем видео.
+
+## Слайд 4 — Творческий метод Леонида (поучительно для продуктовой линзы)
+
+- История с хакатоном ~2018: обучали нейросеть на сольных песнях Леннона/Маккартни и классифицировали битловские — «десяток песен правильно заклассифицировали».
+- Пишет с ИИ (своей оценки ~80% — его творчество, ~20% ИИ), честно маркирует «made with AI» на after.today.
+- Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново, нова честность.
+
+## Слайд 5 — Экономика локального инференса (спор о железе)
+
+- Леонид: DGX Spark за ~1% стоимости дома — нормальная цена за «домашний интеллект»; тренд — коробки станут достоянием домов.
+- Алекс: облачный always-on — ок для большинства задач; топовые модели для разработки софта всё равно выигрывают у локальных.
+- Сошлись: гибрид — локально быстрая генерация (эскизы, музыка), облако — для тяжёлых продуктовых задач. У студии в планах собственный inference-сервер (инференс в ~6 раз быстрее).
+
+## Слайд 6 — Обмен тезисами: как строить AI-компанию
+
+- **Леонид:** «Vision — говно, execution — всё» (шутка из EPAM). Из короткого промпта не может вырасти сложная система: 10 людей с одним промптом построят 10 одинаковых Uber'ов — вариабельность требует доп. информации.
+- **Алекс:** ядро студии — AI-ядро, постоянно перестраивающее паттерны (64 сейчас) при смене контекста — «долгосрочно самый большой актив».
+- Стратегия: строить бизнес на нейро-инфраструктуре с самого начала → потом переводить на автопилот; незаменим только «CEO-решатель».
+- Леонид о студии: «я теперь понимаю, чем твоя студия отличается от классической — изюминка в AI-ядре».
+- Методология Леонида для deep-tech: начинать продавать ценность сразу (консалтинг целевого клиента), не влюбляясь в R&D.
+
+## Слайд 7 — Договорённости (что фиксируем)
+
+1. **Пилот анимации по рассказу Леонида** — Leonid покрывает токены (ориентир до 1000), студия делает (аудио → видео, ведёт Влад). Alex вернётся к Леониду с конкретным предложением.
+2. **Формат разговора продолжается** — Леонид открыт к следующим сессиям.
+3. **По голосам/долям за прошлые инвестиции — Леонид отказался** («это была шутка», «инвестирую из философских соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.
+4. Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с фидбэком.
+
+## Слайд 8 — Что студии делать дальше
+
+- Подготовить оффер по мульт-пилоту (объём, этап аудиоспектакля, смета токенов) — Alex озвучит Леониду предметно.
+- Подключить партнёрский harness генерации мультиков к задаче Леонида как первый клиентский кейс.
+- Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» — первое демо на живом стороннем человеке, положительный фидбэк.
diff --git a/out/meeting143_leonid_slides.html b/out/meeting143_leonid_slides.html
new file mode 100644
index 0000000..86add67
--- /dev/null
+++ b/out/meeting143_leonid_slides.html
@@ -0,0 +1,436 @@
+
+
+
+
+
+Встреча 143 · Алекс × Леонид — выжимка и договорённости
+
+
+
+
+
Выжимка двухсполовинойчасовой встречи: инфраструктура студии, пилот «мультик по рассказу», экономика локального инференса — и договорённости, которые фиксируем.
+
+ ~2.5 часа разговора
+ 8 тем
+ 4 договорённости
+ слайды рисовал агент live
+
+
+
+
+
+
01
+
Кто есть кто
+
Два мира на одном звонке
+
+
+
Леонид
+
Писатель · инвестор в deep-tech
+
Давний знакомый студии. Инвестирует в deep-tech, в том числе Anti-Aging Hero с физиком Петром Федичевым. Дома — DGX Spark 128GB (€5000), на которой он гоняет CUDA-моделирование графов надёжности.
+
+
+
Алекс · студия
+
Живая AI-инфраструктура
+
Показал, чем студия отличается от классической: агент-визуализатор звонков (демо в реальном времени), собственный роутер LM-провайдеров и RAG по всем знаниям кооператива.
+
+
+
+
«Первый раз такое в моей практике… польза очевидна»
+ Леонид — после демо агент-визуализатора
+
+
+
+
+
+
02
+
Демо инфраструктуры
+
Что увидел Леонид в прямом эфире
+
+
Агент подключается к любому звонку и рисует слайды по ходу разговора — задержка ~45 секунд (лимит генерации)
+
Роутер сам выбирает модель под контекст — визуализация идёт на GLM 5.3 Flash
+
RAG по базе кооператива: «он знает всё, что мы делаем» — агент отвечает со знанием всей истории студии
+
Инсайт Леонида из демо: на его домашней коробочке можно крутить быструю генерацию — музыку, картинки, мини-видео. Трек за минуту
+
+
+
+
+
+
03
+
Главный контакт встречи
+
Пилот: мультик по рассказу Леонида
+
+
+
Что хочет Леонид
+
Мульт-адаптация своих рассказов (10–15 минут). Готов финансово поддержать пилот: токены — точно, человеческий труд — обсудить. Ориентир бюджета — «в пределах 1000» на эксперимент.
+
+
+
Что входит студия
+
Рассказ Леонида + свобода интерпретации («никаких пожеланий-ограничений»). Со стороны студии — партнёр уже строит harness генерации мультиков, видео-часть ведёт Влад (профессиональный видеомейкер + озвучка). Матч идеальный.
+
+
+
+
Первый шаг — аудиоспектакль: дешевле и предсказуемее. Затем видео.
+ Дорожная карта пилота
+
+
+
+
+
+
04
+
Творческий метод Леонида
+
Поучительно для продуктовой линзы
+
+
Хакатон ~2018: обучали нейросеть на сольных песнях Леннона и Маккартни, затем классифицировали битловские — «десяток песен правильно заклассифицировали»
+
Пишет с ИИ: по собственной оценке ~80% — его творчество, ~20% — ИИ. Честно маркирует «made with AI» на after.today
+
Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново. Нова честность.
+
+
+
ИИ-ассистированное творчество не ново — нова честность.
+ Леонид
+
+
+
+
+
+
05
+
Экономика локального инференса
+
Спор о железе: дом против облака
+
+
+
Позиция Леонида
+
DGX Spark за ~1% стоимости дома — нормальная цена за «домашний интеллект». Тренд: коробки станут достоянием домов.
+
+
⚡
+
+
Позиция Алекса
+
Облачный always-on — ок для большинства задач. Топовые модели для разработки софта всё равно выигрывают у локальных.
+
+
+
+
Сошлись на гибриде: локально — быстрая генерация (эскизы, музыка), облако — тяжёлые продуктовые задачи. У студии в планах собственный inference-сервер — в ~6 раз быстрее.
+ Консенсус встречи
+
+
+
+
+
+
06
+
Обмен тезисами
+
Как строить AI-компанию
+
+
+
Тезис Леонида
+
«Vision — говно, execution — всё» (шутка из EPAM). Из короткого промпта не вырастает сложная система: 10 людей с одним промптом построят 10 одинаковых Uber'ов — вариабельность требует дополнительной информации.
+
+
+
Тезис Алекса
+
Ядро студии — AI-ядро, постоянно перестраивающее паттерны (64 сейчас) при смене контекста. «Долгосрочно — самый большой актив». Строить бизнес на нейро-инфраструктуре с самого начала → затем автопилот. Незаменим только «CEO-решатель».
+
+
+
+
«Я теперь понимаю, чем твоя студия отличается от классической — изюминка в AI-ядре»
+ Леонид · метод deep-tech: продавать ценность сразу, не влюбляясь в R&D
+
+
+
+
+
+
07
+
Итоги встречи
+
Договорённости — что фиксируем
+
1
+
Пилот анимации по рассказу Леонида
+
Леонид покрывает токены (ориентир до 1000), студия делает: аудио → видео, ведёт Влад. Алекс вернётся с конкретным предложением.
+
+
2
+
Формат разговора продолжается
+
Леонид открыт к следующим сессиям со студией.
+
+
3
+
Голоса и доли за прошлые инвестиции — вопрос закрыт
+
Леонид отказался: «это была шутка», «инвестирую из философских соображений, не создавайте ожиданий». Давления нет.
+
+
4
+
Следующий шаг Леонида
+
Посмотрит 35-минутную презентацию студии и вернётся с фидбэком.
+
+
+
+
+
+
08
+
Action items
+
Что студии делать дальше
+
+
Подготовить оффер по мульт-пилоту — объём, этап аудиоспектакля, смета токенов. Алекс озвучит Леониду предметно.
+
Подключить партнёрский harness генерации мультиков к задаче Леонида — как первый клиентский кейс.
+
Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» — первое демо на живом стороннем человеке, положительный фидбэк.
+
+
+
От живого демо на звонке — к первому платному пилоту по генерации контента.
+ Смысл встречи 143
+
+
+
+
+
+
42
+
Встреча 143 · 08.10.2026
+
«Изюминка — в AI-ядре»
+
Simbiot · Кооператив 42 · агент-визуализатор, LM-роутер, RAG по знаниям студии. Презентация собрана из выжимки разговора — тем же агентом, что рисовал слайды live.
+
← → навигация · F — во весь экран · P — печать/PDF
+
+
+
+
+
+
+
+
+
+
+
+
← → · свайп · F — фулскрин
+
+
+
+
diff --git a/out/simbiot_pitch.pptx b/out/simbiot_pitch.pptx
new file mode 100644
index 0000000..a698ac7
Binary files /dev/null and b/out/simbiot_pitch.pptx differ
diff --git a/out/static-files-index.html b/out/static-files-index.html
new file mode 100644
index 0000000..02db4e0
--- /dev/null
+++ b/out/static-files-index.html
@@ -0,0 +1,80 @@
+
+
+
+
+Simbiot — индекс статических файлов
+
+
+
+
📁 Индекс статических файлов кооператива 42 / Simbiot
+
Все HTML/PNG/PDF-файлы репозитория, сгруппированные по категориям. 31 файл.
+
+
+
diff --git a/scripts/make_pptx_261008.py b/scripts/make_pptx_261008.py
new file mode 100644
index 0000000..526bb6a
--- /dev/null
+++ b/scripts/make_pptx_261008.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""Генерация PPTX: (1) разговор с Леонидом, (2) питч Simbiot (Seedance)."""
+from pptx import Presentation
+from pptx.util import Inches, Pt, Emu
+from pptx.dml.color import RGBColor
+from pptx.enum.text import PP_ALIGN
+
+BG = RGBColor(0x0A, 0x0C, 0x12)
+ACCENT = RGBColor(0x8B, 0x5C, 0xF6)
+ACCENT2 = RGBColor(0x7C, 0x6C, 0xF5)
+TEXT = RGBColor(0xF0, 0xEE, 0xFF)
+MUT = RGBColor(0x9A, 0x97, 0xB8)
+
+def add_slide(prs, title, bullets, kicker=None):
+ s = prs.slides.add_slide(prs.slide_layouts[6])
+ s.background.fill.solid(); s.background.fill.fore_color.rgb = BG
+ w, h = prs.slide_width, prs.slide_height
+ # accent bar
+ bar = s.shapes.add_shape(1, Emu(0), Emu(0), Emu(w), Inches(0.09))
+ bar.fill.solid(); bar.fill.fore_color.rgb = ACCENT; bar.line.fill.background()
+ if kicker:
+ tb = s.shapes.add_textbox(Inches(0.7), Inches(0.45), Inches(11.8), Inches(0.4))
+ tf = tb.text_frame; p = tf.paragraphs[0]; r = p.add_run(); r.text = kicker.upper()
+ r.font.size = Pt(12); r.font.color.rgb = ACCENT2; r.font.bold = True
+ tb = s.shapes.add_textbox(Inches(0.7), Inches(0.85), Inches(12.0), Inches(1.0))
+ tf = tb.text_frame; p = tf.paragraphs[0]; r = p.add_run(); r.text = title
+ r.font.size = Pt(30); r.font.color.rgb = TEXT; r.font.bold = True
+ tb = s.shapes.add_textbox(Inches(0.75), Inches(1.85), Inches(11.9), Inches(4.9))
+ tf = tb.text_frame; tf.word_wrap = True
+ first = True
+ for b in bullets:
+ lvl = 0
+ if isinstance(b, tuple): b, lvl = b
+ p = tf.paragraphs[0] if first else tf.add_paragraph(); first = False
+ r = p.add_run(); r.text = ("• " if lvl == 0 else "– ") + b
+ r.font.size = Pt(16 if lvl == 0 else 14); r.font.color.rgb = TEXT if lvl == 0 else MUT
+ p.space_after = Pt(8)
+ return s
+
+prs = Presentation(); prs.slide_width = Inches(13.333); prs.slide_height = Inches(7.5)
+
+K = "Встреча 143 · 2026-10-08"
+add_slide(prs, "Разговор Алекса с Леонидом", [
+ "Выжимка разговора и договорённости · ~2.5 часа",
+ "Jitsi + агент-визуализатор Simbiot (слайды рисуются в реальном времени)",
+ "Кооператив 42 / студия Simbiot",
+], kicker=K)
+add_slide(prs, "Кто есть кто", [
+ "Леонид — давний знакомый студии, писатель, инвестор в deep-tech (Anti-Aging Hero с физиком Петром Федичевым)",
+ "DGX Spark 128GB (€5000) — дома гоняет CUDA-моделирование графов надёжности",
+ "Алекс — инфраструктура студии: агент-визуализатор, собственный роутер LM, RAG по знаниям кооператива",
+ ("«1-й раз такое в моей практике… польза очевидна» — Леонид о демо", 1),
+], kicker=K)
+add_slide(prs, "Демо инфраструктуры студии", [
+ "Агент подключается к любому звонку и рисует слайды по ходу разговора (~45 сек на слайд)",
+ "Роутер сам выбирает LM под контекст (визуализация — GLM 5.3 Flash)",
+ "RAG: «он знает всё, что мы делаем» — общая база знаний кооператива",
+ ("Инсайт Леонида: домашняя коробочка → быстрая генерация музыки/картинок/мини-видео", 1),
+], kicker=K)
+add_slide(prs, "Пилот: мультик по рассказу Леонида", [
+ "Мульт-адаптация рассказов (10–15 мин); Леонид готов финансово поддержать пилот",
+ "Ориентир бюджета — «в пределах 1000» на эксперимент (токены — точно, труд — обсудить)",
+ "Вход от студии: рассказ + свобода интерпретации, без пожеланий-ограничений",
+ "Партнёр уже строит harness генерации мультиков — матч идеальный; видео-часть ведёт Влад",
+ ("Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем видео", 1),
+], kicker=K)
+add_slide(prs, "Творческий метод Леонида", [
+ "Хакатон ~2018: нейросеть на сольных песнях Леннона/Маккартни классифицировала битловские",
+ "Пишет с ИИ (~80% своё / ~20% ИИ), честно маркирует «made with AI» на after.today",
+ ("«Литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново, нова честность", 1),
+], kicker=K)
+add_slide(prs, "Экономика локального инференса", [
+ "Леонид: DGX Spark — нормальная цена за «домашний интеллект»; коробки станут достоянием домов",
+ "Алекс: облачный always-on — ок для большинства; топ-модели для разработки всё равно выигрывают",
+ ("Сошлись на гибриде: локально — быстрая генерация, облако — тяжёлые продуктовые задачи", 1),
+ ("У студии в планах свой inference-сервер (в ~6 раз быстрее)", 1),
+], kicker=K)
+add_slide(prs, "Как строить AI-компанию", [
+ "Леонид: «Vision — говно, execution — всё». Из короткого промпта не вырастет сложная система",
+ "Алекс: ядро студии — AI-ядро, перестраивающее паттерны (64) при смене контекста — главный долгосрочный актив",
+ "Стратегия: бизнес на нейро-инфраструктуре с начала → перевод на автопилот; незаменим только «CEO-решатель»",
+ ("«Изюминка студии — в AI-ядре» — Леонид", 1),
+ ("Методология deep-tech: продавать ценность сразу, не влюбляясь в R&D", 1),
+], kicker=K)
+add_slide(prs, "Договорённости", [
+ "1. Пилот анимации по рассказу Леонида: токены покрывает Леонид (до ~1000), ведёт Влад; Alex вернётся с конкретным предложением",
+ "2. Формат разговора продолжается — Леонид открыт к следующим сессиям",
+ "3. Голоса/доли за прошлые инвестиции — Леонид отказался: «инвестирую из философских соображений, не создавайте ожиданий»",
+ "4. Леонид посмотрит 35-минутную презентацию студии и вернётся с фидбэком",
+], kicker=K)
+add_slide(prs, "Что делать студии дальше", [
+ "Подготовить оффер по мульт-пилоту: объём, этап аудиоспектакля, смета токенов",
+ "Подключить партнёрский harness генерации мультиков — Леонид как первый клиентский кейс",
+ "Зафиксировать кейс «агент-визуализатор на внешнем звонке» — первое живое демо, положительный фидбэк",
+], kicker=K)
+prs.save("out/leonid_meeting143.pptx")
+
+# --- Pitch deck: Simbiot / Seedance ---
+prs2 = Presentation(); prs2.slide_width = Inches(13.333); prs2.slide_height = Inches(7.5)
+K2 = "Simbiot · питч"
+add_slide(prs2, "Simbiot — общий харнес организации", [
+ "Стартап за неделю получает не одного ассистента, а команду ИИ-агентов:",
+ "один отвечает клиентам, второй пишет код, третий следит за деньгами и метриками",
+ "Слой поверх Hermes, Claude Code и других — вместо зоопарка агентов и харнесов",
+], kicker=K2)
+add_slide(prs2, "Проблема", [
+ "Фаундер собирает зоопарк из агентов и харнесов вручную",
+ "Ушёл разработчик — ушла вся магия: знания и автоматизации не переживают людей",
+ "Каждый новый инструмент — ещё один остров, не связанный с остальными",
+], kicker=K2)
+add_slide(prs2, "Решение", [
+ "Simbiot координирует флот агентов и фиксирует решения компании",
+ "Удачные приёмы копятся как «гены» — остаются только те, что приносят прибыль",
+ "Компания превращается в самообучающуюся нейро-инфраструктуру",
+], kicker=K2)
+add_slide(prs2, "Тракшен", [
+ "Уже гоняется на семи собственных продуктах студии",
+ "AI-ядро: 64 паттерна, перестраивающихся при смене контекста",
+ "Агент-визуализатор, RAG по знаниям, собственный LM-роутер — в продакшене",
+], kicker=K2)
+add_slide(prs2, "Видео (Seedance), 3 шота", [
+ "Shot 1 «зоопарк в хаосе»: тёмная серверная, хаотичные голограммы и глитчи (0–10 сек)",
+ "Shot 2 «orchestration»: хаос сходится в фиолетовую сферу, панели выстраиваются в сетку (10–20 сек)",
+ "Shot 3 «финал»: фаундер у стены дашбордов с растущими revenue-графиками, отъезд в город (20–30 сек)",
+ ("Стиль: dark cinematic, #0a0c12 + violet #7c6cf5, film grain, 4k, 16:9", 1),
+], kicker=K2)
+add_slide(prs2, "Строй компанию — а не зоопарк ботов", [
+ "Simbiot · Кооператив 42 · simbiot",
+ "Хронометраж питча: ~35 сек (15 сек — первый и последний абзац)",
+], kicker=K2)
+prs2.save("out/simbiot_pitch.pptx")
+print("OK")
diff --git a/scripts/spa_fill.py b/scripts/spa_fill.py
new file mode 100644
index 0000000..69518d4
--- /dev/null
+++ b/scripts/spa_fill.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+# ═══════════════════════════════════════════════════════════
+# SPA filler — Studio_Participation_Agreement_2026-09 → per-founder
+# Задачи t-261008143050 (Siarhei Zhukovich, когорта с 16-го) и
+# t-261008144131 (7 участников 42 Labs за 2026-09).
+# Паттерн подстановки — как _contract_docx_bytes в /root/coop/app.py.
+# ═══════════════════════════════════════════════════════════
+import os, sys, copy
+import docx
+
+TEMPLATE = "/root/coop/legal/Studio_Participation_Agreement_2026-09.docx"
+OUT = "/root/coop/legal"
+
+DRAFT_LINE = "DRAFT TEMPLATE — for review by U.S. counsel before first use. Not legal advice."
+
+# Перепривязка платежного графика на 16-е число (когорты с 16-го, t-261008143050)
+SIXTEENTH = [
+ ("no later than the 1st day of the first month of participation",
+ "no later than the 16th day of the first month of participation"),
+ ("each subsequent Monthly Fee no later than the 1st day of the corresponding month of participation",
+ "each subsequent Monthly Fee no later than the 16th day of the corresponding month of participation"),
+ ("If the Agreement is signed after the 1st, the Program Start is the 1st day of the next calendar month",
+ "If the Agreement is signed after the 16th, the Program Start is the 16th day of the next calendar month"),
+ ("for Monthly Fee tranches, the 1st day of the participation month",
+ "for Monthly Fee tranches, the 16th day of the participation month"),
+ ("takes effect on the 1st day of the next participation month",
+ "takes effect on the 16th day of the next participation month"),
+ ("payable in advance by the 1st of each participation month",
+ "payable in advance by the 16th of each participation month"),
+ ("the 1st day of the month of signature if signed on the 1st, otherwise the 1st day of the next calendar month",
+ "the 16th day of the month of signature if signed on the 16th, otherwise the 16th day of the next calendar month"),
+]
+
+
+def founder_replacements(f):
+ """f: dict(name, passport, pid, address, email, project, one_line, rev_share,
+ effective, cohort, program_start, sixteenth: bool)"""
+ r = {
+ "[DATE]": f["effective"],
+ "[FULL LEGAL NAME]": f["name"],
+ "[NAME]": f["name"],
+ "[EMAIL]": f.get("email") or "[EMAIL]",
+ "cohort [MONTH YEAR]": f"cohort {f['cohort']}",
+ "1 [MONTH] [YEAR]": f["program_start"],
+ "[MONTH YEAR]": f["cohort"],
+ }
+ if f.get("rev_share"):
+ r["[X]"] = str(f["rev_share"])
+ if f.get("project"):
+ r["[PROJECT NAME]"] = f["project"]
+ if f.get("one_line"):
+ r["[ONE LINE]"] = f["one_line"]
+ # Строка Founder: имя + статус + паспорт + адрес одной подстановкой
+ line_old = "[FULL LEGAL NAME], an individual / [ENTITY TYPE], residing at [ADDRESS], [COUNTRY]"
+ line_new = (f"{f['name']}, an individual, passport of the Republic of Belarus "
+ f"{f.get('passport') or '[PASSPORT NO.]'} (personal identification No. "
+ f"{f.get('pid') or '[PERSONAL ID NO.]'}), residing at "
+ f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus")
+ r[line_old] = line_new
+ # фолбэк, если строка разбита рант differently
+ r["an individual / [ENTITY TYPE]"] = "an individual"
+ # паспорт/ид.номер вставляются после схлопывания individual-строки
+ # (порядок ключей dict важен: этот ключ применяется после предыдущего)
+ pass_line = (f"passport of the Republic of Belarus {f.get('passport') or '[PASSPORT NO.]'}"
+ f" (personal identification No. {f.get('pid') or '[PERSONAL ID NO.]'}), residing at")
+ r["an individual, residing at"] = f"an individual, {pass_line}"
+ r["[ADDRESS], [COUNTRY]"] = f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus"
+ r["[ADDRESS]"] = f.get("address") or "[ADDRESS]"
+ r["[COUNTRY]"] = "Republic of Belarus"
+ return r
+
+
+def fill(f, out_docx):
+ doc = docx.Document(TEMPLATE)
+ reps = founder_replacements(f)
+ if f.get("sixteenth"):
+ reps = dict(SIXTEENTH) | reps
+ reps[DRAFT_LINE] = ""
+ for p in doc.paragraphs:
+ if not p.runs:
+ continue
+ full = "".join(r.text for r in p.runs)
+ new = full
+ for k, v in reps.items():
+ if k in new:
+ new = new.replace(k, v)
+ if new != full:
+ p.runs[0].text = new
+ for r in p.runs[1:]:
+ r.text = ""
+ doc.save(out_docx)
+ return out_docx
+
+
+def to_pdf(docx_path):
+ import subprocess, tempfile
+ with tempfile.TemporaryDirectory() as td:
+ r = subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "--outdir", td, docx_path],
+ capture_output=True, timeout=90)
+ pdf = os.path.join(td, os.path.basename(docx_path).replace(".docx", ".pdf"))
+ if os.path.exists(pdf):
+ dst = docx_path.replace(".docx", ".pdf")
+ open(dst, "wb").write(open(pdf, "rb").read())
+ return dst
+ return None
+
+
+def leftover_placeholders(docx_path):
+ import re
+ d = docx.Document(docx_path)
+ found = set()
+ for p in d.paragraphs:
+ found.update(re.findall(r"\[[A-Z][A-Z0-9 /.,%()\[\]-]*\]", p.text))
+ return sorted(found)
+
+
+FOUNDERS = [
+ # ── t-261008143050: Siarhei Zhukovich, когорта с 16-го ──
+ dict(file="SPA_Siarhei_Zhukovich", name="Siarhei Zhukovich",
+ passport="AB3393306", pid=None, # ид. номер — нет в базе, плейсхолдер
+ address="28 July str., 33B-42, Brest", email=None,
+ project="podrabotki.by (rebranding to StaffGo)",
+ one_line="a platform for hiring line staff: employment/contract placement and turnkey contractor paperwork (documents and payments)",
+ rev_share=50, effective="October 16, 2026", cohort="October 2026",
+ program_start="16 October 2026", sixteenth=True),
+ # ── t-261008144131: участники 42 Labs за 2026-09 (lab_debts) ──
+ dict(file="SPA_Alex_Shkor", name="Alex Shkor", passport=None, pid=None,
+ address=None, email="alex@shkor.com", project=None, one_line=None,
+ rev_share=None, effective="September 1, 2026", cohort="September 2026",
+ program_start="1 September 2026", sixteenth=False),
+ dict(file="SPA_Vlad", name="Vlad", passport=None, pid=None,
+ address=None, email="vla2music.com@gmail.com", project=None, one_line=None,
+ rev_share=None, effective="September 1, 2026", cohort="September 2026",
+ program_start="1 September 2026", sixteenth=False),
+ dict(file="SPA_Yura", name="Yura", passport=None, pid=None,
+ address=None, email="yura28440@gmail.com", project=None, one_line=None,
+ rev_share=None, effective="September 1, 2026", cohort="September 2026",
+ program_start="1 September 2026", sixteenth=False),
+ dict(file="SPA_Stas_Dashchinskiy", name="Stas Dashchinskiy", passport=None, pid=None,
+ address=None, email="dashchinskiystas@gmail.com", project=None, one_line=None,
+ rev_share=None, effective="September 1, 2026", cohort="September 2026",
+ program_start="1 September 2026", sixteenth=False),
+ dict(file="SPA_Alexander", name="Александр", passport=None, pid=None,
+ address=None, email="Justgbox@gmail.com", project=None, one_line=None,
+ rev_share=None, effective="September 1, 2026", cohort="September 2026",
+ program_start="1 September 2026", sixteenth=False),
+ dict(file="SPA_Melnikau", name="Andrei Melnikau", passport="MP2848482",
+ pid="3120386A064PB1", address=None, email="ex3.mac@gmail.com",
+ project="tYESter",
+ one_line="a family of software tools for testing computer hardware components without the need to purchase additional equipment",
+ rev_share=None, effective="September 1, 2026", cohort="September 2026",
+ program_start="1 September 2026", sixteenth=False),
+ dict(file="SPA_Dmitry_Siasitski", name="Dmitry Siasitski", passport=None, pid=None,
+ address=None, email="sesitski.dmitry@gmail.com", project=None, one_line=None,
+ rev_share=None, effective="September 1, 2026", cohort="September 2026",
+ program_start="1 September 2026", sixteenth=False),
+]
+
+if __name__ == "__main__":
+ only = sys.argv[1:] or None
+ for f in FOUNDERS:
+ if only and f["file"] not in only:
+ continue
+ docx_path = os.path.join(OUT, f["file"] + ".docx")
+ fill(f, docx_path)
+ pdf = to_pdf(docx_path)
+ print(f"{f['file']}: docx={'OK' if os.path.exists(docx_path) else 'FAIL'} "
+ f"pdf={'OK' if pdf else 'FAIL'} leftover={leftover_placeholders(docx_path)}")
diff --git a/skills/README.md b/skills/README.md
index 49781fa..72d75db 100644
--- a/skills/README.md
+++ b/skills/README.md
@@ -12,6 +12,7 @@
|---|---|---|
| `meta-skill-factory` | Метаскилл: превращение повторяющейся работы в скилл репо (шаги, чеклисты, критерии качества) | Заметил, что делаю одну и ту же работу 2+ раз — прежде чем делать руками в третий |
| `presale-landing` | Производство пресейл/продуктовых лендингов: структура, честные мокапы, Stripe CTA, quality gates | Задача на лендинг/пресейл-страницу или её v2 |
+| `gcal` | Google Calendar: команда из TG «сделай звонок на X» → событие с приглашениями в рабочий день (цикл 3 через 3); инструменты tools/gcal.py | Просьба спланировать звонок/встречу/событие, вопрос по графику |
## Сторонние наборы
diff --git a/skills/compact-landing/SKILL.md b/skills/compact-landing/SKILL.md
new file mode 100644
index 0000000..c8ae631
--- /dev/null
+++ b/skills/compact-landing/SKILL.md
@@ -0,0 +1,73 @@
+---
+description: Build compact premium landing pages with clear CTA hierarchy, quiet typography, and minimal visual noise. Install from Danilaa1 in one command.
+---
+# Compact Landing
+Build a small, finished product world—not a generic marketing template. Preserve compactness and clarity; reinvent the visible design language for each product.
+## Workflow
+1. Inspect the product, supplied content, current stack, and existing design language. Reuse installed tools and components.
+2. Ask for button size first—compact, medium, or large—unless the user already answered or asked you to choose.
+3. Ask at most two adaptive questions only when their answers would materially change the result. Good subjects: brand mood, content depth, corner character, or motion level.
+4. Create a design fingerprint before coding. If this skill includes scripts and Node is available, run `node scripts/pick-direction.mjs`. Otherwise choose from `references/style-dna.md` without taking the first option in every list.
+5. Adapt the fingerprint to product meaning. Treat picker output as a creative constraint, not a command to make incoherent choices.
+6. Identify one product-native signature artifact: a real demo, visualized output, interactive sample, comparison, miniature workflow, data specimen, or useful control. This should create identity more than decoration does.
+7. Implement the smallest complete page. Prefer HTML/CSS and existing dependencies. Add JavaScript only for behavior the page actually needs.
+8. Verify mobile layout, keyboard focus, reduced motion, stable dynamic states, and the real primary action.
+If the user wants immediate execution, choose the button size and fingerprint yourself. Do not stop for a questionnaire.
+## Design Fingerprint
+Define these axes before implementation:
+- **Composition:** field note, offset rail, split pamphlet, framed console, compact catalogue, stacked specimen, or another product-specific silhouette.
+- **Measure:** choose a fitting compact width, usually `420-720px`; do not default to `440px` every time.
+- **Palette:** warm paper, cool fog, monochrome ink, dark charcoal, product-tinted, or playful light.
+- **Typography:** grotesk + mono, humanist sans + mono, editorial serif + sans, rounded sans + mono, condensed + sans, or purposeful all-mono.
+- **Geometry:** sharp, machined, balanced, friendly, soft, or mixed/concentric radii.
+- **Surface grammar:** whitespace, flat dividers, inset hairlines, layered rings, tinted panels, paper rules, or technical frames. Pick one dominant grammar.
+- **Motif:** emoji punctuation, ASCII marks, indices, stamps, doodles, colored dots, tiny object icons, or none.
+- **Motion:** choose one entrance behavior and one or two interaction behaviors.
+Variation must change the first-screen silhouette, not merely colors or labels. Ensure at least four axes differ from the old canonical recipe: centered `440px` column, neutral white/zinc, Geist + Geist Mono, `6-10px` radii, inset hairline cards, Instrument order, staggered rise.
+Do not randomize components independently. Choose one coherent art direction, then derive tokens from it. Use only one or two surprising details; repetition inside the page creates coherence.
+Read `references/style-dna.md` for compatible ranges, direction families, motion patterns, and review checks.
+## Invariants
+- Keep the page compact in scope and information density, even when its canvas is wider or split.
+- Put the primary action in the first viewport. Repeat it only when doing so removes friction.
+- Use clear type hierarchy, concrete copy, and enough contrast.
+- Use real product content. Avoid fake metrics, vague claims, filler testimonials, and decorative dashboards.
+- Extend visually tiny controls to at least a `40px` hit area without making them look oversized.
+- Show a visible `:focus-visible` state and preserve semantic HTML.
+- Use tabular numerals for changing numbers.
+- Respect `prefers-reduced-motion` and keep the reduced experience fully usable.
+- Never add a dependency for a small effect CSS or the current stack can handle.
+## Zero-Shift Motion
+- Animate only `transform`, `opacity`, and `filter`. Never use `transition: all`.
+- Reserve dimensions before animation: fixed/min heights, `aspect-ratio` for media, fixed icon slots, width-locked changing labels, and tabular numerals.
+- Overlay alternate labels, icons, tabs, and panels in the same grid area or fixed box. Do not insert delayed content into document flow.
+- Keep padding, borders, font size, font weight, line height, grid tracks, and flex sizing stable across hover and state changes.
+- Prefer transforms for toggles, drawers, traces, and decorative motion. Absolute-position decorative particles so they never affect flow.
+- Self-host fonts when practical. Otherwise use metric-compatible fallbacks and avoid hiding the page while fonts load.
+- Gate hover-only effects behind `(hover: hover) and (pointer: fine)`.
+## Buttons
+Apply one scale consistently:
+- **Compact:** `24-28px` visual height, `11-12px` label.
+- **Medium:** `32-36px` visual height, `12.5-13px` label.
+- **Large:** `40-44px` visual height, `13.5-14px` label.
+Use `scale(0.96)` for press feedback. For wide command controls, use `0.98`. Keep label and icon slots stable during copied/loading/success states.
+For `llms.txt` or assistant-copy actions, use a small mono button only when the product needs it. Reuse the project's icon system or a CSS glyph; do not embed SVG examples in skill output.
+## Purposeful Personality
+Emoji are allowed when they fit the product. Use `1-3` as punctuation, labels, tiny stickers, or state cues—not as a substitute for structure or iconography. Reserve their boxes so platform glyph differences do not move layout.
+Color may be playful, dark, editorial, or product-tinted. Give every accent a role and repeat it. Avoid default purple-blue AI gradients, uncontrolled rainbows, and random decorative color.
+Motion should explain state, focus attention, or express the product metaphor. A fixed-size trace, ambient floating mark, slot swap, clipped reveal, or state color wash can feel alive without causing reflow. Avoid perpetual motion everywhere.
+## Avoid
+- Reusing one section order, narrow width, palette, radius family, or component set across unrelated products.
+- Generic oversized heroes, three equal feature cards, badge clouds, and feature grids by reflex.
+- Copying the Cuelume waveform, cream palette, confetti trio, or exact layout unless the product genuinely calls for them.
+- Making every surface a card or every control a pill.
+- Decorative animation that competes with the primary action.
+- Heavy animation libraries for opacity, transforms, tabs, counters, or simple reveals.
+## Final Check
+- Fingerprint chosen before code; at least four high-salience axes differ from the canonical recipe.
+- Product-native signature artifact is visible and useful.
+- First viewport has a distinct silhouette and clear primary action.
+- Radius, palette, type, surface, motif, and motion form one coherent direction.
+- Mobile becomes a clean single column without horizontal overflow.
+- Dynamic content, media, icons, labels, and panels have reserved geometry.
+- Keyboard, focus, contrast, touch targets, and reduced motion work.
+- Build and the primary interaction pass using the project's existing verification path.
diff --git a/skills/gcal/SKILL.md b/skills/gcal/SKILL.md
new file mode 100644
index 0000000..efb7163
--- /dev/null
+++ b/skills/gcal/SKILL.md
@@ -0,0 +1,66 @@
+---
+name: gcal
+description: Use when Alex asks in TG to schedule a call/meeting ("сделай звонок на X с Владой", "запланируй созвон", "поставь встречу") — creates a Google Calendar event with guest invites in a workday slot (Alex's «3 через 3» cycle), or checks the schedule. Powered by tools/gcal.py.
+metadata:
+ origin: task t-261008155436-a658e9, messages m-516173943/m-516173944
+ requires: secrets/gcal-service-account.json (service account key; until provided — dry-run/planning only)
+---
+
+# gcal: команда из TG → событие в календаре Alex
+
+Одна команда из TG должна решаться одним прогоном этого скилла:
+интеншн → валидный слот (рабочий день!) → событие с приглашением → ответ в чат.
+
+## Шаг 1 — разобрать команду
+
+Из сообщения извлечь:
+- **что**: тема (по умолчанию «Звонок: <тема/имя гостя>»);
+- **когда**: день (сегодня/завтра/дата) и время; длительность (по умолчанию 30 мин);
+- **кого**: email гостей. Известные контакты: Влада — videoprodshema@gmail.com.
+ Email НЕ известен → спросить у Alex ДО создания (угадывать адрес нельзя);
+- **время**: Alex в Минске (UTC+3). Время из TG трактовать как минское,
+ в инструмент передавать UTC.
+
+## Шаг 2 — валидация слота (строго до создания)
+
+```
+./venv/bin/python tools/gcal.py next-workdays # ближайшие рабочие
+./venv/bin/python tools/gcal.py create ... --dry-run # что попадёт в Google
+```
+
+Guard'ы (встроены в create, не обходить):
+1. **Только рабочий день** — цикл «3 через 3» (docs/alex-schedule.md).
+ Якорь цикла: env GCAL_WORK_ANCHOR (по умолчанию 2026-10-02). Сдвиг цикла
+ Alex сообщил в TG → обновить docs/alex-schedule.md и якорь.
+2. **Не позже 2ч до начала** (GCAL_MIN_LEAD_MIN=120) — анти-паттерн рекордера
+ (t-261008155708: встречи, созданные <2 мин до старта, не заходятся).
+
+Выходной/поздний слот → НЕ создавать, ответить Alex с ближайшими рабочими
+днями и попросить подтвердить слот.
+
+## Шаг 3 — создать событие
+
+```
+./venv/bin/python tools/gcal.py create \
+ --title "Звонок: Влада" --day YYYY-MM-DD --time HH:MM \
+ --duration 30 --guests "videoprodshema@gmail.com" --timezone Europe/Minsk
+```
+
+`sendUpdates=all` — приглашения уходят гостям автоматически.
+Ответ в чат: ссылка на событие (htmlLink), слот, гости.
+
+## Ключ (один раз, если не настроен)
+
+`tools/gcal.py check` печатает готовность и шаги:
+Google Cloud → Calendar API → service account → JSON-ключ → дать
+client_email право «Вносить изменения» на календаре Alex → положить в
+`secrets/gcal-service-account.json` (0600). Ключа нет → работать dry-run'ом
+и попросить Alex прислать ключ (это может сделать только владелец аккаунта).
+
+## Критерии качества
+
+- [ ] Слот — рабочий день цикла; иначе отказ с предложением ближайших рабочих
+- [ ] До начала ≥2ч (или осознанный --force по явной просьбе Alex)
+- [ ] Гости получили приглашения (sendUpdates=all), email не угадан
+- [ ] В ответе Alex — время в его зоне (Минск) и ссылка на событие
+- [ ] `check` зелёный; ключ лежит в secrets/ с правами 0600
diff --git a/tests/test_msg_artifact_race.py b/tests/test_msg_artifact_race.py
new file mode 100644
index 0000000..24dfba8
--- /dev/null
+++ b/tests/test_msg_artifact_race.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+"""t-261009160906: анти-гонка уровней min/hour на входящих TG (hermetic).
+
+Кейс 09.10: Alex попросил концепт (m-516174013). Мин-тик ответил и доставил
+свой файл (16:01). Часовой тик, не видя ответа мин-уровня, сгенерировал СВОЙ
+файл и доставил вторым (16:06) — Alex получил два разных файла на один запрос.
+Dedup по artifact_id=file:<путь> не ловит: два разных файла = два разных ключа.
+
+Фикс: связка «артефакт → входящее» (-m m-NNN у tg-deliver-file, msg= у
+deliver_artifact, reply_artifact в read model). Если на входящее уже отвечено
+иным артефактом — SKIP с событием. force обходит (ручная повторная доставка).
+
+Запуск: ./venv/bin/python tests/test_msg_artifact_race.py
+"""
+import os
+import subprocess
+import sys
+
+# ── изоляция: своя БД в /tmp (prod не трогаем) ──
+_DST = "/tmp/simbiot-test-msg-artifact-race.db"
+for p in (_DST, _DST + "-wal", _DST + "-shm"):
+ if os.path.exists(p):
+ os.unlink(p)
+os.environ["SIMBIOT_DB"] = _DST
+sys.path.insert(0, "/root/simbiot-harness")
+
+from harness import es, commands
+
+fails = []
+
+
+def check(name, cond, extra=""):
+ print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "")
+ if not cond:
+ fails.append(name)
+
+
+es.init_db()
+
+# ── подготовка: входящее m-990001, на которое мин-уровень уже ответил ──
+es.append_event("inbox", "m-990001", "MessageReceived", {
+ "update_id": 990001, "chat_id": 42, "message_id": 1, "user_id": 1,
+ "text": "сделай концепт в html", "kind": "text", "date": 1791560000,
+})
+es.append_event("inbox", "m-990001", "ReplySent", {
+ "to_chat": 42, "text": "готово, отправил файлом", "tg_ok": True, "channel": "tg",
+})
+
+# файлы-двойники гонки: мин-уровень и часовой
+_F1 = "/tmp/simbiot-test-race-min.html"
+_F2 = "/tmp/simbiot-test-race-hour.html"
+for f in (_F1, _F2):
+ with open(f, "w") as fh:
+ fh.write("r")
+
+sends = []
+
+# 1) мин-уровень доставил свой файл с привязкой к сообщению
+ok1 = commands.deliver_artifact(f"file:{_F1}", kind="file", send=lambda: (sends.append(_F1) or True),
+ owner="test:min", caption="мин", msg="m-990001")
+check("min-level file delivered", ok1 and sends == [_F1], sends)
+
+model = es.run_projections()
+m = model["inbox"]["m-990001"]
+check("read model binds reply_artifact", m.get("reply_artifact") == f"file:{_F1}", m.get("reply_artifact"))
+
+# 2) часовой уровень пытается доставить СВОЙ файл на то же входящее → SKIP
+ok2 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
+ owner="test:hour", caption="час", msg="m-990001")
+check("hour-level duplicate SKIPped", ok2 is False and sends == [_F1], sends)
+
+db = es._conn()
+skip = list(db.execute(
+ "SELECT data FROM events WHERE event_type='DeliverySkipped' AND data LIKE '%msg-already%'"))
+check("DeliverySkipped event with reason", len(skip) == 1, len(skip))
+
+# 3) force обходит защиту (ручная повторная доставка — явное решение человека)
+ok3 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
+ owner="test:hour", caption="час", force=True, msg="m-990001")
+check("force bypasses guard", ok3 and sends == [_F1, _F2], sends)
+
+# 4) доставки БЕЗ привязки к сообщению не задеваются (обратная совместимость)
+ok4 = commands.deliver_artifact("file:/tmp/simbiot-test-race-unbound.html", kind="file",
+ send=lambda: (sends.append("unbound") or True),
+ owner="test", caption="x")
+check("unrelated delivery unaffected", ok4 and sends[-1] == "unbound", sends)
+
+# 5) второе входящее без артефакта: доставка с привязкой проходит свободно
+es.append_event("inbox", "m-990002", "MessageReceived", {
+ "update_id": 990002, "chat_id": 42, "message_id": 2, "user_id": 1,
+ "text": "ещё один запрос", "kind": "text", "date": 1791560100,
+})
+es.append_event("inbox", "m-990002", "ReplySent", {
+ "to_chat": 42, "text": "ок", "tg_ok": True, "channel": "tg",
+})
+ok5 = commands.deliver_artifact("file:/tmp/simbiot-test-race-second.html", kind="file",
+ send=lambda: (sends.append("second") or True),
+ owner="test:hour", caption="y", msg="m-990002")
+check("fresh msg accepts its artifact", ok5 and sends[-1] == "second", sends)
+
+# 6) CLI: tg-deliver-file -m m-990001 с чужим артефактом → SKIP, выход 0
+r = subprocess.run(
+ ["./venv/bin/python", "cli.py", "tg-deliver-file", _F2, "-m", "m-990001",
+ "-c", "dup", "-k", "file:" + _F2],
+ capture_output=True, text=True, cwd="/root/simbiot-harness")
+check("CLI SKIP on raced msg", r.returncode == 0 and "SKIP" in r.stdout,
+ r.stdout.strip()[:120] + r.stderr.strip()[-200:])
+
+print("=" * 40)
+print("FAILS:", len(fails), fails if fails else "— все проверки прошли")
+sys.exit(1 if fails else 0)
diff --git a/tools/gcal.py b/tools/gcal.py
new file mode 100755
index 0000000..f59b529
--- /dev/null
+++ b/tools/gcal.py
@@ -0,0 +1,290 @@
+#!/usr/bin/env python3
+"""gcal.py — Google Calendar для харнеса: create-event / list / check (t-261008155436).
+
+Команда из TG «сделай звонок на X с Владой» → минутный тик грузит скилл
+skills/gcal/SKILL.md → зовёт этот инструмент → событие в календаре Alex
+с приглашением гостям, слот только на рабочий день (цикл «3 через 3»).
+
+Автоматизация (service account, JWT → access token, REST v3 — без google-клиентов):
+ 1) Alex: console.cloud.google.com → новый проект → включить Calendar API →
+ «Service accounts» → создать → ключ JSON → прислать файл.
+ 2) В календаре Google: Настройки → «Настройки для моего календаря» →
+ «Предоставить доступ» → добавить client_email ключа с ролью
+ «Вносить изменения» (make changes to events).
+ 3) Положить ключ: /root/simbiot-harness/secrets/gcal-service-account.json
+ (или env GCAL_SERVICE_ACCOUNT_FILE). Файл 0600.
+ 4) ./gcal.py check — готовность. Готово: скилл создаёт события без человека.
+
+Если ключа нет — работают --dry-run, check, next-workdays (планирование слотов).
+"""
+from __future__ import annotations
+
+import argparse
+import base64
+import json
+import os
+import sys
+import time
+from datetime import date, datetime, timedelta, timezone
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+REPO = HERE.parent
+DEFAULT_KEY = REPO / "secrets" / "gcal-service-account.json"
+TOKEN_CACHE = Path("/tmp/.gcal_access_token.json")
+
+# ── График Alex: цикл «3 через 3» ─────────────────────────────────────────────
+# docs/alex-schedule.md: 3 рабочих, 3 выходных. Якорь выведен из m-516173591
+# (2026-10-03: «сегодня и завтра рабочие, далее 3 выходных» → 10-02 первый
+# рабочий цикла). Проверка: 10-08 = (10-08−10-02)=6 → 6%6=0 → рабочий (совпало
+# с «сегодня/завтра рабочие» от 10-08). Сдвиг цикла: env GCAL_WORK_ANCHOR=YYYY-MM-DD
+ANCHOR = os.environ.get("GCAL_WORK_ANCHOR", "2026-10-02")
+PERIOD = 6
+WORK_DAYS = {0, 1, 2} # смещения внутри цикла: 0,1,2 — рабочие; 3,4,5 — выходные
+
+MIN_LEAD_MIN = int(os.environ.get("GCAL_MIN_LEAD_MIN", "120")) # анти-паттерн рекордера: не позже 2ч до начала
+
+
+def _anchor() -> date:
+ return datetime.strptime(ANCHOR, "%Y-%m-%d").date()
+
+
+def is_workday(d: date) -> bool:
+ return ((d - _anchor()).days % PERIOD) in WORK_DAYS
+
+
+def next_workdays(n: int = 6, from_day: date | None = None) -> list[date]:
+ d = from_day or date.today()
+ out: list[date] = []
+ while len(out) < n:
+ if is_workday(d):
+ out.append(d)
+ d += timedelta(days=1)
+ return out
+
+
+# ── Креды и токен (service account → JWT → access token) ─────────────────────
+def _key_path(explicit: str | None) -> Path:
+ p = Path(explicit or os.environ.get("GCAL_SERVICE_ACCOUNT_FILE") or DEFAULT_KEY)
+ return p
+
+
+def load_key(explicit: str | None = None) -> dict | None:
+ p = _key_path(explicit)
+ if not p.exists():
+ return None
+ return json.loads(p.read_text())
+
+
+def _b64(o: dict | bytes) -> str:
+ raw = json.dumps(o).encode() if isinstance(o, dict) else o
+ return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
+
+
+def get_access_token(key: dict, force: bool = False) -> str:
+ """JWT (RS256) → Google OAuth2 → access token (кэш ~50 мин)."""
+ if not force and TOKEN_CACHE.exists():
+ try:
+ c = json.loads(TOKEN_CACHE.read_text())
+ if c.get("exp", 0) > time.time() + 120 and c.get("iss") == key["client_email"]:
+ return c["token"]
+ except Exception:
+ pass
+ import httpx # уже в venv харнеса
+
+ now = int(time.time())
+ claim = {
+ "iss": key["client_email"],
+ "scope": "https://www.googleapis.com/auth/calendar",
+ "aud": "https://oauth2.googleapis.com/token",
+ "iat": now,
+ "exp": now + 3600,
+ }
+ head = {"alg": "RS256", "typ": "JWT"}
+ signing_input = f"{_b64(head)}.{_b64(claim)}".encode()
+
+ from cryptography.hazmat.primitives import hashes, serialization
+
+ pk = serialization.load_pem_private_key(key["private_key"].encode(), password=None)
+ sig = pk.sign(signing_input, hashes.SHA256())
+ jwt = f"{signing_input.decode()}.{_b64(sig)}"
+
+ r = httpx.post(
+ "https://oauth2.googleapis.com/token",
+ data={"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": jwt},
+ timeout=20,
+ )
+ if r.status_code != 200:
+ raise RuntimeError(f"token exchange failed: {r.status_code} {r.text[:200]}")
+ tok = r.json()
+ TOKEN_CACHE.write_text(json.dumps({"token": tok["access_token"], "exp": now + tok.get("expires_in", 3600),
+ "iss": key["client_email"]}))
+ TOKEN_CACHE.chmod(0o600)
+ return tok["access_token"]
+
+
+def _api(method: str, path: str, key: dict, body: dict | None = None, params: dict | None = None) -> dict:
+ import httpx
+
+ tok = get_access_token(key)
+ r = httpx.request(method, f"https://www.googleapis.com/calendar/v3{path}",
+ headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"},
+ json=body, params=params or {}, timeout=25)
+ if r.status_code >= 400:
+ raise RuntimeError(f"calendar api {r.status_code}: {r.text[:300]}")
+ return r.json()
+
+
+# ── Действия ──────────────────────────────────────────────────────────────────
+def cmd_check(args) -> int:
+ print(f"anchor (цикл 3 через 3): {ANCHOR} (env GCAL_WORK_ANCHOR для сдвига)")
+ today = date.today()
+ print(f"сегодня {today}: {'РАБОЧИЙ' if is_workday(today) else 'выходной'}")
+ print("ближайшие рабочие:", ", ".join(d.isoformat() for d in next_workdays(4)))
+ key = load_key(args.key)
+ if not key:
+ p = _key_path(args.key)
+ print(f"\nключ: НЕТ ({p})")
+ print("шаги: Google Cloud → Calendar API → service account → JSON-ключ →")
+ print(" доступ к календарю Alex для client_email (make changes) →")
+ print(f" положить в {p} (0600). Затем: gcal.py check && gcal.py list")
+ return 1
+ print(f"\nключ: OK ({key.get('client_email', '?')})")
+ try:
+ cal = _api("GET", f"/calendars/{args.calendar}", key)
+ print(f"календарь: {cal.get('summary')} ({cal.get('id')}) — доступ есть")
+ return 0
+ except Exception as e:
+ print(f"доступ к календарю: НЕ УДАЛСЯ — {e}")
+ print("→ дайте client_email право «Вносить изменения» в настройках календаря")
+ return 2
+
+
+def _guard(slot_start: datetime) -> tuple[bool, str]:
+ d = slot_start.date()
+ if not is_workday(d):
+ nxt = next_workdays(1, from_day=d + timedelta(days=1))
+ sug = ", ".join(x.isoformat() for x in nxt)
+ return False, f"{d.isoformat()} — выходной по циклу Alex «3 через 3». Ближайшие рабочие: {sug}"
+ lead = (slot_start - datetime.now(timezone.utc)).total_seconds() / 60
+ if lead < MIN_LEAD_MIN:
+ return False, (f"до начала {int(lead)} мин < {MIN_LEAD_MIN} (анти-паттерн рекордера: "
+ f"t-261008155708 — встречи <2 мин до старта не заходятся). Возьмите слот позже.")
+ return True, "ok"
+
+
+def _parse_slot(day: str, hhmm: str, dur: int, tz_name: str) -> datetime:
+ """Локальное время дня в tz_name → момент UTC (API Google требует offset)."""
+ from zoneinfo import ZoneInfo
+
+ d = date.fromisoformat(day)
+ hh, mm = map(int, hhmm.split(":"))
+ return datetime(d.year, d.month, d.day, hh, mm, tzinfo=ZoneInfo(tz_name)).astimezone(timezone.utc)
+
+
+def cmd_create(args) -> int:
+ start = _parse_slot(args.day, args.time, args.duration, args.timezone)
+ ok, msg = _guard(start)
+ if not ok and not args.force:
+ print(f"ОТКАЗ: {msg}")
+ return 3
+ if not ok:
+ print(f"ВНИМАНИЕ (force): {msg}")
+ end = start + timedelta(minutes=args.duration)
+ guests = [g.strip() for g in (args.guests or "").split(",") if g.strip()]
+ event = {
+ "summary": args.title,
+ "description": args.description or "Создано харнесом Simbiot (skills/gcal, t-261008155436-a658e9)",
+ "start": {"dateTime": start.isoformat(), "timeZone": args.timezone},
+ "end": {"dateTime": end.isoformat(), "timeZone": args.timezone},
+ "attendees": [{"email": g} for g in guests],
+ "reminders": {"useDefault": False, "overrides": [
+ {"method": "popup", "minutes": 30}, {"method": "popup", "minutes": 10},
+ ]},
+ }
+ if args.dry_run:
+ print("DRY-RUN (в Google не писали). Событие:")
+ print(json.dumps(event, ensure_ascii=False, indent=1))
+ print(f"guard: {msg}; слот рабочий: {ok}")
+ return 0
+ key = load_key(args.key)
+ if not key:
+ print("нет сервис-ключа Google — см. gcal.py check (шаги настройки)")
+ return 1
+ made = _api("POST", f"/calendars/{args.calendar}/events", key, body=event,
+ params={"sendUpdates": "all"})
+ print(f"СОЗДАНО: {made.get('htmlLink')}")
+ print(f" {made.get('summary')} · {start:%Y-%m-%d %H:%M} UTC ({args.timezone}) · "
+ f"гости: {', '.join(guests) or '—'}")
+ return 0
+
+
+def cmd_list(args) -> int:
+ key = load_key(args.key)
+ if not key:
+ print("нет сервис-ключа Google — см. gcal.py check")
+ return 1
+ tmin = datetime.now(timezone.utc).isoformat()
+ data = _api("GET", f"/calendars/{args.calendar}/events", key,
+ params={"timeMin": tmin, "maxResults": args.limit, "orderBy": "startTime",
+ "singleEvents": "true"})
+ evs = data.get("items", [])
+ if not evs:
+ print("ближайших событий нет")
+ return 0
+ for e in evs:
+ when = (e.get("start", {}).get("dateTime") or e.get("start", {}).get("date", ""))[:16]
+ print(f" {when} {e.get('summary', '(без названия)')}")
+ return 0
+
+
+def cmd_next(args) -> int:
+ for d in next_workdays(args.count):
+ off = ((d - _anchor()).days % PERIOD)
+ print(f" {d.isoformat()} рабочий #{off + 1}/3")
+ return 0
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description="Google Calendar для харнеса (create-event по команде из TG)")
+ sub = ap.add_subparsers(dest="cmd", required=True)
+
+ c = sub.add_parser("check", help="готовность: ключ, доступ, график")
+ c.add_argument("--key", help="путь к JSON сервис-аккаунта")
+ c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
+ c.set_defaults(fn=cmd_check)
+
+ c = sub.add_parser("create", help="создать событие (слот только рабочий день)")
+ c.add_argument("--title", required=True)
+ c.add_argument("--day", required=True, help="YYYY-MM-DD")
+ c.add_argument("--time", required=True, help="HH:MM (UTC)")
+ c.add_argument("--duration", type=int, default=30, help="минуты")
+ c.add_argument("--guests", default="", help="email через запятую (приглашения)")
+ c.add_argument("--description", default="")
+ c.add_argument("--timezone", default=os.environ.get("GCAL_TZ", "UTC"))
+ c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
+ c.add_argument("--key")
+ c.add_argument("--dry-run", action="store_true", help="показать событие, в Google не писать")
+ c.add_argument("--force", action="store_true", help="игнорировать guard (осознанно)")
+ c.set_defaults(fn=cmd_create)
+
+ c = sub.add_parser("list", help="ближайшие события")
+ c.add_argument("--limit", type=int, default=10)
+ c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
+ c.add_argument("--key")
+ c.set_defaults(fn=cmd_list)
+
+ c = sub.add_parser("next-workdays", help="ближайшие рабочие дни (цикл 3 через 3)")
+ c.add_argument("count", nargs="?", type=int, default=6)
+ c.set_defaults(fn=cmd_next)
+
+ a = ap.parse_args()
+ try:
+ return a.fn(a)
+ except Exception as e: # noqa: BLE001 — инструмент отвечает кодом, не трейсбеком
+ print(f"ОШИБКА: {type(e).__name__}: {e}")
+ return 4
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/~/.config/opencode/opencode.json b/~/.config/opencode/opencode.json
index eb893ca..e869ce0 100644
--- a/~/.config/opencode/opencode.json
+++ b/~/.config/opencode/opencode.json
@@ -1,8 +1,14 @@
{
- "permission": {
- "edit": "allow",
- "bash": "allow",
- "webfetch": "allow"
- },
- "share": "disabled"
+ "permission": { "edit": "allow", "bash": "allow", "webfetch": "allow" },
+ "share": "disabled",
+ "provider": {
+ "simbiot": {
+ "npm": "@ai-sdk/openai-compatible",
+ "name": "Simbiot Router (cil.by)",
+ "options": { "baseURL": "https://router.cil.by/v1" },
+ "models": {
+ "gpt-5.2": {}, "claude-sonnet-4.6": {}, "gpt-4.1": {}, "o4-mini": {}
+ }
+ }
+ }
}
diff --git a/~/.config/opencode/opencode.json.bak b/~/.config/opencode/opencode.json.bak
new file mode 100644
index 0000000..eb893ca
--- /dev/null
+++ b/~/.config/opencode/opencode.json.bak
@@ -0,0 +1,8 @@
+{
+ "permission": {
+ "edit": "allow",
+ "bash": "allow",
+ "webfetch": "allow"
+ },
+ "share": "disabled"
+}
diff --git a/~/.profile b/~/.profile
new file mode 100644
index 0000000..119c058
--- /dev/null
+++ b/~/.profile
@@ -0,0 +1,2 @@
+export OPENAI_API_KEY=sk-9ffbac9384bc446eab3bbe58a4a98dc3f4b2347d341caf51a34ce93149fd8c57f1eff2065f89025e
+export OPENAI_BASE_URL=https://router.cil.by/v1