diff --git a/artifacts/shkor-beauty-prototype.html b/artifacts/shkor-beauty-prototype.html index 9b48343..d94d8c2 100644 --- a/artifacts/shkor-beauty-prototype.html +++ b/artifacts/shkor-beauty-prototype.html @@ -40,6 +40,8 @@ .hero h1{font-size:clamp(30px,4.5vw,46px);line-height:1.1;letter-spacing:-.02em;max-width:16ch} .hero p{margin-top:14px;max-width:48ch;color:var(--muted);font-size:17px} .hero .btn{display:inline-block;margin-top:22px;background:var(--acc);color:#fff;padding:13px 26px;border-radius:6px;font-weight:600} + .hero .btn.ghost{background:transparent;color:var(--acc);border:1.5px solid var(--acc);margin-left:10px} + .hero .btn.ghost:hover{background:var(--acc-soft)} .hero .btn:hover{filter:brightness(1.08)} .rhythm{flex:1;min-width:260px;display:flex;align-items:flex-end;gap:4px;height:120px;opacity:.9} .rhythm span{flex:1;background:var(--acc);border-radius:3px 3px 0 0;animation:grow 2.8s ease-in-out infinite} @@ -94,9 +96,25 @@ .dark input:focus{border-color:var(--acc)} .dark button{background:var(--acc);border:none;color:#fff;padding:12px 22px;border-radius:6px;font:inherit;font-weight:600;cursor:pointer} + .plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-top:8px} + .plan-card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:24px} + .plan-card h3{font-size:20px} + .plan-price{font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:6px 0 14px} + .plan-card ul{list-style:none;margin:0 0 18px} + .plan-card li{padding:6px 0 6px 22px;position:relative;font-size:14.5px;color:#3d3a34} + .plan-card li:before{content:"✓";position:absolute;left:0;color:var(--acc);font-weight:700} + .plan-card .btn{display:inline-block;background:var(--acc);color:#fff;padding:10px 18px;border-radius:6px;font-weight:600} + .plan-card.featured{border:2px solid var(--acc);box-shadow:0 6px 24px rgba(14,122,95,.12)} + .plan-card code{font-family:var(--mono);font-size:12px;background:var(--acc-soft);color:var(--acc);padding:1px 5px;border-radius:4px} + #reqform{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;max-width:760px} + #reqform button{margin-top:4px} + footer{padding:30px 0 40px;color:var(--muted);font-size:13.5px} footer .wrap{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap} footer .proto{font-family:var(--mono);font-size:12px} + footer .flinks{display:flex;gap:18px;font-size:13.5px} + footer .flinks a{text-decoration:underline;color:var(--muted)} + footer .flinks a:hover{color:var(--acc)} #toast{position:fixed;left:50%;bottom:24px;transform:translate(-50%,80px);background:var(--ink);color:#fff;padding:12px 20px;border-radius:8px;font-size:14px;opacity:0;transition:.25s ease;z-index:9} #toast.show{transform:translate(-50%,0);opacity:1} @@ -113,7 +131,7 @@ -
Прототип витрины — шаг 1 из плана Shkor beauty. Наполнение демонстрационное, оплата не подключена.
+
Витрина работает: AI-ассистент подбирает уход, заявки принимаются (Lite-план). Оплата онлайн — на шаге 2.
@@ -126,6 +144,7 @@
@@ -143,9 +162,9 @@
-

Корейский уход — с полки до двери за 1 день

-

Каталог проверенных K-beauty брендов в Минске: очищение, тонизирование, сыворотки, SPF. Самовывоз и доставка по Беларуси.

- Смотреть каталог +

Корейский уход — AI подберёт, привезём за 1 день

+

Каталог проверенных K-beauty брендов в Минске + AI-ассистент: разберёт состояние кожи, соберёт рутину и примет заявку. Доставка по Беларуси.

+ Смотреть каталог Оставить заявку
+
+
+

Магазинам корейской косметики

+

Shkor beauty — это AI-ассистент витрины (B2B): подбор ухода, заказы и заявки — единое API для вашего сайта и любых агентов. Оператор сервиса — Shkor Labs LLC.

+
+
+

Lite

+

базовый план

+
    +
  • Витрина с каталогом и брендами
  • +
  • Форма заявки → общая база заказов
  • +
  • Заявки приходят с пометкой plan=lite
  • +
  • Договор с магазином лично: телефон/мессенджер
  • +
+ Начать с заявки +
+ +
+
+
+ +
+
+

Оставить заявку

+

Расскажите, что нужно — подберём уход или подключим магазин. Заявка пишется в общую базу заказов, ответим в течение дня.

+
+ + + + +
+

+
+
+

Подписка на поступления

@@ -334,7 +398,13 @@
-
Shkor beauty — витрина корейской косметики. Прототип шага 1: каталог без платежей.
+
Shkor beauty — витрина корейской косметики + AI-ассистент. Оператор сервиса: Shkor Labs LLC (Wyoming, USA) · alex.shkor@gmail.com
+
собрано харнесом Simbiot · кооператив 42 · cil.by
@@ -347,7 +417,25 @@ function toast(t){ clearTimeout(el._t);el._t=setTimeout(function(){el.classList.remove('show')},2200); } document.querySelectorAll('.p button').forEach(function(b){ - b.addEventListener('click',function(){toast('Прототип: заказ и оплата будут на шаге 2')}); + b.addEventListener('click',function(){ + var card=b.closest('.pb'), h=card?card.querySelector('h3'):null; + var note=document.getElementById('reqform').note; + if(h&¬e) note.value='Интересует: '+h.textContent; + document.getElementById('request').scrollIntoView({behavior:'smooth'}); + toast('Добавлено в заявку — оставьте контакты ниже'); + }); +}); +document.getElementById('reqform').addEventListener('submit',function(e){ + e.preventDefault(); + var f=e.target, st=document.getElementById('reqstatus'); + st.textContent='Отправляем…'; + fetch('/api/call',{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({name:'submit_request',arguments:{name:f.name.value.trim(),phone:f.phone.value.trim(),note:f.note.value.trim()}}) + }).then(function(r){return r.json()}).then(function(d){ + var res=d.result||d; + if(res.order_id){st.textContent='Заявка принята: '+res.order_id+'. Свяжемся в течение дня.';f.reset();} + else{st.textContent='Не получилось: '+(res.error||JSON.stringify(res));} + }).catch(function(err){st.textContent='Сеть недоступна — напишите на alex.shkor@gmail.com';}); }); diff --git a/artifacts/shkor/assistant.py b/artifacts/shkor/assistant.py index 3defe05..c415a12 100644 --- a/artifacts/shkor/assistant.py +++ b/artifacts/shkor/assistant.py @@ -255,6 +255,8 @@ def place_order(args: dict) -> dict: oid = "SB-" + uuid.uuid4().hex[:8].upper() order = { + "order_id": oid, + "plan": args.get("plan") if args.get("plan") in ("full", "lite") else "full", "order_id": oid, "items": lines, "subtotal": round(total, 2), @@ -283,9 +285,47 @@ def check_order(args: dict) -> dict: o = _load_orders().get(oid) if not o: return {"error": f"заказ {oid} не найден", "known": list(_load_orders())[:10]} - return {"order_id": oid, "status": o["status"], "payment": o["payment"]["status"], - "total": o["total"], "delivery": o["delivery"]["label"], "eta": o["delivery"]["eta"], - "items": [{"title": l["title"], "qty": l["qty"]} for l in o["items"]]} + out = {"order_id": oid, "plan": o.get("plan", "full"), "status": o["status"], + "payment": (o.get("payment") or {}).get("status")} + if o.get("kind") == "request": + out.update({"kind": "request", "note": o.get("note", ""), + "products": o.get("products", []), + "customer": o.get("customer", {})}) + return out + out.update({"total": o["total"], "delivery": o["delivery"]["label"], "eta": o["delivery"]["eta"], + "items": [{"title": l["title"], "qty": l["qty"]} for l in o["items"]]}) + return out + + +def submit_request(args: dict) -> dict: + """Заявка из витрины Lite-плана: без корзины/оплаты, пишем в ту же базу orders.json с plan=lite. + + Витрина формы («Хочу это», «Подобрать рутину») → request SB-R-XXXXXXXX → менеджер/агент + договаривается лично. Когда магазин переходит на Full — та же запись дооформляется + в заказ (check_order видит статус request). + """ + contact = (args.get("phone") or args.get("email") or "").strip() + if not contact and not (args.get("name") or "").strip(): + return {"error": "нужен хотя бы один контакт: phone или email (или name)"} + oid = "SB-R-" + uuid.uuid4().hex[:8].upper() + rec = { + "order_id": oid, + "plan": "lite", + "kind": "request", + "note": (args.get("note") or "").strip(), + "products": args.get("products") or [], + "customer": {"name": args.get("name") or "", "phone": args.get("phone") or "", + "email": args.get("email") or "", "address": args.get("address") or ""}, + "payment": {"status": "none", "method": "none", "pay_link": None}, + "total": 0.0, + "status": "request", + "created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + orders = _load_orders() + orders[oid] = rec + _save_orders(orders) + return {"order_id": oid, "status": "request", "plan": "lite", + "next": "Магазин свяжется по контакту. Статус: tools/call check_order {\"order_id\": \"%s\"}" % oid} def delivery_options(args: dict) -> dict: @@ -339,7 +379,8 @@ TOOLS = { ["self_report?", "symptoms?", "photo_report?", "age?", "climate?"]), "recommend": ("Подбор продуктов: concerns+skin_type+budget_max → карточки", ["concerns?", "skin_type?", "budget_max?", "limit?"]), "search": ("Поиск по каталогу", ["query?", "category?", "limit?"]), - "place_order": ("Оформить заказ", ["items[{id,qty}]", "delivery?", "name?", "phone?", "address?", "pay_method?"]), + "place_order": ("Оформить заказ (Full-план)", ["items[{id,qty}]", "delivery?", "name?", "phone?", "address?", "pay_method?", "plan?"]), + "submit_request": ("Заявка из витрины Lite-плана: без корзины/оплаты → orders.json plan=lite, статус request", ["phone?", "email?", "name?", "note?", "products?"]), "check_order": ("Статус заказа и доставки", ["order_id"]), "delivery_options": ("Варианты доставки и цены", ["cart_total?"]), "usage_instructions": ("Инструкции по применению + порядок рутины", ["product_ids"]), @@ -365,7 +406,7 @@ def handle(request: dict) -> dict: if name not in TOOLS: return {"error": f"неизвестный инструмент {name!r}", "available": list(TOOLS)} fn = {"analyze_skin": analyze_skin, "recommend": recommend, "search": search, - "place_order": place_order, "check_order": check_order, + "place_order": place_order, "submit_request": submit_request, "check_order": check_order, "delivery_options": delivery_options, "usage_instructions": usage_instructions}[name] try: result = fn(arguments if isinstance(arguments, dict) else {}) diff --git a/artifacts/shkor/connector.py b/artifacts/shkor/connector.py index 7b7480c..25c839e 100644 --- a/artifacts/shkor/connector.py +++ b/artifacts/shkor/connector.py @@ -58,12 +58,24 @@ def serve_http(addr: str) -> None: self.send_response(code) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) + self.send_header("Access-Control-Allow-Origin", "*") # витрина на другом домене: форма заявки → /call + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() self.wfile.write(body) + def do_OPTIONS(self) -> None: # preflight для формы витрины (fetch с другого origin) + self.send_response(204) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type") + self.send_header("Content-Length", "0") + self.end_headers() + def do_GET(self) -> None: # health / открыть в браузере if self.path in ("/", "/health"): - self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1}) + self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1, + "tools": "POST /tools/list", "call": "POST /call"}) else: self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]}) diff --git a/artifacts/shkor/gen_legal_pages.py b/artifacts/shkor/gen_legal_pages.py new file mode 100644 index 0000000..bfcc7bd --- /dev/null +++ b/artifacts/shkor/gen_legal_pages.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Генератор юр-страниц Shkor beauty (t-261008154151-faa550). + +Privacy Policy / Terms of Service / Cookie & Disclaimer / About & Contacts. +Единый шаблон = тот же визуальный язык, что у витрины. Оператор сервиса: +Shkor Labs LLC (Wyoming, USA), alex.shkor@gmail.com. +""" +from pathlib import Path + +OUT = Path("/var/www/beauty") +OUT.mkdir(parents=True, exist_ok=True) + +CSS = """ + :root{--ink:#1c1a17;--paper:#faf7f2;--line:#e4ddd2;--acc:#0e7a5f;--muted:#6f6a61; + --sans:"Segoe UI",-apple-system,Arial,sans-serif;--mono:ui-monospace,Menlo,Consolas,monospace} + *{box-sizing:border-box;margin:0;padding:0} + body{background:var(--paper);color:var(--ink);font-family:var(--sans);line-height:1.65;font-size:16px} + .wrap{max-width:820px;margin:0 auto;padding:0 20px} + header{border-bottom:1px solid var(--line);background:#fff} + .hbar{display:flex;align-items:center;justify-content:space-between;padding:16px 0} + .logo{font-size:20px;font-weight:700} + .logo span{color:var(--acc)} + .logo small{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)} + nav a{margin-left:16px;font-size:14px;color:var(--muted)} + nav a:hover{color:var(--acc)} + main{padding:40px 0 60px} + h1{font-size:clamp(26px,4vw,36px);letter-spacing:-.02em;margin-bottom:6px} + .meta{font-family:var(--mono);font-size:12px;color:var(--muted);margin-bottom:28px} + h2{font-size:19px;margin:28px 0 10px} + p,li{color:#3d3a34;margin-bottom:10px} + ul{padding-left:22px} + a{color:var(--acc)} + .card{background:#fff;border:1px solid var(--line);border-radius:10px;padding:22px;margin-top:24px} + footer{padding:26px 0 40px;border-top:1px solid var(--line);color:var(--muted);font-size:13.5px} + footer .links{display:flex;gap:18px;margin-bottom:8px} + footer .links a{text-decoration:underline;color:var(--muted)} + footer .proto{font-family:var(--mono);font-size:12px;margin-top:6px} +""" + + +def page(slug: str, title: str, updated: str, body: str) -> None: + html = f""" + + + + +{title} — Shkor beauty + + + + +
+ +
+
+

{title}

+
Last updated: {updated} · v1.0 · Operator: Shkor Labs LLC (Wyoming, USA)
+ {body} +
+ + +""" + (OUT / f"{slug}.html").write_text(html, encoding="utf-8") + print("ok", OUT / f"{slug}.html") + + +D = "2026-10-08" + +page("privacy", "Privacy Policy", D, """ +

Настоящая Политика конфиденциальности объясняет, какие данные собирает сервис Shkor beauty +(далее — «Сервис») и как они используются. Оператор Сервиса — Shkor Labs LLC, компания, +зарегистрированная в штате Вайоминг, США (далее — «Оператор»). Контакт: alex.shkor@gmail.com.

+ +

1. Какие данные мы собираем

+
    +
  • Заявка (Lite-план): имя, телефон и текст заявки, которые вы сами указываете в форме «Оставить заявку».
  • +
  • Заказ (Full-план): имя, телефон, адрес доставки и состав заказа.
  • +
  • Обращения: письма на email Оператора и переписка в мессенджерах — в объёме, необходимом для ответа.
  • +
  • Технические данные: анонимная статистика посещений (страницы-рефереры, устройство, страна) — без персональных идентификаторов.
  • +
+ +

2. Чего мы НЕ собираем

+
    +
  • Данные банковских карт: при онлайн-оплате (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 дней с момента получения.
  • +
  • Средства гигиены и косметика с нарушенной упаковкой возврату не подлежат.
  • +
  • Для возврата напишите на alex.shkor@gmail.com с номером заказа.
  • +
+ +

5. Интеллектуальная собственность

+

Материалы витрины, AI-подбор и программный код ассистента принадлежат Оператору. Товарные знаки +брендов принадлежат их правообладателям.

+ +

6. Ограничение ответственности

+

Сервис предоставляется «как есть». AI-подбор средств — рекомендательный характер и не является +медицинской консультацией (см. Disclaimer). Оператор не несёт +ответственности за индивидуальные реакции кожи и решения, принятые на основе рекомендаций без +ознакомления с составами и инструкциями.

+ +

7. Применимое право

+

Настоящие Условия регулируются правом, применимым к месту регистрации Оператора (Wyoming, USA), +без ущерба для обязательных потребительских прав покупателей по законодательству места доставки.

+""") + +page("disclaimer", "Cookie & Disclaimer", D, """ +

Страница объединяет политику 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. Внешние ссылки

+

Витрина может ссылаться на сайты брендов и службы доставки — за их содержание Оператор +ответственности не несёт.

+ +

5. Вопросы

+

Любые вопросы по дисклеймерам и cookie — alex.shkor@gmail.com.

+""") + +page("about", "About & Contacts", D, """ +

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.

+
+ Start your studio — $999/mo + Live demo + flat rate · cancel anytime +
+ +
+
+ + studio ops · task ladder + 3 agents online +
+
+
4 done
tasks this cycle
+
4-cadence
reflex→day runtime
+
S·M·L
task sizes
+
+
+
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.
+
✓
The agent harness 4-cadence
Reflex, minute, hour, day — escalation, memory, semantic knowledge base. Coop-tested daily.
+
✓
One-click export no lock-in
Take everything with you, anytime. Ready in ~10 minutes after payment.
+
+
+ + +
+
04

One plan each. Everything included.

founding cohort
+
+
+
Personal · first come
+
$99.9 / month
+
    +
  • 1 dedicated AI agent on its own server
  • +
  • Private Matrix chat — web + mobile
  • +
  • Inference, hosting, updates included
  • +
  • One-click export, no lock-in
  • +
+ Get my agent +
secure checkout by Stripe · cancel anytime
+
+
+
Studio
+
$999 / month
+
    +
  • 12 AI agents — engineers, producers, content
  • +
  • White-label portal on your subdomain
  • +
  • Projects, treasury, decisions, meetings — one system
  • +
  • Team members free · priority support
  • +
+ Start your studio +
deployed the same day · self-hosted? talk to us
+
+
+
+ + +
+
+ + + + diff --git a/out/iishnitsa-panel-concept-2026-10-09.html b/out/iishnitsa-panel-concept-2026-10-09.html new file mode 100644 index 0000000..102088f --- /dev/null +++ b/out/iishnitsa-panel-concept-2026-10-09.html @@ -0,0 +1,209 @@ + + + + + +ИИШНИЦА · Панельная сессия «Общее видение» — концепт кооператива 42 + + + +
+ +
+
ИИШНИЦА · Панельная сессия · Концепт
+

Общее видение

+

Формат панельной сессии на ИИШНИЦЕ: миссия 42 задаёт рамку, спикеры из разных индустрий описывают будущее — и панель сходится к названию общего видения.

+
+ 90 минут + 5–6 спикеров + 3 круга + зал + Миссия 42 + Минск · cil.by +
+
+ +
+
01 / ИДЕЯ
+

Суть формата

+

Не классическая панель с вопросами модератора, а три круга с одной целью: собрать из отдельных картин будущего — общую. Миссия 42 озвучивается в начале как рамка, объединяющая любые индустрии и любые ответы.

+
+

Миссия «42»: все миссии одинаково важны. Миссия «42» объединяет абсолютно все возможные миссии. Нет неважных миссий — все люди важны, у каждого своя миссия.

+
— кооператив «Друзья Коллективного Разума», Минск · cil.by
+
+

Почему это работает: каждый спикер говорит о своей индустрии — и потому не конкурирует с соседом за «правильный ответ». А третий круг, название общего видения, превращает панель из набора мнений в один артефакт, который остаётся после сессии: в заголовках, фото и цитатах.

+
+ +
+
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 на сцене — фирменный приём кооператива, отличающий сессию от других панелей конференции.
  • +
+
+ +
+ Концепт панельной сессии «Общее видение» · ИИШНИЦА · кооператив «Друзья Коллективного Разума» · Минск · cil.by
+ Черновик для обсуждения v0.1 · simbiot-harness / hour · 2026-10-09 +
+ +
+ + 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)

+
+

Разговор +Алекса с Леонидом — выжимка и договорённости

+

Встреча 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. +
  5. По голосам/долям за прошлые инвестиции — Леонид +отказался («это была шутка», «инвестирую из философских +соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.
  6. +
  7. Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с +фидбэком.
  8. +
+

Слайд 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 · Алекс × Леонид — выжимка и договорённости + + + +
+
Simbiot · Кооператив 42
+ +
+ + +
+
143
+
Встреча 143 · 08.10.2026 · Jitsi + агент-визуализатор Simbiot
+

Разговор
Алекса × Леонида

+

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

+
+ ~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 файл.

+ +

🌍 Лендинги и презентации

+ + + + + + + + + +
docs/simbiot-landing-v2.htmlЛендинг Simbiot v233 КБ
docs/simbiot-landing-v2-live.htmlЛендинг v2 (live-версия)30 КБ
docs/simbiot-landing-dashboard.htmlЛендинг + дашборд20 КБ
docs/simbiot_presale_landing.htmlПресейл-лендинг17 КБ
artifacts/landing-simbiot.htmlЛендинг (артefакт)11 КБ
artifacts/landing-simbiot-opencode.htmlЛендинг (OpenCode)20 КБ
simbiot-presentation.htmlПрезентация Simbiot22 КБ
architecture.htmlАрхитектура системы40 КБ
+ +

👤 Порталы и UI-клиенты

+ + + + + + +
out/cil-by-compact-landing.htmlПортал CIL by Compact23 КБ
out/ui-cil-by-compact-landing.htmlUI-версия портала CIL22 КБ
out/simbiot_ui_presale.htmlПресейл UI12 КБ
harness/roy.htmlПортал Roy3 КБ
harness/dashboard.htmlДашборд харнеса40 КБ
+ +

🗓 Встречи и материалы к переговорам

+ + + + + + +
out/meeting143_leonid_presentation.htmlВстреча 143 — презентация для Леонида13 КБ
out/meeting143_leonid_slides.htmlВстреча 143 — слайды25 КБ
docs/yulia-call-136-proposal.htmlЗвонок 136 — предложение Юлии6 КБ
docs/yulia-call-136-recap.htmlЗвонок 136 — рекап6 КБ
docs/nadezhda-call-recap.htmlЗвонок с Надеждой — рекап10 КБ
+ +

📄 Документы и договоры

+ + + + + + +
Studio_Participation_Agreement_2026-09.pdfСоглашение об участии в студии (PDF)17 КБ
lab42-conditions.htmlУсловия Lab4215 КБ
artifacts/studio-conditions-2026-10.htmlУсловия студии 2026-106 КБ
artifacts/rental-x-simbiot-agreement.htmlСоглашение Rental×Simbiot16 КБ
artifacts/rental-x-simbiot-side-letter-15k.htmlСайд-леттер Rental×Simbiot 15k10 КБ
+ +

🛠 Продуктовые планы и гайды

+ + + + +
docs/ai-mascots-product-plan.htmlПродуктовый план AI-маскотов16 КБ
docs/fpga-neuralnet-guide.htmlГайд: нейросети на FPGA9 КБ
docs/self-diagnostics.htmlСамодиагностика8 КБ
+ +

🎨 Прототипы

+ + +
artifacts/shkor-beauty-prototype.htmlПрототип Shkor Beauty25 КБ
+ +

🖼 Изображения

+ + + + + +
artifacts/imagegen-e2e-20261003.pngE2E-тест генерации изображений977 КБ
artifacts/dashboard-channels-t-d8112b.pngСкриншот дашборда (каналы)138 КБ
artifacts/dashboard-channels-t-d8112b-full.pngСкриншот дашборда (полный)145 КБ
out/9router-gemini-test.pngТест 9router/Gemini1 КБ
+ + + 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