diff --git a/artifacts/dashboard-channels-t-d8112b-full.png b/artifacts/dashboard-channels-t-d8112b-full.png new file mode 100644 index 0000000..81d960a Binary files /dev/null and b/artifacts/dashboard-channels-t-d8112b-full.png differ diff --git a/artifacts/dashboard-channels-t-d8112b.png b/artifacts/dashboard-channels-t-d8112b.png new file mode 100644 index 0000000..953ae00 Binary files /dev/null and b/artifacts/dashboard-channels-t-d8112b.png differ diff --git a/artifacts/imagegen-e2e-20261003.png b/artifacts/imagegen-e2e-20261003.png new file mode 100644 index 0000000..f5949de Binary files /dev/null and b/artifacts/imagegen-e2e-20261003.png differ diff --git a/artifacts/landing-simbiot-opencode.html b/artifacts/landing-simbiot-opencode.html new file mode 100644 index 0000000..ab13c29 --- /dev/null +++ b/artifacts/landing-simbiot-opencode.html @@ -0,0 +1,598 @@ + + + + + + Simbiot — Ритмичный ИИ-харнес кооператива 42 + + + + +
+
+

Харнес превращает решения в исполненные задачи

+

Для кооперативов, которые хотят ≥ +1% к AUM в горизонте год+

+ Начать использовать Simbiot +
+
+ + +
+
+

Что это

+
+
+

Проблема

+

Решения из звонков и чатов теряются, не доходят до исполнителей, важное тонет в рутине

+
+
+

Механизм

+

Харнес автоматически извлекает решения из диалогов, создает задачи с четкими сроками и ответственными

+
+
+

Результат

+

Важное всегда на виду, эскалации важного поднимаются наверх, AUM растет системно

+
+
+
+
+ + +
+
+

Что включено

+ +
+
+ + +
+
+

Как это работает

+
+
+
1
+

Обсуждение

+

Вы обсуждаете решения в Telegram или звонках

+
+
+
2
+

Авто-создание задач

+

Simbiot превращает решения в задачи с ответственными

+
+
+
3
+

Контроль исполнения

+

Агенты выполняют по тактам, важное эскалируется

+
+
+
+
+ + +
+
+

Визуалы продукта

+ +
+

Дашборд задач

+
+
Панель управления задачами
+
+
+ Анализ Q3 отчетов + В работе +
+
+ Подготовить презентацию для инвесторов + Ожидание +
+
+ Обновить CRM систему + Готово +
+
+ Проверить compliance требования + Ожидание +
+
+
+
+ +
+

Telegram-диалог

+
+
+ Simbiot: Обнаружено решение: "Запланировать встречу с командой по стратегии Q4" +
+
+ Вы: Подтверждаю, создай задачу +
+
+ Simbiot: Задача создана: Встреча по стратегии Q4, ответственный: Вы, срок: 25.10.2024 +
+
+
+
+
+ + +
+
+

Соцдоказательство

+ +
+
+ + +
+
+

Тарифные планы

+
+
+

Старт

+
₽15к/мес
+
    +
  • До 5 пользователей
  • +
  • 1 Telegram-чат
  • +
  • Базовый дашборд
  • +
  • Стандартная поддержка
  • +
+ Начать +
+ +
+

Про

+
₽35к/мес
+
    +
  • До 20 пользователей
  • +
  • 5 Telegram-чатов
  • +
  • Продвинутый дашборд
  • +
  • Приоритетная поддержка
  • +
  • API интеграции
  • +
+ Выбрать +
+ +
+

Enterprise

+
По запросу
+
    +
  • Неограниченно пользователей
  • +
  • Неограниченно чатов
  • +
  • Полный функционал
  • +
  • Персональный менеджер
  • +
  • Кастомные интеграции
  • +
+ Контакты +
+
+
+
+ + +
+
+

Харнес превращает решения в исполненные задачи

+

Для кооперативов, которые хотят ≥ +1% к AUM в горизонте год+

+ Начать использовать Simbiot +
+
+ + + + + \ No newline at end of file diff --git a/artifacts/landing-simbiot.html b/artifacts/landing-simbiot.html new file mode 100644 index 0000000..fe6ec4a --- /dev/null +++ b/artifacts/landing-simbiot.html @@ -0,0 +1,162 @@ + + + + + +Simbiot — общий харнес кооператива 42 + + + + + +
+
simbiot·42
+ +
+ +
+

Компания, которая думает тактами, а не совещаниями

+

Simbiot — ритмичный ИИ-харнес кооператива 42. Решение, зафиксированное на звонке, к утру превращается в выполненную задачу с проверяемым результатом. Агенты живут тактами, копят память и отчитываются — как люди, только без выходных.

+ +
каждая полоска — один такт агента · минутный пульс → часовой → дневной
+
+ Зайти в портал + Как это работает ↓ +
+
+ +
+

Не чат-бот. Операционная система студии

+
+
+ 01 / симбиоз +

Общий харнес, а не личный

+

Разработчики строят личные харнесы и уносят их с собой. Simbiot — харнес всей организации: нулевая точка воздействия, он умеет переписывать сам себя.

+
+
+ 02 / координация +

Интегрирует открытое

+

Координатор харнесов: встраивает открытые системы (Hermes, OpenClaw) и свои скиллы в один контур исполнения.

+
+
+ 03 / память +

CQRS + Event Sourcing

+

Каждое решение — событие. История не теряется, любой такт можно переиграть. База знаний живая: протоколы встреч, договоры, проекты.

+
+
+ 04 / эскалация +

Уровни, а не один мозг

+

Минутный пульс отвечает в чате, часовой и дневной — берут сложное. Что не по зубам уровню, уходит наверх с контекстом.

+
+
+
+ +
+

От голоса — к результату

+
+ звонок → протокол решений→ + задача на доске с критерием готовности→ + агент исполняет такт за тактом→ + результат в чат, метрика AUM растёт +
+
Фильтр любого решения: добавляет ли оно +1% к AUM в горизонте год и больше?
+
+ +
+

Цифры, без глянца

+
+
7
продуктов в студии, живые деньги — уже у двух
+
42
кооператив «Друзья Коллективного Разума» — юрлицо живое с 2026
+
24/7
пульс: минутный, часовой и дневной уровни агентов
+
100%
уходя, участник забирает свой проект — база не разваливается
+
+
+ +
+

Встроиться в симбиоз

+

Мы берём проекты в программу 42 Labs: юрлицо и платёжный контур, студийная ОС с первого дня, плата из выручки — когда проект заработал. Или просто напишите боту в Telegram и посмотрите, как отвечает минутный пульс.

+
+ Подать заявку на портале + Написать в Telegram +
+
+ + + + + diff --git a/artifacts/mirofish-analysis-2026-10-04.md b/artifacts/mirofish-analysis-2026-10-04.md new file mode 100644 index 0000000..c582622 --- /dev/null +++ b/artifacts/mirofish-analysis-2026-10-04.md @@ -0,0 +1,91 @@ +# MiroFish (github 666ghj/MiroFish): разбор и применимость — 2026-10-04 + +Задача: t-261004062405-fa9926. Метод: клон репо (depth 1), разбор кода backend +(58 py-файлов, ~17.2k строк) + README. Версия зависимостей: camel-oasis==0.2.5, +zep-cloud==3.25.0, flask>=3.0, openai>=1.0. + +## Вердикт + +**Как движок — НЕ берём. Как источник паттернов — да, 3-5 идей.** +Причины против движка: (1) стоимость — README сам пишет «High consumption, +попробуйте сначала <40 раундов»; (2) жёсткая привязка к Zep Cloud (внешний +SaaS-граф памяти, счёт отдельно от LLM) и OASIS/camel-ai; (3) китайский +дефолт-стек (qwen-plus через dashscope) — у нас 9router; (4) это продукт-платформа +(Flask 5001 + React 3000 + Docker), а не библиотека — интеграция = second brain. + +## Архитектура (проверено по коду) + +Пайплайн: **Graph Building → Environment Setup → Simulation → Report → Interaction** + +1. **Graph Building** (`graph_builder.py` 879 строк, `ontology_generator.py` 732): + seed-материалы (новости/политики/финанс.сигналы, PDF/текст) → извлечение + сущностей и связей → GraphRAG-граф в Zep (онтология генерится LLM). +2. **Environment Setup** (`oasis_profile_generator.py` 1248): из сущностей Zep + LLM-ом генерятся персоны агентов — индивидуальные и групповые + (`generate_profile_from_entity` → `_generate_profile_with_llm`, JSON-схема, + username, bio, persona-текст) → конфиг агентов инжектится в OASIS. +3. **Simulation** (`simulation_runner.py` 2033, `run_parallel_simulation.py` 1699, + twitter/reddit скрипты): движок **OASIS (camel-ai)** — двухплатформенная + параллельная симуляция соцсети (Twitter+Reddit). Раунды = часы×60/минут_на_раунд, + есть max_rounds-обрезка (контроль стоимости). Каждый агент за раунд — + действие через `LLMAction` (≥1 LLM-вызов на активного агента за раунд). + Память: каждое действие → episode-текст → динамическое обновление Zep-графа + (`zep_graph_memory_updater.py` 791: описатели create_post/like/repost/ + comment/follow/search). +4. **Report** (`report_agent.py` 2619 — самый большой модуль): ReportAgent с 4 + инструментами — `insight_forge`, `panorama_search`, `quick_search`, + `interview_agents` (интервьюирует агентов мира post-factum); протокол + `{...}`, трекер «не все инструменты использованы» + дожимает агент до полного отчёта. +5. **Interaction**: чат с любым агентом мира + ReportAgent (фронт). + +Память = **Zep Cloud** (knowledge-graph + temporal): не векторная база, а граф +сущностей с би-темпоралью; free-тариф «на простое использование». + +## Оценка стоимости прогона + +Формула расхода: ~ `N_агентов × N_раундов` LLM-вызовов (действия) + граф/онтология ++ персоны + отчёт (десятки вызовов) + инференс Zep (граф-обновления, отдельный счёт). + +- Заявленный дефолт: qwen-plus. README: «<40 раундов на старте». +- Пример: 100 агентов × 40 раундов ≈ 4–5k вызовов ≈ 15–25M входящих токенов + (контекст платформы в каждом) → **~$10–25 на qwen-plus за прогон**; + на премиум-модели класса GPT-4o — **×10–30 (сотни $)**. +- Осмысленный прогнозный прогон (500+ агентов, 100+ раундов) — **сотни $ за + прогон** даже на дешёвых моделях + Zep Cloud сверх. +- Контекст: наш burn 9router = 2291.81 у.е./7д — один-два «взрослых» прогона + MiroFish съели бы заметный кусок недельного бюджета без прямой выручки. + +## Что берём (идеи заимствования) + +1. **Персона-из-сущности** (`_generate_profile_with_llm`): LLM-генерация + структурированной персоны агента из записи графа знаний (JSON-схема, инди/групп). + Куда: v2 aum-pilot — вместо placeholder-модели продуктивности, агентные симуляции + решений кооператива (роли: Alex/Vlad/Yura/клиент) с сгенерированными персонами. +2. **ReportAgent-паттерн**: отчётёр = агент с узким набором инструментов + (сводка/поиск/дип-поиск/интервью) + трекер обязательного использования всех + инструментов. Куда: наш «отчёт-адвокат» в aum-pilot и часовые дайджесты — + интервью опрошенных агентов симуляции вместо голой статистики. +3. **Episode→граф памяти**: действие → компактный episode-текст → обновление + графа по ходу симуляции (не в конце). Куда: память ритмичных агентов харнеса + — но на нашем RAG (без Zep: episode → embedding → граф связей) — паттерн + повторяем, сервис не покупаем. +4. **Предпродажная симуляция спроса**: OASIS даёт готовую соц-динамику + (лайк/репост/коммент/фоллов). Идея под AUM: перед запуском лендинга/прайса — + дешёвый прогон (20–40 раундов, flash-модель, 50–100 агентов-«клиентов» из + персон сегмента) → прогноз реакции и возражений. Это прямой кандидат в + наши симуляции поведения пользователей. +5. **Автогенерация конфига из NL-запроса** (`simulation_config_generator.py`): + «что хочешь узнать» естественным языком → параметры прогона + max_rounds + (жёсткий потолок стоимости). Куда: постановка задачи → конфиг прогона + aum-pilot; потолок раундов — в наши длинные симуляции. +6. **IPC/прогресс долгих прогонов** (`simulation_ipc.py` + progress %): + отдельный процесс, структурированный прогресс, чистая обрезка. У нас уже + есть _llm_worker — позаимствовать схему прогресса/тримминга. + +## Чего не берём + +- Zep Cloud (внешний SaaS, счёт отдельно, данные наружу) — свой RAG/graph. +- OASIS целиком (тяжёлый camel-ai стек) — только если сделаем п.4; тогда + изолированный Docker-стенд, flash-модель через 9router, budget-cap на прогон. +- Их фронт/платформу — у нас свои каналы (портал, MCP, TG). diff --git a/artifacts/rental-x-simbiot-agreement.html b/artifacts/rental-x-simbiot-agreement.html new file mode 100644 index 0000000..9d7611e --- /dev/null +++ b/artifacts/rental-x-simbiot-agreement.html @@ -0,0 +1,176 @@ + + + + + +Прокат авто × Simbiot — партнёрское соглашение v1 + + + +
+ +
+
Потребительский кооператив «Друзья Коллективного Разума» · Минск
+

Соглашение о партнёрском вознаграждении № 1

+
Прокат авто (Минск) × Simbiot. Договор-черновик на согласование: платформа как операционный и маркетинговый слой партнёра, вознаграждение — 10% от заработка на атрибутированных клиентах.
+
+ Черновик v1 · 04.10.2026 + 10% от нетто-выручки + Ставка фиксирована 12 мес + Атрибуция — 24 мес +
+
+ +
+
00 / СТОРОНЫ
+

Стороны соглашения

+
+
ОПЕРАТОР ПЛАТФОРМЫ

Кооператив

[полное наименование кооператива 42], г. Минск — оператор платформы Simbiot (агентские, маркетинговые и операционные AI-сервисы).

+
ПАРТНЁР

Прокатная компания

[наименование юрлица/ИП], г. Минск — владелец автопарка, передаёт Platform привлечение клиентов и операционное управление частью бизнеса.

+
+
Разделение документов · Этот документ — только про партнёрское вознаграждение и рост выручки. Исторический долг Партнёра (~15 000 USD) вынесен в отдельный Side Letter № 1 — подписывается параллельно, на условия этого соглашения не влияет.
+
+ +
+
01 / ПРЕДМЕТ
+

Предмет соглашения

+
1.1

Оператор предоставляет Партнёру доступ к Платформе Simbiot (агентские, маркетинговые и операционные AI-сервисы) для привлечения клиентов и управления автопарком Партнёра.

+
1.2

За это Партнёр уплачивает Оператору партнёрское вознаграждение в размере 10% от заработка по каждому Атрибутированному клиенту (раздел 02).

+
+ +
+
02 / АТРИБУЦИЯ
+

Кто считается атрибутированным клиентом

+

Три категории. Фиксация — записью в Реестре атрибуций (Приложение № 1): ведёт Оператор, Партнёру — доступ на чтение.

+ + + + + +
КатегорияОпределение
(а)Привлечённая машина — собственник, передавший автомобиль в управление/арендную схему через Платформу.
(б)Клиент на аренду — арендатор, пришедший по лиду/воронке/агентству Платформы (метка в CRM/UTM/реестр).
(в)Агентство — клиент кооператива, обслуженный через Платформу (корпоративный, трансферный, подписочный и т.п.).
+
2.1

Спорная атрибуция: приоритет у более ранней фиксации лида; неразрешённые споры — на ежемесячной сверке.

+
2.2

Клиент считается атрибутированным на 24 месяца с первой фиксации (включая повторные аренды и продления).

+
+ +
+
03 / ДЕНЬГИ
+

Вознаграждение и порядок расчётов

+
3.1

База расчёта: нетто-выручка по атрибутированному клиенту (полученные платежи за вычетом возвратов, депозитов, комиссий платёжных систем и налогов).

+
3.2

Вознаграждение: 10% от базы. Ставка фиксируется на 12 месяцев.

+
3.3

Расчётный период — календарный месяц. Партнёр до 10-го числа следующего месяца направляет подписанный акт + выгрузку; оплата в течение 5 банковских дней, BYN по курсу НБ РБ на дату оплаты (либо USDT — по выбору сторон).

+
3.4

Просрочка >15 дней: пеня 0,1%/день; >45 дней — право Оператора приостановить доступ к Платформе.

+
+ +
+
04 / РОЛЬ ПЛАТФОРМЫ
+

Что делает Simbiot

+

Ровно тот функционал, на котором строится «заработок на атрибутированном клиенте»:

+ +
+ +
+
05 / РОСТ ВЫРУЧКИ
+

Рычаги роста парка

+

Ориентир: полное развёртывание рычагов 1–4 даёт расчётно +60–100% выручки за 6–12 мес., из них ~+20% — в первые 8 недель (монетизация простоя). CAPEX — только ребаланс парка. Оценки сверить с фактическими данными парка (машин, ставки, vacancy) — выгрузка от Партнёра до запуска.

+ + + + + + + + +
#РычагЭффект
1Подписка на простой — простаивающие машины сдаются по подписке (такси, доставки, каршеринг-операторы)быстрые деньги: монетизация vacancy, ~+20% за 8 недель
2Аренда с выкупом — пилот 2–3 эконом-машины, скоринг водителейдлинные деньги: выкупные платежи + продажа машины в конце
3Привлечённые машины — приём авто собственников в управлениерост парка без CAPEX
4Ребаланс парка — перераспределение под спрос (класс, сезон)повышение ставки/дня и снижение vacancy
5Воронка и агент-диспетчерконверсия лидов, снижение стоимости обслуживания
6PR «полностью AI-based агентство»отложенный: лиды + заявки на франшизу/white label
+

Последовательность внедрения

+ +
+ +
+
06 / ПОРЯДОК ПОДПИСАНИЯ
+

Как подписываем

+
6.1

Стороны согласовывают реквизиты и Приложение № 1 (Реестр атрибуций, стартовый состав).

+
6.2

Подписывается настоящее Соглашение и параллельно — Side Letter № 1 (урегулирование исторической задолженности, отдельный документ).

+
6.3

Юридическая проверка обеих документов до подписания обязательна.

+
+ +
+
07 / РЕКВИЗИТЫ
+

Подписи

+
+
+
Оператор Платформы · Кооператив
+
+
Председатель правления · подпись
+
Alex Shkor · «___» _______ 2026 г.
+
+
+
Партнёр · [наименование прокатной компании]
+
+
[должность, ФИО] · подпись
+
М.П. · «___» _______ 2026 г.
+
+
+
+ + + +
+ + diff --git a/artifacts/rental-x-simbiot-deal-2026-10-04.md b/artifacts/rental-x-simbiot-deal-2026-10-04.md new file mode 100644 index 0000000..104677e --- /dev/null +++ b/artifacts/rental-x-simbiot-deal-2026-10-04.md @@ -0,0 +1,79 @@ +# Прокат авто (Минск) × Simbiot — договор-черновик и план роста выручки + +**Дата:** 04.10.2026 · **Основа:** встреча meeting-6feceb4c (04.10.2026), фиксация договорённостей +**Owner:** Alex · **Статус:** черновик на согласование (юридическая проверка перед подписанием обязательна) + +--- + +## Часть I. Договор-черновик + +> СОГЛАШЕНИЕ О ПАРТНЁРСКОМ ВОЗНАГРАЖДЕНИИ № 1 +> г. Минск, «04» октября 2026 г. + +**Стороны:** +- **Кооператив** — [полное наименование кооператива 42], далее «Оператор Платформы» (Simbiot); +- **Партнёр** — [наименование юрлица/ИП прокатной компании], г. Минск, далее «Партнёр». + +### 1. Предмет соглашения +1.1. Оператор предоставляет Партнёру доступ к Платформе Simbiot (агентские, маркетинговые и операционные AI-сервисы) для привлечения клиентов и управления автопарком Партнёра. +1.2. За это Партнёр уплачивает Оператору **партнёрское вознаграждение в размере 10%** от заработка по каждому **Атрибутированному клиенту** (п. 2). + +### 2. Атрибуция. Атрибутированным считается клиент: +- **(а) Привлечённая машина** — собственник, передавший автомобиль в управление/арендную схему через Платформу; +- **(б) Клиент на аренду** — арендатор, пришедший по лиду/воронке/агентству Платформы (метка в CRM/UTM/реестр); +- **(в) Агентство** — клиент кооператива, обслуженный через Платформу (корпоративный, трансферный, подписочный и т.п.). +2.1. Факт атрибуции фиксируется записью в **Реестре атрибуций** (Приложение №1, ведёт Оператор, доступ Партнёру — чтение). Спорная атрибуция: приоритет у более ранней фиксации лида; неразрешённые споры — на ежемесячной сверке. +2.2. Клиент считается атрибутированным на **24 месяца** с первой фиксации (включая повторные аренды и продления). + +### 3. Вознаграждение и порядок расчётов +3.1. База расчёта: **нетто-выручка** по атрибутированному клиенту (полученные платежи за вычетом возвратов, депозитов, комиссий платёжных систем и налогов). +3.2. Вознаграждение: **10% от базы**. Ставка фиксируется на 12 месяцев. +3.3. Расчётный период — календарный месяц. Партнёр до 10-го числа следующего месяца направляет подписанный акт + выгрузку; оплата в течение 5 банковских дней, BYN по курсу НБ РБ на дату оплаты (либо USDT — по выбору сторон). +3.4. Просрочка >15 дней: пеня 0,1%/день; >45 дней — право Оператора приостановить доступ к Платформе. + +### 4. Конвертация долга в инвестицию +4.1. Стороны признают задолженность Партнёра перед Оператором за работы за период ~3 месяца, с инфляционной индексацией, **эквивалент ~15 000 USD, выраженный в BTC по курсу на дату образования долга** (курс-привязка: сумма фиксируется в BTC по курсу BTC/USD на [дату образования долга], рублёвая/долларовая переоценка — по курсу на дату расчётов). +4.2. По подписании настоящего Соглашения долг **полностью конвертируется в инвестиционный вклад Партнёра в Проект «Интеграция Simbiot в прокатный бизнес»** (юнит: развитие Партнёра как витринного кейса платформы) на условиях Приложения №2 (Инвестиционное соглашение: доля/profit-share, reporting, выход). +4.3. С момента подписания Приложения №2 требование по долгу считается урегулированным (новация). + +### 5. ИП и конфиденциальность +5.1. Все права на Платформу Simbiot, модели, данные обучения и CRM принадлежат Оператору. Данные о клиентах Партнёра — совместная коммерческая тайна; Оператор вправе использовать **обезличенную** статистику в маркетинге. +5.2. Кейс Партнёра («AI-агентство в прокате») Оператор вправе публиковать по согласованию формулировок. + +### 6. Срок, расторжение +6.1. Срок: 12 месяцев с даты подписания, автопролонгация на 12 мес., если ни одна сторона не уведомила за 30 дней. +6.2. Расторжение не отменяет вознаграждение за атрибутированных клиентов в текущем периоде и 24-месячный хвост п. 2.2. + +### 7. Прочее +Форс-мажор, применимое право РБ, арбитраж по месту Оператора, обмен сканами = оригинал, ЭЦП/eDocument допустимы. Реквизиты — Приложение №3. + +**Открытые вопросы к Alex (до подписания):** +1. База 10% — валовая или нетто-выручка (п. 3.1)? +2. Хвост атрибуции 24 мес. — устраивает? +3. Параметры Приложения №2: доля Партнёра в проекте / profit-share % и срок. +4. Точная дата образования долга для фиксации BTC-курса. + +--- + +## Часть II. Анализ рычагов роста выручки (приоритизировано) + +**Исходная проблема парка:** перекос в премиум (CLA45, X7, Audi, BMW2) при нишевом спросе → премиум-активы простаивают, простой = 0 выручки при ненулевых издержках (страхование, стоянка, амортизация). + +| # | Рычаг | Усилие | Эффект (оценка) | Почему сработает | +|---|-------|--------|------------------|------------------| +| **1** | **Подписка на простой, ~2990/мес** | S | **+15–25% к выручке парка за 1–2 мес.**, почти без CAPEX | «Любая несданная тачка окупается за 3 дня подписки»: 2990/30 ≈ 100/день. Каждые 3 простых дня сегодня = потерянный месячный платёж. Простой превращается из 0 в выручку. ЦА: экспаты/долгосрочники/малый бизнес | +| **2** | **Аренда с выкупом под таксистов (~30%/год)** | M | **×1.6 выручки с машины за 2 года**, vacancy → ~0 | Эконом-авто ~25k: клиент-таксист платит ~30% годовых сверх стоимости 24 мес. → возврат ~40k с одной машины, предсказуемый денежный поток, износ компенсирован ставкой | +| **3** | **Привлечённые машины (кейс Vestgroup: 0 своих)** | M | **рост парка ×2–3 без CAPEX**, комиссия ~25–35% с каждой | Asset-light: владельцы отдают машины в управление, платформа монетизирует их же воронкой. Снимает потолок роста, ограниченный собственным капиталом | +| **4** | **Ребаланс парка: прем → эконом** | M (капитал) | **+10–15% выручки + высвобождение замороженного капитала** | Непроседающий премиум продаётся (вернуть капитал), вместо него 2–3 эконом — стабильный спрос, ниже износ и риск | +| **5** | **Дешёвый трафик / новые сегменты** | S–M | **+20–30% лидов при том же бюджете** | Такопарки и драйверы (связка с рычагом 2), резиденты на выходные (не только туристы), корпоративные; AI-боты Simbiot = обработка лида ~0$ 24/7 → CAC падает | +| **6** | **PR «полностью AI-based агентство»** | S | отложенный: лиды + входящие заявки на франшизу/белые лейблы | Медиа-крючок первого в РБ AI-агентства в прокате; контент генерирует сам Simbiot — почти бесплатный охват; усиливает рычаги 1–3 | + +### Последовательность внедрения +1. **Недели 1–2:** подписка на простой (рычаг 1) + запуск PR-волны (6) — быстрые деньги и охват. +2. **Месяц 1–2:** пилот аренды с выкупом на 2–3 эконом-машинах (2) + приём первых привлечённых машин (3). +3. **Месяц 2–3:** ребаланс парка (4), масштабирование подписки и привлечённого парка. + +### Сводная оценка +Полное развёртывание рычагов 1–4 даёт расчётно **+60–100% выручки парка за 6–12 мес.** при умеренном CAPEX (только ребаланс), из них ~+20% — в первые 8 недель за счёт монетизации простоя. Оценки требуют сверки с фактическими данными парка (кол-во машин, ставки, vacancy) — запросить у Партнёра выгрузку до запуска. + +**Роль Simbiot:** воронки и лид-скоринг (5), агент-диспетчер 24/7 по аренде/подписке (1), скоринг таксистов для аренды-с-выкупом (2), онбординг владельцев машин (3), контент-PR (6) — всё это и есть база «заработка на атрибутированном клиенте» из Части I. diff --git a/artifacts/rental-x-simbiot-side-letter-15k.html b/artifacts/rental-x-simbiot-side-letter-15k.html new file mode 100644 index 0000000..f66c2f7 --- /dev/null +++ b/artifacts/rental-x-simbiot-side-letter-15k.html @@ -0,0 +1,115 @@ + + + + + +Side Letter № 1 — урегулирование задолженности (~15 000 USD) + + + +
+ +
+
Потребительский кооператив «Друзья Коллективного Разума» · Минск
+

Side Letter № 1
к Соглашению о партнёрском вознаграждении

+
Урегулирование исторической задолженности Партнёра перед Оператором: признание, фиксация, конвертация в инвестиционный вклад. Подписывается параллельно с Соглашением № 1, отдельно — чтобы не смешивать долги с партнёрскими расчётами.
+
+ +
+
СТОРОНЫ И ОСНОВАНИЕ
+

Преамбула

+

Стороны — те же, что в Соглашении о партнёрском вознаграждении № 1 от «___» _______ 2026 г. («Соглашение»): Кооператив ПК «Друзья Коллективного Разума» («Оператор») и [наименование прокатной компании] («Партнёр»).

+

Side Letter — неотъемлемая часть Соглашения, регулирует только исторический долг и не меняет условия о партнёрском вознаграждении (10%, атрибуция, порядок расчётов).

+
+ +
+
01 / ПРИЗНАНИЕ ДОЛГА
+

Признание задолженности

+
1.1

Стороны признают задолженность Партнёра перед Оператором за выполненные работы за период ~3 месяца (по актам/переписке, перечислены в Приложении к настоящему Side Letter).

+
1.2

Стороны не имеют взаимных претензий по задолженности за указанный период, кроме урегулированной настоящим Side Letter.

+
+ +
+
02 / ФИКСАЦИЯ СУММЫ
+

Сумма и валютная привязка

+ + + + + + +
ПараметрЗначение
Эквивалент~15 000 USD
Валютная привязкавыражен в BTC по курсу BTC/USD на дату образования долга; рублёвая/долларовая переоценка — по курсу на дату расчётов
Индексацияинфляционная индексация за период образования долга (если применимо)
Дата образования[заполнить: последний акт за период долга]
+
2.1

Итоговая сумма фиксируется в Приложении к Side Letter в BTC и в USD-эквиваленте; стороны подтверждают её подписями.

+
+ +
+
03 / КОНВЕРТАЦИЯ
+

Конвертация долга в инвестицию

+
3.1

С момента подписания Side Letter долг полностью конвертируется в инвестиционный вклад Партнёра в Проект «Интеграция Simbiot в прокатный бизнес» (юнит: развитие Партнёра как витринного кейса платформы) на условиях инвестиционного соглашения (Приложение № 2 к Соглашению: доля / profit-share, [заполнить]).

+
3.2

С момента конвертации Партнёр не имеет требований к Оператору по возврату суммы долга как денежного обязательства; права Партнёра реализуются исключительно через условия инвестиционного соглашения (доля/profit-share, порядок распределения, выход).

+
3.3

Инвестиционный вклад не гарантирует доходность и не является займом; риск убытка несёт Партнёр как инвестор.

+
+ +
+
04 / ПОРЯДОК
+

Подписание и юридическая проверка

+
4.1

Side Letter подписывается одновременно с Соглашением № 1. Юридическая проверка обоих документов до подписания обязательна.

+
4.2

До подписания стороны заполняют: дату образования долга, состав работ (Приложение), точную сумму в BTC + USD-эквивалент, условия инвестиционного соглашения (доля/profit-share).

+
+ +
+
05 / ПОДПИСИ
+

Реквизиты

+
+
+
Оператор · Кооператив
+
+
Председатель правления · подпись
+
Alex Shkor · «___» _______ 2026 г.
+
+
+
Партнёр · [наименование прокатной компании]
+
+
[должность, ФИО] · подпись
+
М.П. · «___» _______ 2026 г.
+
+
+
+ + + +
+ + diff --git a/artifacts/self-improvement-log.md b/artifacts/self-improvement-log.md new file mode 100644 index 0000000..062ff92 --- /dev/null +++ b/artifacts/self-improvement-log.md @@ -0,0 +1,35 @@ +# Журнал ночной самодоработки харнеса + +Директива Alex (голосовое m-516173718, эскалация e-261005074512): ночами +создавать себе задачи, ставить эксперименты над собой, смотреть что работает +лучше/хуже и продолжать дорабатываться. + +Цикл: крон в 00:00 UTC (03:00 Минск) подаёт задачу «Ночная самодоработка» [M]; +ближайший hour-тик выполняет; каждая итерация дописывает строку ниже. + +Формат строки (одна на итерацию): + +`#N | дата | гипотеза | метрика до → после | вывод (работает/нет/нужно больше данных)` + +Правила: +- один эксперимент за ночь, мелкий (лёгкий — провести сразу, тяжёлый — задачей на день); +- фильтр AUM: эксперимент должен служить «≥ +1% к AUM в горизонте год+»; +- ночь — тишина в чате (кроме критичного); результат поднимает утренний дайджест; +- не выдумывать метрики: только измеримое из стора (`cli.py events/status/aum`), + логов тиков и этого журнала. + +## Итерации + +#0 | 2026-10-05 | Калибровка цикла (не эксперимент) | механизм: крон-податчик + журнал заведены, первая задача в очереди | ожидаем первую ночную итерацию + +#1 | 2026-10-05 | Дедуп «ВЫЖИМОК НИЖНИХ УРОВНЕЙ» в промпте тика: тихий мин-уровень кладёт 6 идентичных [gate:quiet]-строк в каждый шаг промпта hour-тика; гипотеза — схлопывание повторов с счётчиком ×N срежет burn без потери информации | секция ВЫЖИМОК в hour-промпте: 6 строк/522 байт → 1 строка/95 байт (−82% секции, −427 байт/шаг; при 991 шаге/сутки ≈ −0.4М байт ≈ ~100k промпт-токенов/сутки при 57.8k avg/шаг — оценка) | работает: фикс в scheduler.py build_context (дедуп по (level, text)), верифицирован на живой модели (×6 подряд в контексте), test_gate + test_task_batch ALL PASS; контроль — avg prompt/step в BudgetSpent hour за следующие сутки (базлайн 57,781) + +## Ретро суток 2026-10-04T13:00 → 2026-10-05T13:00 (UTC) + +- Задачи: закрыто 27 (в т.ч. 8 L), подано 29, эскалаций открыто 0 (3 raised / 3 acked). +- Падения: 1 CycleError hour (14:48, таймаут воркера 2880с — тик потерян целиком; max инвокация 2872с из 990 завершённых). +- Гейт работает: 1381 GateSkipped min-тиков = LLM не запускался в тишине (экономия). +- Burn: hour 57.3M промпт-токенов (991 шаг, avg 57.8k/шаг, completion всего 1.1%); min 5.5M (520 шагов, avg 10.6k). Доминирующая статья расхода — раздувание промпта hour-шага, а не число вызовов (инвокаций всего 8+56). +- AUM 26.7 у.е.; burn 9router 7д 1417.88 — burn остаётся прямым минусом к AUM, главный рычаг — тоньше контекст шага. +- Вывод: следующие эксперименты — что именно раздувает avg 57.8k/шаг (история сообщений? RAG? батч?), паттерн «дедуп + кап» применим к секциям эскалаций/инбокса. + diff --git a/artifacts/self-review-2026-10-04.md b/artifacts/self-review-2026-10-04.md new file mode 100644 index 0000000..32e3566 --- /dev/null +++ b/artifacts/self-review-2026-10-04.md @@ -0,0 +1,27 @@ +# Самоанализ харнеса — 2026-10-04 (t-261004152714) + +## Цифры +- Задачи: 147 всего — 142 done (S 51 / M 61 / L 30), 2 blocked, 3 running. Провалов мало, поток стабилен. +- Эскалации: 79 (warn 56 / info 19 / critical 4) — все acked, открытых 0. +- Инбокс TG: 100 сообщений (text 81, voice 15, video 1); транскрипция 16/16 непустая после фикса. +- Ошибки уровней: min 38, hour 6, day 1. Главный паттерн — таймаут LLM-воркера (480с/2880с). + +## Что работает +- Лестница sec→min→hour→day с батчем 3 задач/тик: пропускная способность есть, анти-старение очереди соблюдается. +- ES-стор + ed25519-подписи: verify проходит, тампер-цепочка цела. +- Идемпотентная доставка в TG (tg-notify/tg-deliver-file, SKIP на повторе) — дублей нет (t-260930083937). +- Голосовые: двухъярусный ASR, короткие фразы → large-v3-turbo, кэш по file_unique_id. +- Эскалации доходят и закрываются (e-261004144805 — фикс «смерть тика не воскрешает закрытые задачи» уже в git). + +## Что болит (→ задачи саморазвития) +1. **Звук из видео не извлекался** — kind=video не входил в TRANSCRIBABLE_KINDS и поллер не звал транскрипцию. ИСПРАВЛЕНО в этом тике: ffmpeg-извлечение дорожки (16к моно opus) + turbo-роутинг для ≤60с; e2e на m-516173698 подтверждён. Осталось: тесты и наблюдение за памятью. +2. **Память инбокс-поллера 3.7G peak** (systemd Consumption) — транскрипция в процессе поллера; нужен MemoryMax и/или вынос ASR-вызова из поллера. +3. **Кириллица в setMyCommands даёт HTTP 400** (костыль-транслит latin) — бот теряет русские подписи команд. +4. **Таймауты LLM-воркеров** (min×38, hour×6) — причина №1 потерь тиков; воркер убивается до записи BudgetSpent. +5. **Клоны задач из служебных 'list'** — ловушка чинилась дважды; нужен регресс-тест на генерацию задач из inbox-текста. + +## Задачи саморазвития (созданы, проверяемый результат) +- t-261004160531-ed25ed [M]: тесты voice-пайплайна (extract_audio_track + роутинг) — pytest зелёный, кейс видео↔текст. +- t-261004160531-780666 [L]: память поллера — MemoryMax + вынос тяжёлой транскрипции из поллера; peak < 1G за сутки. +- t-261004160532-690fcb [M]: TG-команды кириллицей — setMyCommands без 400 и без транслита. +- t-261004160532-0fb500 [M]: регресс-тест «служебный текст inbox не порождает задачу-клон». diff --git a/artifacts/shkor-beauty-prototype.html b/artifacts/shkor-beauty-prototype.html new file mode 100644 index 0000000..9b48343 --- /dev/null +++ b/artifacts/shkor-beauty-prototype.html @@ -0,0 +1,355 @@ + + + + + +Shkor beauty — корейская косметика в Минске (прототип витрины) + + + + + +
Прототип витрины — шаг 1 из плана Shkor beauty. Наполнение демонстрационное, оплата не подключена.
+ +
+
+ + +
+
+ +
+
+
+

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

+

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

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

Каталог

+

Пять разделов, как у эталона рынка — структура навигации Korealab.

+ +
+
+ +
+
+

Уход за лицом

+

Подкатегории первого уровня — быстрый вход в нужный шаг рутины.

+ +
+ +
+
−30%SKIN627
+
+ СывороткиSKIN627 +

Увлажняющая сыворотка для лица Hyaluron Centella Ampoule

+
35,00 руб.50,00 руб.
+ +
+
+ +
+
НовинкаJMsolution
+
+ ПэдыJMsolution +

Увлажняющие пэды с гиалуроновой кислотой Multi Hyaluronic

+
59,00 руб.
+ +
+
+ +
+
Beauty of Joseon
+
+ SPFBeauty of Joseon +

Солнцезащитный гель Relief Sun SPF50+ PA++++

+
49,00 руб.
+ +
+
+ +
+
−25%Biodance
+
+ МаскиBiodance +

Коллагеновая маска-пленка Collagen Wrapping Peel-off Mask

+
12,00 руб.16,00 руб.
+ +
+
+ +
+
Anua
+
+ ТонерыAnua +

Успокаивающий тонер с сердечником Centella Toner 77%

+
62,00 руб.
+ +
+
+ +
+
Banila Co
+
+ ОчищениеBanila Co +

Гидрофильное масло Clean It Zero Original

+
54,00 руб.
+ +
+
+ +
+
SKIN627
+
+ СывороткиSKIN627 +

Сыворотка для лица с ретинолом Retinol Pantenol Ampoule

+
35,00 руб.
+ +
+
+ +
+
НовинкаA'PIEU
+
+ ПатчиA'PIEU +

Патчи для глаз с молочной кислотой Milk One-Day Perfector

+
18,00 руб.
+ +
+
+ +
+
+
+ +
+
+

Уход за телом и волосами

+

Витрина разделов вне лица — по мотивам карточек эталона.

+
+
+
WEPROVE
+
+ Для душаWEPROVE +

Обновляющий гель для душа Renewing Aloe Shower Gel

+
29,00 руб.
+ +
+
+
+
−15%WEPROVE
+
+ ШампуниWEPROVE +

Восстанавливающий шампунь Daily Hair Repairing Shampoo

+
23,60 руб.27,80 руб.
+ +
+
+
+
Care:Nel
+
+ Кремы для рукCare:Nel +

Питательный крем для рук с ши и центеллой

+
14,00 руб.
+ +
+
+
+
Ciracle
+
+ АксессуарыCiracle +

Силиконовый спонж для умывания с чехлом

+
9,00 руб.
+ +
+
+
+
+
+ +
+
+

Бренды

+

Формат «Алфавит брендов» как у эталона — вход через доверие к марке.

+
+ A'PIEUANSKINAnuaAtopalm + AXIS-YBanila CoBeauty of JoseonBiodance + Care:NelCelimaxCos de BahaJMsolution + SKIN627WEPROVE +
+
+
+ +
+
+

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

+

Одно письмо в неделю: новинки, скидки и разборы составов. Без спама.

+
+ + +
+
+
+ + + +
+ + + + diff --git a/artifacts/shkor/README.md b/artifacts/shkor/README.md new file mode 100644 index 0000000..690e8c5 --- /dev/null +++ b/artifacts/shkor/README.md @@ -0,0 +1,79 @@ +# Shkor Beauty Assistant + +Beauty-ассистент магазина **Shkor beauty**: анализирует состояние кожи, подбирает продукты +из каталога витрины, оформляет заказ с доставкой и выдаёт инструкции по применению. +Подключается к **любому агенту** (LLM-бот, MCP-клиент, крон-скрипт, мобильное приложение). + +Задача-источник: `t-261005120225-672f52` (требования Alex). Каталог синхронизирован +с прототипом витрины `../shkor-beauty-prototype.html` (t-261005115918-d38b36). + +## Состав + +| Файл | Что это | +|---|---| +| `assistant.py` | Ядро: анализ кожи, рекомендации, заказы, инструкции. Один вход `handle(request) -> dict` | +| `connector.py` | Универсальный коннектор: STDIO (MCP-стиль), HTTP API, CLI, демо-чат | +| `catalog.json` | 12 товаров витрины с мета-данными (concerns, actives, routine_step) | +| `orders.json` | Локальный леджер заказов (создаётся при первом заказе) | + +## Подключение любым агентом + +**1) STDIO, MCP-стиль** — для Claude/любого MCP-клиента: +```json +{"mcpServers": {"shkor": {"command": "python3", "args": ["/root/simbiot-harness/artifacts/shkor/connector.py"], "transport": "stdio"}}} +``` +Протокол: построчный JSONL. `{"method":"tools/list"}` → список инструментов; +`{"method":"tools/call","params":{"name":"analyze_skin","arguments":{...}},"id":1}` → результат. + +**2) HTTP API** — агент = любой HTTP-клиент: +```bash +connector.py --http 127.0.0.1:8791 +curl -XPOST localhost:8791/call -d '{"name":"recommend","arguments":{"concerns":["dryness"]}}' +``` +Роуты: `GET /health`, `POST /tools/list`, `POST /call`. + +**3) CLI разовый вызов** — крон/скрипты: +```bash +connector.py --call analyze_skin --args '{"self_report":"стянутость и покраснения"}' +``` + +**4) Живая проверка:** `connector.py --demo` — полный сценарий: анализ → подбор → заказ → инструкция → статус. + +## Инструменты (7) + +- `analyze_skin` `{self_report?, symptoms?, photo_report?}` → тип кожи, concerns, hydration-индекс, + состояние барьера, фокус рутины. `photo_report` — принимаем отчёт vision-агента по селфи + (JSON или текст): мержим с самоотчётом, источники считаем отдельно. +- `recommend` `{concerns?, skin_type?, budget_max?}` → подобранные карточки с объяснением «почему». +- `search` `{query?, category?}` → поиск по каталогу (бренд/название/актив/категория). +- `place_order` `{items:[{id,qty}], delivery?, name?, phone?, address?, pay_method?, pay_link?}` → + заказ `SB-XXXXXXXX`, сумма, доставка. Платёж: `cash_on_delivery` или `pay_link` от + платёжного агента (Stripe/Альфа-Банк) — ассистент не хранит карты. +- `check_order` `{order_id}` → статус заказа и доставки. +- `delivery_options` `{cart_total?}` → курьер Минск / Европочта / самовывоз, бесплатно от 80 BYN. +- `usage_instructions` `{product_ids}` → пошаговые инструкции, порядок нанесения, предостережения + (ретинол только на ночь и не в один день с кислотами, SPF обязателен). + +## Пример потока агента + +``` +1. analyze_skin {"self_report": "кожа стянутая, лоб блестит, покраснения"} + → {"skin_type":"combination","concerns":["dryness","redness","oiliness"],"barrier":"compromised",...} +2. recommend {"concerns":["dryness","redness"], "skin_type":"sensitive"} + → пэды JMsolution, тонер Anua Centella 77%, сыворотка SKIN627 +3. place_order {"items":[{"id":"p05","qty":1},{"id":"p03","qty":1}],"delivery":"minsk_courier"} + → SB-XXXXXXXX, 111.0 BYN, доставка бесплатно (корзина ≥80), курьер сегодня/завтра +4. usage_instructions {"product_ids":["p05","p03"]} + → тонер (шаг 2) → SPF (шаг 7), предупреждения +5. check_order {"order_id":"SB-XXXXXXXX"} +``` + +## Границы (честно) + +- Анализ — эвристический (словарь стеммов RU/EN + голосование по типу), НЕ постановка диагноза; + в каждом ответе дисклеймер «не медицинская консультация». +- Оплата — заглушка `pending`: реальный эквайринг подключает агент-хост через `pay_link` + (см. задачу по методам оплаты портала). +- Каталог = витрина-прототип (12 SKU). При росте — заменить `catalog.json` без правок кода. + +Запуск тестов: `python3 connector.py --demo` (создаёт тестовый заказ в orders.json). diff --git a/artifacts/shkor/assistant.py b/artifacts/shkor/assistant.py new file mode 100644 index 0000000..3defe05 --- /dev/null +++ b/artifacts/shkor/assistant.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +"""Shkor Beauty Assistant — ядро. + +Универсальный beauty-ассистент магазина Shkor beauty: + * анализ состояния кожи (self-report + отчёт vision-агента), + * рекомендация продуктов из каталога каталога витрины, + * оформление заказа + варианта доставки, + * инструкция по применению (порядок рутины, частота, предостережения). + +Протокол: один вход `handle(request: dict) -> dict`, MCP-совместимая форма +(JSON-RPC tools/call) + простая форма {action, args}. Оболочки (STDIO/HTTP/CLI) +живут в connector.py — ассистент не знает, какой агент его вызывает. +""" +from __future__ import annotations + +import json +import os +import re +import time +import uuid +from pathlib import Path + +HERE = Path(__file__).resolve().parent +CATALOG = json.loads((HERE / "catalog.json").read_text(encoding="utf-8")) +ORDERS_PATH = HERE / "orders.json" +PRODUCTS = {p["id"]: p for p in CATALOG["products"]} + +# ── Анализ кожи ────────────────────────────────────────────────────────────── + +# ключи — стеммы (корни слов), чтобы ловить любые словоформы: «стянутая/стянутость», «покраснения/покраснение» +SYMPTOM_MAP = { + "стянут": "dryness", "сух": "dryness", "шелуш": "dryness", "обезвож": "dehydration", "dry": "dryness", + "flak": "dryness", "peel": "dryness", + "покрасн": "redness", "краснот": "redness", "раздраж": "sensitivity", "чувствитель": "sensitivity", + "sensitive": "sensitivity", "redness": "redness", "red ": "redness", "itch": "redness", + "блеск": "oiliness", "блест": "oiliness", "жирн": "oiliness", "oily": "oiliness", "shine": "oiliness", + "чёрных точ": "clogged-pores", "черных точ": "clogged-pores", "blackhead": "clogged-pores", + "расширенн пор": "clogged-pores", "поры": "clogged-pores", "pores": "clogged-pores", + "высып": "acne", "акн": "acne", "прыщ": "acne", "breakout": "acne", "угр": "acne", + "пигмент": "pigmentation", "пятн": "pigmentation", "веснушк": "pigmentation", "dark spot": "pigmentation", + "морщин": "fine-lines", "wrinkle": "fine-lines", "мимическ": "fine-lines", + "туск": "dullness", "dull": "dullness", "dullness": "dullness", + "тёмн круг": "dark-circles", "темн круг": "dark-circles", "dark circle": "dark-circles", "круги под глаз": "dark-circles", + "отёк": "puffiness", "отек": "puffiness", "puffin": "puffiness", "опух": "puffiness", + "чёрных угр": "clogged-pores", +} + +TYPE_RULES = [ + ("sensitive", 2, ["redness", "sensitivity"]), + ("dry", 2, ["dryness"]), + ("oily", 2, ["oiliness", "clogged-pores"]), + ("combination", 0, []), # базовый исход +] + +# точные имена concerns для структурированного ввода (symptoms:[...]) +CONCERN_ALIASES = {c: c for c in ["dryness", "dehydration", "sensitivity", "redness", "oiliness", + "clogged-pores", "acne", "pigmentation", "photoaging", "fine-lines", + "dullness", "dark-circles", "puffiness", "texture"]} + +ROUTINE_ORDER = ["cleanse", "toner", "exfoliate", "serum", "mask", "spot", "spf", + "body-cleanse", "body-care", "hair-wash", "tool"] + + +def analyze_skin(args: dict) -> dict: + """Анализ: self-report (текст/чек-лист) + опциональный vision-отчёт агента-фотографа. + + args: + self_report: str — свободный текст жалоб (опционально) + symptoms: [str] — структурированные симптомы (опционально) + photo_report: dict/str — отчёт vision-модели по селфи (опционально) + age: int, climate: str — контекст (опционально) + """ + signals: list[str] = [] + src = {"self_report": 0, "symptoms": 0, "photo_report": 0} + + if args.get("self_report"): + text = str(args["self_report"]).lower() + for kw, concern in SYMPTOM_MAP.items(): + if kw in text and concern not in signals: + signals.append(concern) + src["self_report"] = len([c for c in signals]) + + for s in args.get("symptoms") or []: + raw = str(s).lower().strip() + c = CONCERN_ALIASES.get(raw) or SYMPTOM_MAP.get(raw) or raw + if c and c not in signals: + signals.append(c) + src["symptoms"] += 1 + + photo = args.get("photo_report") + if photo: + blob = photo if isinstance(photo, str) else json.dumps(photo, ensure_ascii=False) + blob = blob.lower() + for kw, concern in SYMPTOM_MAP.items(): + if kw in blob and concern not in signals: + signals.append(concern) + src["photo_report"] = len([c for c in signals]) - src["self_report"] - src["symptoms"] + + # тип кожи: голосование по правилам + scores = {"sensitive": 0, "dry": 0, "oily": 0} + for concern in signals: + if concern in ("redness", "sensitivity"): + scores["sensitive"] += 1 + if concern in ("dryness", "dehydration", "fine-lines"): + scores["dry"] += 1 + if concern in ("oiliness", "clogged-pores", "acne"): + scores["oily"] += 1 + top = max(scores.values()) + winners = [k for k, v in scores.items() if v == top] + skin_type = winners[0] if len(winners) == 1 else "combination" # ничья/пусто → честный combination + + hydration = 5 # 0..10, нейтрально + if "dryness" in signals: + hydration -= 3 + if "oiliness" in signals: + hydration += 2 + if "dehydration" in signals: + hydration -= 2 + hydration = max(0, min(10, hydration)) + + barrier = "ok" + if "redness" in signals or "sensitivity" in signals: + barrier = "compromised" + if "acne" in signals and barrier == "compromised": + barrier = "compromised+breakouts" + + recommendations = [p["id"] for p in rank_products(signals, skin_type)] + + return { + "skin_type": skin_type, + "concerns": signals, + "hydration_index_0_10": hydration, + "barrier": barrier, + "sources": src, + "routine_focus": routine_focus(signals), + "recommended_product_ids": recommendations[:5], + "disclaimer": "Не медицинская консультация. При стойком раздражении/акне — к дерматологу.", + } + + +def routine_focus(concerns: list[str]) -> list[str]: + focus = [] + if "dryness" in concerns or "dehydration" in concerns: + focus.append("увлажнение: гиалурон + центелла, тонер-эссенция до сыворотки") + if "sensitivity" in concerns or "redness" in concerns: + focus.append("успокоение: центелла 77%, без кислот и ретинола первые 2 недели") + if "acne" in concerns or "clogged-pores" in concerns: + focus.append("очищение: гидрофильное масло + мягкий BHA, не щёлочь") + if "pigmentation" in concerns or "photoaging" in concerns: + focus.append("SPF каждый день + ретинол на ночь (вводить постепенно)") + if "fine-lines" in concerns: + focus.append("анти-эйдж: ретинол 2–3 р/нед, коллагеновые маски") + if "dullness" in concerns: + focus.append("тон/сияние: пэды с кислотами 2 р/нед, маска-плёнка") + if not focus: + focus.append("база: очищение → тонер → сыворотка → крем → SPF") + return focus + + +def rank_products(concerns: list[str], skin_type: str) -> list[dict]: + scored = [] + for p in CATALOG["products"]: + score = 0 + for c in concerns: + if c in p["concerns"]: + score += 3 + if skin_type == "sensitive" and ("retinol" in " ".join(p["actives"]) or "acid" in " ".join(p["actives"])): + score -= 2 + if skin_type == "dry" and "hyaluronic" in " ".join(p["actives"]): + score += 2 + if skin_type == "oily" and p["routine_step"] in ("cleanse", "exfoliate"): + score += 1 + if score > 0: + scored.append((score, p)) + scored.sort(key=lambda x: (-x[0], x[1]["price"])) + return [p for _, p in scored] or CATALOG["products"][:3] + + +# ── Рекомендации / поиск ────────────────────────────────────────────────────── + +def recommend(args: dict) -> dict: + concerns = [CONCERN_ALIASES.get(c.lower().strip()) or SYMPTOM_MAP.get(c.lower().strip(), c.lower().strip()) + for c in (args.get("concerns") or [])] + skin_type = args.get("skin_type") or "combination" + budget = args.get("budget_max") + picks = rank_products(concerns, skin_type) + if budget: + picks = [p for p in picks if p["price"] <= float(budget)] + return { + "matched": len(picks), + "products": [ + {"id": p["id"], "brand": p["brand"], "title": p["title"], "price": p["price"], + "why": ", ".join(c for c in concerns if c in p["concerns"]) or "базовый уход"} + for p in picks[: int(args.get("limit") or 5)] + ], + } + + +def search(args: dict) -> dict: + q = (args.get("query") or "").lower().strip() + cat = args.get("category") + out = [] + for p in CATALOG["products"]: + if cat and p["category"] != cat: + continue + if q and not (q in p["title"].lower() or q in p["brand"].lower() or q in p["sub"].lower() + or any(q in a for a in p["actives"])): + continue + out.append({"id": p["id"], "brand": p["brand"], "title": p["title"], "price": p["price"], + "category": p["category"], "sub": p["sub"]}) + return {"found": len(out), "products": out[: int(args.get("limit") or 20)]} + + +# ── Заказы и доставка ───────────────────────────────────────────────────────── + +DELIVERY = { + "minsk_courier": {"label": "Курьер по Минску", "eta": "сегодня/завтра", "price": 7.0, "free_from": 80.0}, + "europochta": {"label": "Европочта (РБ)", "eta": "2–3 дня", "price": 4.5, "free_from": 80.0}, + "pickup": {"label": "Самовывоз (Минск, пр-т Независимости)", "eta": "через 2 часа", "price": 0.0, "free_from": 0.0}, +} + + +def _load_orders() -> dict: + if ORDERS_PATH.exists(): + return json.loads(ORDERS_PATH.read_text(encoding="utf-8")) + return {} + + +def _save_orders(orders: dict) -> None: + ORDERS_PATH.write_text(json.dumps(orders, ensure_ascii=False, indent=1), encoding="utf-8") + + +def place_order(args: dict) -> dict: + items = args.get("items") or [] + if not items: + return {"error": "items обязателен: [{id, qty}]"} + lines, unknown = [], [] + total = 0.0 + for it in items: + p = PRODUCTS.get(it.get("id")) + if not p: + unknown.append(it.get("id")) + continue + qty = int(it.get("qty") or 1) + lines.append({"id": p["id"], "title": p["title"], "qty": qty, "price": p["price"], "sum": round(p["price"] * qty, 2)}) + total += p["price"] * qty + if unknown: + return {"error": f"неизвестные товары: {unknown}", "hint": "tools/call search для поиска id"} + + delivery_key = args.get("delivery") or "minsk_courier" + d = DELIVERY.get(delivery_key) + if not d: + return {"error": f"нет такой доставки: {delivery_key}", "available": list(DELIVERY)} + d_price = 0.0 if (d["free_from"] and total >= d["free_from"]) else d["price"] + + oid = "SB-" + uuid.uuid4().hex[:8].upper() + order = { + "order_id": oid, + "items": lines, + "subtotal": round(total, 2), + "delivery": {"key": delivery_key, **d, "charged": d_price}, + "total": round(total + d_price, 2), + "customer": {"name": args.get("name") or "", "phone": args.get("phone") or "", + "address": args.get("address") or ""}, + "payment": {"status": "pending", "method": args.get("pay_method") or "cash_on_delivery", + "pay_link": args.get("pay_link")}, # платёжку подключает агент-хост + "status": "placed", + "created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + orders = _load_orders() + orders[oid] = order + _save_orders(orders) + return { + "order_id": oid, "status": "placed", "total": order["total"], + "delivery_eta": d["eta"], "delivery_price": d_price, + "next": f"Статус: tools/call check_order {{\"order_id\": \"{oid}\"}}. " + f"Оплата: pay_method=cash_on_delivery или передайте pay_link от платёжного агента.", + } + + +def check_order(args: dict) -> dict: + oid = args.get("order_id") or "" + 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"]]} + + +def delivery_options(args: dict) -> dict: + total = float(args.get("cart_total") or 0) + return {"options": [ + {"key": k, "label": v["label"], "eta": v["eta"], + "price": 0.0 if (v["free_from"] and total >= v["free_from"]) else v["price"]} + for k, v in DELIVERY.items()]} + + +# ── Инструкции по применению ────────────────────────────────────────────────── + +INSTRUCTIONS = { + "cleanse": "Нанести на сухую кожу, помассировать 30–60 сек, смыть тёплой водой. Утром можно мягче.", + "toner": "После очищения протереть лицо/нанести ладонями, не вытирать — следующий шаг на влажную кожу.", + "exfoliate": "2–3 раза в неделю, не в один день с ретинолом. При покраснении — прекратить.", + "serum": "3–4 капли на влажную кожу, дать впитаться 1–2 мин. Ретинол — только на ночь, старт 2–3 р/нед.", + "mask": "1–2 раза в неделю на очищенную кожу, 20–30 мин, снять плёнку снизу вверх.", + "spot": "Патчи — 10–15 мин на зону под глазами, после — лёгкий крем.", + "spf": "Финальный утренний шаг, 2 пальца средства, обновлять каждые 2–3 часа при солнце. Обязателен при ретиноле/кислотах.", + "body-cleanse": "Ежедневно, мягкой губкой, завершить увлажнением.", + "body-care": "На влажную кожу рук после каждого мытья.", + "hair-wash": "Втирать в кожу головы 1–2 мин, смыть; бальзам — только по длине.", + "tool": "Спонж смочить тёплой водой, использовать с пенкой; промывать и сушить после каждого применения.", +} + + +def usage_instructions(args: dict) -> dict: + ids = args.get("product_ids") or args.get("ids") or [] + if isinstance(ids, str): + ids = [ids] + got = [] + for pid in ids: + p = PRODUCTS.get(pid) + if p: + got.append({"id": pid, "title": p["title"], "how": INSTRUCTIONS.get(p["routine_step"], "по инструкции бренда"), + "step_order": ROUTINE_ORDER.index(p["routine_step"]) + 1 if p["routine_step"] in ROUTINE_ORDER else 99, + "caution": p.get("caution")}) + if not got: + return {"error": "укажите product_ids (см. search/recommend)", "known_ids": list(PRODUCTS)} + got.sort(key=lambda x: x["step_order"]) + plan = " → ".join(g["title"].split()[0] for g in got) + return {"steps": got, "routine_summary": f"Порядок нанесения: {plan}. Утро: очищение→увлажнение→SPF; вечер: очищение→акт (ретинол/кислоты)→крем.", + "general": "Новое средство — сначала на сгиб локтя 24ч. Не сочетать ретинол с кислотами в один день."} + + +# ── Диспетчер инструментов ──────────────────────────────────────────────────── + +TOOLS = { + "analyze_skin": ("Анализ состояния кожи: self_report/symptoms/photo_report → тип, concerns, фокус рутины", + ["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?"]), + "check_order": ("Статус заказа и доставки", ["order_id"]), + "delivery_options": ("Варианты доставки и цены", ["cart_total?"]), + "usage_instructions": ("Инструкции по применению + порядок рутины", ["product_ids"]), +} + + +def handle(request: dict) -> dict: + """Единая точка входа. Понимает MCP tools/call и простую форму {action, args}.""" + if not isinstance(request, dict): + return {"error": "request должен быть JSON-объектом"} + + method = request.get("method") + if method == "tools/list": + return {"tools": [{"name": n, "description": d, "args": a} for n, (d, a) in TOOLS.items()]} + if method == "initialize": + return {"protocol": "shkor-assistant/1", "tools": list(TOOLS), "shop": CATALOG["shop"]} + if method == "tools/call": + params = request.get("params") or {} + name, arguments = params.get("name"), params.get("arguments") or {} + else: + name, arguments = request.get("action"), request.get("args") or request.get("arguments") or {} + + 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, + "delivery_options": delivery_options, "usage_instructions": usage_instructions}[name] + try: + result = fn(arguments if isinstance(arguments, dict) else {}) + except Exception as e: # noqa: BLE001 — ассистент отвечает структурой, не падает + return {"error": f"{type(e).__name__}: {e}"} + rid = request.get("id") + out = {"result": result} + if rid is not None: + out["id"] = rid + return out + + +if __name__ == "__main__": + import sys + payload = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {"method": "tools/list"} + print(json.dumps(handle(payload), ensure_ascii=False, indent=1)) diff --git a/artifacts/shkor/catalog.json b/artifacts/shkor/catalog.json new file mode 100644 index 0000000..e992f30 --- /dev/null +++ b/artifacts/shkor/catalog.json @@ -0,0 +1,19 @@ +{ + "shop": "Shkor beauty", + "currency": "BYN", + "note": "Каталог синхронизирован с прототипом витрины artifacts/shkor-beauty-prototype.html (t-261005115918-d38b36)", + "products": [ + {"id": "p01", "category": "face", "sub": "Сыворотки", "brand": "SKIN627", "title": "Увлажняющая сыворотка для лица Hyaluron Centella Ampoule", "price": 35.00, "old_price": 50.00, "concerns": ["dryness", "sensitivity", "dehydration"], "actives": ["hyaluronic acid", "centella"], "routine_step": "serum"}, + {"id": "p02", "category": "face", "sub": "Пэды", "brand": "JMsolution", "title": "Увлажняющие пэды с гиалуроновой кислотой Multi Hyaluronic", "price": 59.00, "concerns": ["dryness", "dullness"], "actives": ["multi hyaluronic"], "routine_step": "exfoliate"}, + {"id": "p03", "category": "face", "sub": "SPF", "brand": "Beauty of Joseon", "title": "Солнцезащитный гель Relief Sun SPF50+ PA++++", "price": 49.00, "concerns": ["photoaging", "pigmentation", "sensitivity"], "actives": ["SPF50+", "rice extract"], "routine_step": "spf"}, + {"id": "p04", "category": "face", "sub": "Маски", "brand": "Biodance", "title": "Коллагеновая маска-пленка Collagen Wrapping Peel-off Mask", "price": 12.00, "concerns": ["dullness", "fine-lines"], "actives": ["collagen"], "routine_step": "mask"}, + {"id": "p05", "category": "face", "sub": "Тонеры", "brand": "Anua", "title": "Успокаивающий тонер с сердечником Centella Toner 77%", "price": 62.00, "concerns": ["sensitivity", "redness"], "actives": ["centella 77%"], "routine_step": "toner"}, + {"id": "p06", "category": "face", "sub": "Очищение", "brand": "Banila Co", "title": "Гидрофильное масло Clean It Zero Original", "price": 54.00, "concerns": ["clogged-pores", "makeup-removal"], "actives": ["cleansing oil"], "routine_step": "cleanse"}, + {"id": "p07", "category": "face", "sub": "Сыворотки", "brand": "SKIN627", "title": "Сыворотка для лица с ретинолом Retinol Pantenol Ampoule", "price": 35.00, "concerns": ["fine-lines", "texture", "acne"], "actives": ["retinol", "panthenol"], "routine_step": "serum", "caution": "ретинол: вводить 2–3 раза в неделю, только на ночь, обязателен SPF утром"}, + {"id": "p08", "category": "face", "sub": "Патчи", "brand": "A'PIEU", "title": "Патчи для глаз с молочной кислотой Milk One-Day Perfector", "price": 18.00, "concerns": ["dark-circles", "puffiness"], "actives": ["lactic acid", "milk"], "routine_step": "spot"}, + {"id": "p09", "category": "body", "sub": "Для душа", "brand": "WEPROVE", "title": "Обновляющий гель для душа Renewing Aloe Shower Gel", "price": 29.00, "concerns": ["body-dryness"], "actives": ["aloe"], "routine_step": "body-cleanse"}, + {"id": "p10", "category": "hair", "sub": "Шампуни", "brand": "WEPROVE", "title": "Восстанавливающий шампунь Daily Hair Repairing Shampoo", "price": 23.60, "concerns": ["hair-damage"], "actives": ["repairing complex"], "routine_step": "hair-wash"}, + {"id": "p11", "category": "body", "sub": "Кремы для рук", "brand": "Care:Nel", "title": "Питательный крем для рук с ши и центеллой", "price": 14.00, "concerns": ["body-dryness"], "actives": ["shea", "centella"], "routine_step": "body-care"}, + {"id": "p12", "category": "accessories", "sub": "Аксессуары", "brand": "Ciracle", "title": "Силиконовый спонж для умывания с чехлом", "price": 9.00, "concerns": [], "actives": [], "routine_step": "tool"} + ] +} diff --git a/artifacts/shkor/connector.py b/artifacts/shkor/connector.py new file mode 100644 index 0000000..7b7480c --- /dev/null +++ b/artifacts/shkor/connector.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Shkor Beauty Assistant — универсальный коннектор. + +Один и тот же ассистент — три способа подключения для ЛЮБОГО агента: + + 1) STDIO MCP-стиль (Claude/любой MCP-клиент): + assistant → connector.py (JSONL: {"method":"tools/list"} по строке) + в конфиге агента: + {"mcpServers": {"shkor": {"command": "python3", "args": [".../connector.py"], "transport": "stdio"}}} + + 2) HTTP JSON API (агент = просто HTTP-клиент): + connector.py --http 127.0.0.1:8791 + POST /tools/list | POST /call {"name": "...", "arguments": {...}} + + 3) CLI разовый вызов (curl/крон/скрипт): + connector.py --call analyze_skin --args '{"self_report":"стянутость и покраснение"}' + + 4) Демо-чат (для живой проверки человеком): + connector.py --demo + +Форма запроса принимает MCP tools/call и простую {action, args} — см. assistant.handle(). +""" +from __future__ import annotations + +import argparse +import json +import sys +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from assistant import handle # noqa: E402 + + +# ── 1) STDIO: JSONL, одна строка = один запрос ─────────────────────────────── +def serve_stdio() -> None: + print(json.dumps({"ok": True, "transport": "stdio-jsonl", + "hint": "отправляйте JSON-строки, читайте ответ построчно"}, ensure_ascii=False), flush=True) + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + resp = handle(json.loads(line)) + except json.JSONDecodeError as e: + resp = {"error": f"bad json: {e}"} + print(json.dumps(resp, ensure_ascii=False), flush=True) + + +# ── 2) HTTP ────────────────────────────────────────────────────────────────── +def serve_http(addr: str) -> None: + host, port = addr.split(":") + port_i = int(port) + + class H(BaseHTTPRequestHandler): + def _send(self, code: int, payload: dict) -> None: + body = json.dumps(payload, ensure_ascii=False).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self) -> None: # health / открыть в браузере + if self.path in ("/", "/health"): + self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1}) + else: + self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]}) + + def do_POST(self) -> None: + try: + n = int(self.headers.get("Content-Length") or 0) + data = json.loads(self.rfile.read(n) or b"{}") + except Exception as e: # noqa: BLE001 + self._send(400, {"error": f"bad request: {e}"}) + return + if self.path == "/tools/list": + self._send(200, handle({"method": "tools/list"})) + elif self.path == "/call": + self._send(200, handle({"method": "tools/call", "params": data})) + else: + self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]}) + + def log_message(self, *a) -> None: # тише в логи + pass + + srv = ThreadingHTTPServer((host, port_i), H) + print(f"shkor-assistant HTTP on http://{host}:{port_i} (POST /tools/list, POST /call)", file=sys.stderr) + srv.serve_forever() + + +# ── 4) Демо-чат ─────────────────────────────────────────────────────────────── +DEMO = [ + ("analyze_skin", {"self_report": "кожа стянутая после умывания, лоб блестит к обеду, периодически покраснения"}), + ("recommend", {"concerns": ["dryness", "redness"], "skin_type": "sensitive", "budget_max": 70}), + ("place_order", {"items": [{"id": "p05", "qty": 1}, {"id": "p03", "qty": 1}], "delivery": "minsk_courier", + "name": "Тест", "phone": "+375XX0000000"}), + ("usage_instructions", {"product_ids": ["p05", "p03"]}), + ("check_order", {}), # order_id подставится после place_order +] + + +def run_demo() -> None: + last_order = None + for action, args in DEMO: + if action == "check_order": + if not last_order: + continue + args = {"order_id": last_order} + print(f"\n▶ {action} {json.dumps(args, ensure_ascii=False)}") + resp = handle({"method": "tools/call", "params": {"name": action, "arguments": args}}) + print(json.dumps(resp.get("result", resp), ensure_ascii=False, indent=1)[:1200]) + if action == "place_order" and (resp.get("result") or {}).get("order_id"): + last_order = resp["result"]["order_id"] + + +def main() -> None: + ap = argparse.ArgumentParser(description="Shkor Beauty Assistant connector") + ap.add_argument("--http", metavar="HOST:PORT", help="поднять HTTP API") + ap.add_argument("--stdio", action="store_true", help="STDIO JSONL (MCP-стиль)") + ap.add_argument("--call", metavar="TOOL", help="разовый вызов инструмента") + ap.add_argument("--args", default="{}", help="JSON-аргументы для --call") + ap.add_argument("--list-tools", action="store_true") + ap.add_argument("--demo", action="store_true") + a = ap.parse_args() + + if a.http: + serve_http(a.http) + elif a.stdio: + serve_stdio() + elif a.list_tools: + print(json.dumps(handle({"method": "tools/list"}), ensure_ascii=False, indent=1)) + elif a.demo: + run_demo() + elif a.call: + try: + arguments = json.loads(a.args) + except json.JSONDecodeError as e: + sys.exit(f"bad --args json: {e}") + print(json.dumps(handle({"method": "tools/call", "params": {"name": a.call, "arguments": arguments}}), + ensure_ascii=False, indent=1)) + else: + ap.print_help() + + +if __name__ == "__main__": + main() diff --git a/bin/simbiot b/bin/simbiot index 90444ea..5a39abc 100755 --- a/bin/simbiot +++ b/bin/simbiot @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Simbiot Harness: запуск без установки обёртки (cli.py + scheduler-демон). # simbiot help — справка по CLI-командам (как python cli.py -h) +export SIMBIOT_HOME="${SIMBIOT_HOME:-/var/lib/simbiot-harness}" # simbiot — без аргументов: статус + как управлять демоном/CLI # simbiot --help — как help (аргумент для argparse-привычки) DIR="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)" diff --git a/docs/aum/balances.json b/docs/aum/balances.json index c48fd92..db23fd4 100644 --- a/docs/aum/balances.json +++ b/docs/aum/balances.json @@ -7,7 +7,7 @@ "amount_usd": 26.7, "src": "openrouter.ai/api/v1/credits (замер 2026-10-03 23:5x UTC: total=163, usage=136.30)", "auto": true, - "measured_at": "2026-10-04T00:15:21+00:00" + "measured_at": "2026-10-04T01:13:01+00:00" }, { "name": "US LLC Delaware (операционный)", @@ -24,7 +24,7 @@ "amount_usd": 0.0, "src": "api.stripe.com/v1/balance livemode=True, STRIPE_LIVE_KEY из coop/.env (замер 2026-10-03): available=0, pending=0", "auto": true, - "measured_at": "2026-10-04T00:15:22+00:00" + "measured_at": "2026-10-04T01:13:01+00:00" } ], "debts": [], diff --git a/docs/digests/hour-2026-10-04-0700.md b/docs/digests/hour-2026-10-04-0700.md new file mode 100644 index 0000000..30e56f9 --- /dev/null +++ b/docs/digests/hour-2026-10-04-0700.md @@ -0,0 +1,38 @@ +# Дайджест hour 2026-10-04 ~07:00 UTC + +## Батч задач (3/3 закрыты, все [M]) + +1. **t-261004061059-84e112** — MCP для продвижения сервисов. Диагноз: + `alex.score.com` не резолвится глобально (зона score.com → мёртвые + NS.SCORE.NET, SERVFAIL по DoH Google/Cloudflare, EDE 22) — деплой туда + невозможен, нужно восстановление делегирования владельцем. Рабочая точка + входа — https://cil.by/mcp (E2E: initialize/whoami OK). Найден и исправлен + прод-баг портала: `/mcp tools/call` падал 500 NameError (mcp регистрируется + раньше stripe/alfa/booking — хелперы `_gen_slots*`/`_send_email`/ + `_stripe_product_objects` не определены). Фикс `_late_bind_ctx()` на каждом + tools/call; коммит f0febd9 (shkor-labs/portal), портал перезапущен, + бэкап mcp.py.backup-1004-ctx-latebind. MCP_CONNECT.md дополнен. +2. **t-261004061251-2d3f56** — запись на 20-мин разбор через MCP работает: + booking_slots(type='session') → 70 свободных слотов (18:00–22:00 Минск, + график 3/3), booking_create(kind='session') → jitsi+запись+транскрибация + + письма/TG. Новый инструмент **coop_catalog**: все сервисы кооператива с + ценами и Stripe pay_url прямо через MCP (live: Симбиот 33/59/1000$, + Рилсап 199/299/699$/мес, Lunara/ReLeaf 4.99$/мес) — полный цикл покупки + без веб-UI (MCP-first). +3. **t-261004062405-fa9926** — MiroFish разобран (клон, ~17.2k строк backend). + Вердикт: движок не берём (стоимость: 100 агентов×40 раундов ≈ $10–25 на + qwen-plus, ×10–30 на премиум; + Zep Cloud SaaS; OASIS/camel-ai стек; + платформа, не библиотека). Идеи заимствования: персоны-из-сущностей графа, + ReportAgent (4 инструмента + интервью агентов + трекер полноты), + episode→граф памяти на своём RAG, дешёвая симуляция спроса перед запусками, + NL→конфиг прогона с budget-cap, IPC-прогресс. Отчёт: + artifacts/mirofish-analysis-2026-10-04.md (доставлен в TG). + +## Инфра/события + +- Мин-тик: 6× gate:quiet — событий выше порога нет, LLM не запускался. +- Фоновые задачи: нет (list пуст). +- Эскалации: открытых нет; e-261003170542-389f29 (hour Request timed out) уже + acked уровнем day — повторный ack не требуется. +- Внешние сбои, не наша зона: DNS score.com мёртв (см. п.1); TLS on-demand + корректно отвергает alex.cil.by (ask-гейт /api/caddy/ask — by design). diff --git a/docs/digests/hour-2026-10-04-1000.md b/docs/digests/hour-2026-10-04-1000.md new file mode 100644 index 0000000..6a68b92 --- /dev/null +++ b/docs/digests/hour-2026-10-04-1000.md @@ -0,0 +1,59 @@ +# Дайджест hour 2026-10-04 ~10:00 UTC + +## Батч задач (1/2 закрыта; вторая — L, подготовка готова, ждёт Alex) + +1. **t-261004091808-70985e [M] — ЗАКРЫТА** (failed[NNN]/DNA у агентов). + Диагноз: heartbeat шли нормально (agent_stats обновлялись каждую минуту), но + репортёр coop-reporter v7 жёстко опрашивал `systemctl is-active hermes-agent` — + у переведённых на Simbiot агентов (Mentalis/Lunar/Rita/Roy/Mon, + harness_type=simbiot) hermes-agent остановлен при переезде, главный юнит — + simbiot-harness.service → дашборд красил живых агентов в failed[NNN]. + «DNA — не синхр.» — отдельная причина: harness_setups пуста, simbiot-cli + (node) на машинах агентов никогда не ставился → CLI-sync был невозможен. + Фикс (3 части): (а) репортёр v8 — активный simbiot-harness → статус + «active (simbiot)», плюс сам собирает DNA-факты (skills/SKILL.md + README.md, + скоринг по формуле CLI) и пушит на новый POST /api/agent/dna → upsert + harness_setups; (б) dashboard.py — fallback-матч DNA по developer_name, когда + hostname машины не содержит slug агента (случай AI corp на Ubuntu-2404…); + (в) agents_dashboard.html — фронт принимал только service==='active', + добавлен 'active (simbiot)' (чип «online · simbiot»). Раскатка v8 — + автоматическая через self-update по manifest, заняла ~5 мин на весь флот, + SSH-ручного вмешательства не потребовалось. Верификация по БД: все цели + тикета online · simbiot, DNA=59/100 (Mentalis/Lunar/Rita), Roy/Mon починены + тем же фиксом, 14 агентов с DNA. портал перезапускан ×2 (reporter.py, + dashboard.py), coop-portal active. + +2. **t-261004092629-40655b [L] — НЕ закрыта, подготовка выполнена** (подписать + SAFE MFN с предыдущими инвесторами; подписание — сторона Alex). + Сделано: подписной комплект в `/root/coop/fundraising/safe-package/` — + каноническая **YC Post-Money SAFE (MFN Only)** FINAL.docx (форма №3 по + RAISE_PLAN: uncapped, no discount; источник ycombinator.com/safe, скачана + 04.10, целостность проверена) + YC SAFE User Guide.pdf + шаблон MFN side + letter + SIGNING_CHECKLIST.md с шагами из постановки. Чек-лист доставлен + Alex в TG (идемпотентно). Блокер по списку инвесторов: в coop.db реальных + инвесторов НЕТ — `backers` содержит 2 тестовые записи, stripe/alfa payments — + тесты, presale_orders — предзаказы, не чеки → список предыдущих инвесторов + ведётся вне портала, у Alex. Остаток (за Alex): список+реквизиты, юр. ревью + формы и side letter до первой подписи ($1.5–3K), санкционный скрининг + каждого инвестора на дату подписания (OFAC/ЕС — критично для резидентов РБ), + подписание, фиксация экземпляров в safe-package/signed/. Задача вернётся + в ready следующего тика. + +## Инфра/события + +- Мин-тик: 6× gate:quiet — событий выше порога нет, LLM не запускался. +- Фоновые задачи: нет (list пуст). +- Эскалации: открытых нет, ack-ов не требовалось. +- coop-portal перезапускан 2× в рамках фикса (10:18, 10:27), перерыв сервиса + секунды, ошибок в journalctl нет. +- Раскатка coop-reporter v8 прошла на все машины с reporter-токеном; DNA-пуш + идёт с каждым тиком таймера (2 мин), данные обновляются. +- Конфиг уровней: масштабированная контекст-схема по тактам активна + (e-261001082710): min 256k → hour 512k → day 1M ctx_tokens. + +## AUM + +- Прямых расходов тик не добавил; фиксом убран ложный «failed» по 5 прод-агентам + (сигнальный шум → ложные ремонты/рестарты больше не триггерятся). +- SAFE-комплект приближает закрытие рейз-шага (юр. ревью — плановые $1.5–3K, + решение за Alex). diff --git a/docs/digests/hour-2026-10-05-1400.md b/docs/digests/hour-2026-10-05-1400.md new file mode 100644 index 0000000..4c97660 --- /dev/null +++ b/docs/digests/hour-2026-10-05-1400.md @@ -0,0 +1,58 @@ +# Дайджест hour 2026-10-05 ~14:00 UTC + +## Батч задач (1/1 закрыта) + +1. **t-261005135853-186b42 [M] — ЗАКРЫТА** (гейтвей: входящие документы на диск). + Диагноз подтверждён: `harness/tg_inbox.py` качал только voice (voice-пайплайн), + documents/photo терялись — file_id в событии есть, файла нет (кейс 4 PDF-выписок + Alex 05.10, доставал вручную getFile'ом). + Фикс: `save_document()` — при kind=document|photo getFile→download→атомарная + запись (.part→rename) в `inbox/files/.` (новый + `paths.inbox_files_dir()`, env `SIMBIOT_INBOX_FILES_DIR`); фото — максимальный + размер. В MessageReceived теперь `file_path` (абс, открывается с любого уровня) + + `file_name`/`file_size`/`mime_type` — триаж видит что за файл без getFile. + Идемпотентно по file_unique_id (повторный апдейт не перекачивает), сбой + скачивания (истёкший у TG файл, HTTP 400/410) не валит поллер — file_id + остаётся в событии для ручного достёгивания. Голосовые — как раньше. + Верификация: `tests/test_tg_documents.py` — 22 PASS (document e2e, photo e2e, + идемпотентность, сбой, _file_ext); регресс voice_video/seen_indication/ + inbox_batch — зелёный. Поллер перезапущен, активен (systemd). Коммит c5c3e8f. + Живая проверка: следующий отправленный боту документ сам появится в inbox/files/. + +## Батч входящих m-516173771..777 (7 сообщений, один разговор) — отвечено сводно + +Контекст: Alex скинул 4 PDF-выписки → «гейтвей их не сохраняет» (→ задача выше, +закрыта этим же тиком) → «сделай реальную ссылку на оплату в баксах с alipay» +→ «на $100» → «ссылку» → «а тут уже почему-то нету алипей, хоть в предыдущей +линке был». + +Сделано: +- Боевой $100-линк подтверждён и отдан: https://buy.stripe.com/3cIdR82WH4RpfPY3EY1B60c + (plink_1UNCSvBT53G7FGSQDqXJOE8W, price $100 USD one_time, livemode). +- Почему нет Alipay — разбрано по факам, а не догадкой: через live-API проверена + payment_method_configuration — alipay/wechat_pay/paypal `available=false` + (методы НЕ активированы в боевом аккаунте Stripe), card и amazon_pay — on. + Попытки: пересоздание линка с явными методами → «ensure the type is activated + in your dashboard»; Checkout Session с явными методами → тот же отказ; + включение display_preference через API → невозможно (available=false). + Вывод: единственный путь — ручной шаг в дашборде (Settings → Payment methods), + после чего пересоздание линка — одна команда. Alex'у дана инструкция и обещание + пересоздать по сигналу «алипей включил». +- Amazon Pay в payment links Stripe не поддерживается в принципе (список + допустимых payment_method_types его не содержит; он работает только динамически + в чекауте — потому и был виден в старых линках без явных методов). + +## Инциденты/эскалации + +- Эскалаций от нижних уровней за час нет (4 тихих минутных тика, 0 событий выше + порога — LLM не запускался, burn экономлен). +- В ходе теста поймал и починил собственный баг: ранние выходы save_document + возвращали кортеж (None, None) вместо None — тест ловит такие вещи именно + поэтому. + +## AUM-фильтр часа + +Фикс гейтвея — прямая база долгосрочной AUM: до него входящие документы Алекса +(выписки банков — материал для учёта AUM/оборотки) терялись физически, а ручное +доставание съедало тики уровней. Alipay-линк — короткий доход (предзаказы $100), +но блокер снят до предела: осталась одна ручная кнопка у Alex. diff --git a/harness/commands.py b/harness/commands.py index 667ab4b..fcc04fa 100644 --- a/harness/commands.py +++ b/harness/commands.py @@ -147,6 +147,28 @@ def archive_stale_failed(max_age_hours: float = 24.0, now=None) -> list: return closed +def block_task(task_id: str, reason: str = "ожидает уточнений") -> str: + """Заблокировать живую задачу: TaskBlocked → статус blocked (t-261004122019). + + Задача есть, но не готова к работе — ждёт уточнений от Alex или внешнего + события. scheduler берёт только ready → blocked не попадает в батчи. + Обратно: requeue (blocked → ready). Идемпотентно: уже blocked — no-op. + """ + model = es.run_projections() + t = model["tasks"].get(task_id) + if t is None: + raise ValueError(f"задача {task_id} не найдена") + if t["status"] == "blocked": + return task_id # уже заблокирована — no-op + if t["status"] in ("done", "closed"): + raise ValueError( + f"задача {task_id} в статусе {t['status']} — blocked только для живых; " + f"оживить можно requeue") + es.append_event("task", task_id, "TaskBlocked", + {"by": "manual", "reason": reason[:300]}) + return task_id + + def requeue_task(task_id: str, reason: str = "manual requeue"): """Вернуть failed/running/прерванную задачу в очередь (TaskPromoted → ready). @@ -159,7 +181,7 @@ def requeue_task(task_id: str, reason: str = "manual requeue"): raise ValueError(f"задача {task_id} не найдена") # closed тоже возвращаем (авто-схлопывание старых фейлов, e-261003094432): # закрытая в архив задача может понадобиться снова — осознанная доработка. - if t["status"] not in ("failed", "closed", "done", "running"): + if t["status"] not in ("failed", "closed", "done", "running", "blocked"): raise ValueError(f"задача {task_id} в статусе {t['status']} — requeue не нужен") # идемпотентная доставка (t-260930083937): requeue = новая работа над # задачей, её результат должен уйти в чат заново → сбрасываем факт diff --git a/harness/dashboard.html b/harness/dashboard.html index 078cdf7..96c1e4c 100644 --- a/harness/dashboard.html +++ b/harness/dashboard.html @@ -93,6 +93,10 @@ .chat-send{border:1px solid var(--ink);background:var(--signal);color:#fff;font-weight:700;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;padding:0 20px;cursor:pointer;border-radius:6px} .chat-send:disabled{opacity:.45;cursor:default} .chat-note{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--dim);margin:8px 2px 0} + /* живой статус каналов (t-261004103718-d8112b) */ + .chan{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--ink);margin-right:16px;white-space:nowrap} + .chan .dot{width:8px;height:8px} + .chan small{color:var(--dim);font-size:11px} /* ── детали агента (t-261002175316-eb24cc) ── */ .back{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.1em;text-transform:uppercase; border:1px solid var(--ink);background:transparent;color:var(--dim);padding:8px 14px;cursor:pointer;border-radius:6px} @@ -156,7 +160,7 @@

Чат с Simbiot

-
сообщение уходит агенту по порталу-каналу, ответ придёт сам (обычно ≤ 1 мин) · тоже дублируется в TG, если бот подключён
+
сообщение уходит агенту по порталу-каналу, ответ придёт сам (обычно ≤ 1 мин) · каналы: каналы: узнаю…
загружаю…
@@ -164,7 +168,7 @@
-
канал: портал (portal:<agent>) · ответ агент пишет сам по такту
+
канал: портал (portal:<agent>) · ответ агент пишет сам по такту · статус каналов: узнаю…
@@ -455,6 +459,46 @@ function chatBubble(m){ `
${esc(who)}${t ? " · " + esc(t) : ""}${pend}${rep}${esc(m.text || "")}
`; } +// ── живой статус каналов (t-261004103718-d8112b) ────────────────────────── +// ok → зелёный + возраст последнего успеха; нет свежего штампа → красный. +// hermes-лейбл — только если бэкенд подтвердил маркер старого Гермес-харнеса. +function chanAge(s){ + if (s.age_s == null) return ""; + if (s.age_s < 60) return s.age_s + "с назад"; + if (s.age_s < 3600) return Math.round(s.age_s / 60) + "м назад"; + return Math.round(s.age_s / 3600) + "ч назад"; +} +function chanChip(name, label, s){ + s = s || {}; + const cls = s.ok ? "" : "err"; + const detail = s.ok ? (chanAge(s) || "успех") : (s.detail || "нет связи"); + return `${label}` + + `${esc(detail)}`; +} +function renderChannels(ch){ + const box = $("chan-status"); + if (!box) return; + if (!ch){ return; } // бэкенд без статусов — оставляем «узнаю…» до след. тика + let html = ""; + if (ch.hermes) html += `⚠ старый харнес (Hermes): данные могут быть устаревшими`; + html += chanChip("portal", "портал", ch.portal); + const tg = ch.telegram || {}; + if (tg.token_loaded === false){ + html += `telegramбот не подключён`; + } else { + html += chanChip("tg", "telegram", tg); + } + box.innerHTML = html; + // краткая сводка там же, где раньше жил статичный «тоже дублируется в TG…» + const note = $("chan-note"); + if (note){ + const p = ch.portal || {}, t = ch.telegram || {}; + const fmt = s => s.ok ? `ok (${esc(chanAge(s) || "сейчас")})` + : `down`; + note.innerHTML = "статус: портал " + fmt(p) + " · telegram " + fmt(t); + } +} + async function chatTick(){ if (getViewName() !== "chat") return; chatOpen = true; @@ -469,6 +513,7 @@ async function chatTick(){ log.innerHTML = html || `
сообщений пока нет — напиши первым
`; if (stick) log.scrollTop = log.scrollHeight; $("chat-note").textContent = "канал: портал (portal:" + (s.agent || "?") + ") · ответ агент пишет сам по такту"; + renderChannels(s.channels); }catch(e){ if (e instanceof SyntaxError) return; // полу-JSON от 503 — следующий тик перепишет log.innerHTML = `
чат недоступен: ${esc(e.message)} — попробую через ${CHAT_REFRESH}с
`; diff --git a/harness/dashboard.py b/harness/dashboard.py index 6dd73c7..dde21d0 100644 --- a/harness/dashboard.py +++ b/harness/dashboard.py @@ -35,7 +35,7 @@ _HEALTH_PORT = int(os.environ.get("SIMBIOT_DASH_HEALTH_PORT", "8766")) _TOKEN = os.environ.get("SIMBIOT_DASH_TOKEN", "") _REFRESH = int(os.environ.get("SIMBIOT_DASH_REFRESH", "5")) -_STATUS_ORDER = {"ready": 0, "running": 1, "failed": 2, "done": 3, "closed": 4} +_STATUS_ORDER = {"ready": 0, "running": 1, "blocked": 2, "failed": 3, "done": 4, "closed": 5} _SIZE_COLOR = {"S": "#1d7a3e", "M": "#a3690f", "L": "#FF4F00", "XL": "#b3261e"} # ── чат Simbiot прямо в дашборде (t-261001100740-99d55e) ────────────────── @@ -183,7 +183,33 @@ def build_chat(limit: int = 80) -> dict: collapsed[-1]["ts"] = m["ts"] continue collapsed.append(m) - return {"ok": True, "agent": agent_name, "messages": collapsed[-limit:]} + out = {"ok": True, "agent": agent_name, "messages": collapsed[-limit:]} + # живой статус каналов (t-261004103718-d8112b): UI рендерит индикаторы + # вместо статичного «тоже дублируется в TG, если бот подключён» + try: + st = channel_status() + tg = _tg_stats() + tgst = st.get("telegram") or {} + tgst["token_loaded"] = tg.get("token_loaded") + tgst["received_24h"] = tg.get("received_24h") + tgst["last_msg_ts"] = tg.get("last_msg_ts") + # fallback: штампа поллера нет (старый main-харнес до рестарта), но + # токен загружен и свежие TG-сообщения капают в стор → канал жив. + if (not tgst.get("ok") and tg.get("token_loaded") + and (tg.get("last_msg_age_s") is not None) + and tg["last_msg_age_s"] <= 600): + tgst["ok"] = True + tgst["age_s"] = tg["last_msg_age_s"] + tgst["detail"] = "жив по событиям стора (штампа поллера ещё нет)" + st["telegram"] = tgst + st["hermes"] = _hermes_marker() + out["channels"] = st + except Exception as e: + # статус — диагностика: чат не должен падать из-за него, но причина + # должна быть видна (journalctl), иначе «пропавшие индикаторы» + # отлаживаются вслепую (t-261004103718: молчаливый pass скрыл сбой) + print(f"[chat] channels status error: {e}", flush=True) + return out def chat_send(text: str) -> dict: @@ -291,7 +317,7 @@ def build_state() -> dict: "tasks_total": len(tasks), "tasks_by_status": { s: sum(1 for t in tasks if t["status"] == s) - for s in ("ready", "running", "failed", "done", "closed") + for s in ("ready", "running", "blocked", "failed", "done", "closed") }, "tasks_by_status_size": counts, "escalations_open": len(escalations), @@ -610,5 +636,145 @@ def main(): ThreadingHTTPServer((_ADDR, _PORT), _Handler).serve_forever() +# ── живой статус каналов портала/TG (t-261004103718-d8112b) ──────────────── +# Каждый реальный пользователь канала (сек-рефлекс portal_channel.sec_reflex, +# TG-поллер tg_inbox.poll_loop) пишет штамп harness/channel_.json +# после УСПЕШНОГО обмена. Дашборд только читает штампы — сам портал не дёргает +# (GET-проба poll-эндпоинта дублировала бы сек-рефлекс на его айсберге), +# и getUpdates не пробует (конфликт long polling с процессом поллера). +_CHANNEL_MAX_AGE_MS = 10 * 60 * 1000 # штамп старше 10 мин = канал вниз + +_portal_probe_cache = {"ts": 0.0, "res": None} + + +def _portal_probe() -> dict: + """Fallback-проба портала (когда штампа рефлекса нет/протух). + + main-харнес новой версии пишет штамп при каждом сек-рефлексе; пока он не + перезапущен (рестарт откладывается до тихого окна — LLM-воркеры), дашборд + пробует портал сам: GET /channel/poll?since=now — read-only, consumed + ставится только отдельным POST (не делаем). Кэш 60с: клиент чата тикает + каждые 10с — дёргать портал чаще незачем. + """ + import urllib.parse + import urllib.request + + now = time.time() + if _portal_probe_cache["res"] and now - _portal_probe_cache["ts"] < 60: + return _portal_probe_cache["res"] + try: + from harness import portal_channel as _pch + cfg = _pch._cfg() + if not (cfg.get("portal_url") and cfg.get("agent_token") + and cfg.get("agent_name")): + res = {"ok": False, "age_s": None, "ts": 0, + "detail": "канал не сконфигурён (нет /etc/coop-reporter.env)"} + else: + url = (f"{cfg['portal_url']}/api/agent/" + f"{urllib.parse.quote(cfg['agent_name'])}" + f"/channel/poll?since={int(now * 1000)}") + req = urllib.request.Request( + url, headers={"X-Agent-Token": cfg["agent_token"]}) + with urllib.request.urlopen(req, timeout=5) as r: + json.loads(r.read().decode()) + res = {"ok": True, "age_s": 0, "ts": int(now * 1000), + "detail": "проба ok (штампа рефлекса ещё нет — старый main-харнес)"} + except Exception as e: + res = {"ok": False, "age_s": None, "ts": 0, + "detail": f"проба не прошла: {str(e)[:80]}"} + _portal_probe_cache.update(ts=now, res=res) + return res + +# Старый харнес на Гермесе (hermes-rita, 2.28.125.152 — docs/migration-hermes.md) +# не узнаётся по /etc/coop-reporter.env (документированные ключи: PORTAL_URL, +# AGENT_TOKEN). Без честного признака не рисуем "старый харнес" в UI. +_HERMES_MARKER = None + + +def _hermes_marker() -> str | None: + """Признак, что эти файлы запущены на старом Гермес-харнесе (для UI). + + Пока легитимного маркера нет — None (Simbiot-дашборд ни с чем не путаем). + """ + return _HERMES_MARKER + + +def channel_status() -> dict: + """Живой статус каналов чата: portal (poll портала) + telegram (inbox). + + Каждый канал: ok (свежий успешный обмен), age_s (возраст штампа), + ts (ms последнего успеха), detail (что именно успешно). + """ + now_ms = int(time.time() * 1000) + + def _read(name: str) -> dict: + path = os.path.join(_HERE, f"channel_{name}.json") + try: + with open(path) as f: + d = json.load(f) + ts = int(d.get("ts") or 0) + age = now_ms - ts + if age < 0 or age > _CHANNEL_MAX_AGE_MS: + raise FileNotFoundError # штамп протух → считаем как отсутствующий + return {"ok": bool(d.get("ok")), "age_s": round(age / 1000), + "ts": ts, "detail": str(d.get("detail") or "")[:120]} + except (FileNotFoundError, ValueError, TypeError): + # штампа нет (файл не создан / битый / старше 10 мин). + # portal: честная fallback-проба (штамп пишет main-харнес новой + # версии — до его рестарта проба даёт живую правду). + if name == "portal": + return _portal_probe() + return {"ok": False, "age_s": None, "ts": 0, + "detail": "нет свежего штампа поллера"} + except Exception as e: + return {"ok": False, "age_s": None, "ts": 0, + "detail": f"штамп не читается: {str(e)[:80]}"} + + return {"portal": _read("portal"), "telegram": _read("tg"), + "checked_at": now_ms} + + +def _tg_stats(days: float = 1.0) -> dict: + """TG-факты из стора: токен загружен? + входящие за последние 24ч.""" + from datetime import datetime, timedelta, timezone + + tg_token = None + try: + from harness import tg_inbox as _tg + _tg._load_env() + tg_token = bool(_tg.TG_TOKEN) + except Exception: + pass + n24, last_ts = 0, None + try: + db = es._conn() + cutoff = (datetime.now(timezone.utc) + - timedelta(hours=int(days * 24))).isoformat() + # portal-канальные MessageReceived пишет НЕ TG-поллер — исключаем: + # иначе живой портал-чат рисовал бы «зелёный» telegram (найдено в + # t-261004103718: portal ok, tg штампа нет — а события капают). + row = db.execute( + "SELECT count(*) AS n, max(timestamp) AS last FROM events " + "WHERE event_type='MessageReceived' AND timestamp >= ? " + "AND (data IS NULL OR data NOT LIKE '%\"chat_id\": \"portal%')", + (cutoff,)).fetchone() + db.close() + n24 = int(row["n"] or 0) + last_ts = row["last"] + except Exception: + pass + age_s = None + if last_ts: + try: + from datetime import datetime as _dt + age_s = max(0, round(time.time() + - _dt.fromisoformat(str(last_ts)).timestamp())) + except Exception: + age_s = None + return {"token_loaded": tg_token, "received_24h": n24, "last_msg_ts": last_ts, + "last_msg_age_s": age_s} + + if __name__ == "__main__": - main() \ No newline at end of file + main() + diff --git a/harness/scheduler.py b/harness/scheduler.py index 3acdc33..851bb3e 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -268,8 +268,16 @@ def build_context(level_cfg, model: dict, batch=None): digests.sort(key=lambda d: d["ts"], reverse=True) if digests: parts.append("ВЫЖИМКИ НИЖНИХ УРОВНЕЙ (последние, сжатые):") + # дедуп по тексту (эксп. #1, t-261005121525): тихий мин-уровень давал + # 6 идентичных [gate:quiet]-строк в каждом шаге промпта — чистый burn. + # Теперь повтор схлопывается в одну строку с счётчиком ×N. + seen: dict = {} for d in digests[:6]: - parts.append(f"- [{d['level']}] {d['text'][:400]}") + key = (d["level"], d["text"][:400]) + seen[key] = seen.get(key, 0) + 1 + for (lvl, text), n in seen.items(): + rep = f" (×{n} подряд)" if n > 1 else "" + parts.append(f"- [{lvl}] {text}{rep}") # эскалации: открытые, адресованные этому уровню и выше — в приоритете escs = [] diff --git a/harness/tg_commands.py b/harness/tg_commands.py index 9aaeb56..1753f7e 100644 --- a/harness/tg_commands.py +++ b/harness/tg_commands.py @@ -99,7 +99,7 @@ def cmd_status() -> str: st = cs.get(name) or {} err = f", ошибок {st['errors']}" if st.get("errors") else "" lines.append(f" {name}: тиков {st.get('ticks', 0)}{err}") - parts = [f"{k} {counts[k]}" for k in ("ready", "running", "done", "failed") if counts.get(k)] + parts = [f"{k} {counts[k]}" for k in ("ready", "running", "blocked", "failed", "done") if counts.get(k)] lines.append("📋 задачи: " + (" · ".join(parts) if parts else "нет")) lines.append(f"🚨 эскалаций открытых: {open_esc}") lines.append(f"💰 спенд (последние {len(spend)} тиков): ~{tokens:,} токенов".replace(",", " ")) diff --git a/out/9router-gemini-test.png b/out/9router-gemini-test.png new file mode 100644 index 0000000..3c2c2f8 --- /dev/null +++ b/out/9router-gemini-test.png @@ -0,0 +1 @@ +{"error":{"message":"[gemini/gemini-3.1-flash-image-preview] [429]: You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-3.1-flash-image\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-3.1-flash-image\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-3.1-flash-image\nPlease retry in 12h6m1.119176629s. (reset after 4s)"}} \ No newline at end of file diff --git a/out/seedance_simbiot_pitch.md b/out/seedance_simbiot_pitch.md new file mode 100644 index 0000000..431fff2 --- /dev/null +++ b/out/seedance_simbiot_pitch.md @@ -0,0 +1,54 @@ +# Simbiot — питч для видео (Seedance) + +## 1. Что говорить на камеру (~30 сек, живым языком) + +> Представь: ты заходишь в стартап — и через неделю у тебя работает не один ассистент, +> а целая команда ИИ-агентов: один отвечает клиентам, второй пишет код, третий следит +> за деньгами и метриками. +> +> Это Simbiot. Обычно фаундер собирает зоопарк из агентов и харнесов — а когда уходит +> разработчик, вся эта магия уходит вместе с ним. Simbiot — это общий харнес организации: +> слой поверх Hermes, Claude Code и других. Он координирует флот агентов, фиксирует +> решения компании и копит удачные приёмы как «гены» — и оставляет только те, что +> реально приносят прибыль. +> +> Мы уже гоняем его на семи собственных продуктах. Строй компанию — а не зоопарк ботов. + +Хронометраж: ~35 сек. Если нужно короче (15 сек) — оставить первый и последний абзац. + +## 2. Промт для Seedance (весь, целиком — скормить как есть) + +Seedance генерирует клипы по 5–10 сек, поэтому видео собирается из 3 шотов +( narrator говорит поверх — в кадре речи не нужно, генерим b-roll ). + +**Общий стиль (добавлять в конец каждого промта):** +> Dark cinematic tech aesthetic, deep navy-black background (#0a0c12) with violet +> and purple glow accents (#7c6cf5, #8b5cf6), soft volumetric light, shallow depth +> of field, film grain, high detail, 4k, smooth camera motion, no text, no captions, +> no people talking, ambient mood. 16:9. + +**Shot 1 (0–10 сек) — «зоопарк в хаосе»:** +> Slow dolly-in through a dark server room: dozens of small glowing robots and holographic +> screens flickering chaotically, tangled cables, scattered disconnected UI panels floating +> in the air, each glowing a different cold color, subtle digital glitches. + +**Shot 2 (10–20 сек) — «Simbiot orchestration»:** +> The chaos converges: tangled light streams flow into one glowing violet orb at the center +> of a dark control room, holographic dashboard panels organize around it in a perfect grid, +> elegant lines of data pulse from the orb to the panels like a neural network heartbeat, +> slow orbit camera movement. + +**Shot 3 (20–30 сек) — «финал / бренд»:** +> Wide cinematic shot of a modern dark startup office at night, a founder silhouette +> calmly watching a large wall display where agent dashboards grow revenue charts, +> violet light washes the room, camera slowly pulls back through a window into the +> city skyline at dusk. + +## 3. Быстрый вариант (один промт, если лень склеивать) + +> Cinematic 30-second tech brand film: dark navy-black world with violet glow (#7c6cf5). +> It opens on chaos — dozens of flickering holographic screens and tangled light streams. +> A luminous violet orb emerges and orchestrates everything: panels align into a grid, +> data flows like a heartbeat. Final wide shot: founder silhouette watching revenue +> charts grow on a wall of dashboards in a dark modern office. Slow dolly and orbit +> camera, volumetric light, film grain, no text, 16:9. diff --git a/scripts/alipay_watch.py b/scripts/alipay_watch.py new file mode 100644 index 0000000..5807e1b --- /dev/null +++ b/scripts/alipay_watch.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Ватчер Alipay на live-счёте Stripe (эскалация e-261005142309-390896). + +Контекст: Alex включил тумблер Alipay в дашборде (preference=on), но +capability ещё not available — счёт ждёт аппрува Stripe («говорит что нужен +ещё аппрувал, ждем», m-516173789). Включить через API нельзя (rk_live +restricted, available=false). + +Этот скрипт — детерминированный поллинг без LLM (systemd-таймер, 5 мин): +- пока alipay.available=false → тихо выходим (state=waiting); +- как только Stripe активирует capability → создаёт Checkout Session + $100 one_time (price_1UNCYGBT53G7FGSQya04h28r) с card+alipay и + идемпотентно (cli.py tg-notify -k) отправляет ссылку в чат; +- после успешной доставки пишет done-маркер и больше не работает. + +Ключ читается из /root/coop/.env (STRIPE_LIVE_KEY, реестр KEYS-REGISTRY). +Состояние: /var/lib/simbiot-harness/alipay-watch.state. +Выход: 0 = ждём/отработал, 3 = ошибка конфигурации (для журнала). +""" +import json +import os +import subprocess +import sys +import urllib.error +import urllib.parse +import urllib.request + +STATE = "/var/lib/simbiot-harness/alipay-watch.state" +ENV_FILE = "/root/coop/.env" +PRICE = "price_1UNCYGBT53G7FGSQya04h28r" # $100 USD one_time, live +NOTIFY_KEY = "alipay-live-100-link" # идемпотентность доставки (t-260930083937) +HARNESS = "/root/simbiot-harness" +VENV_PY = f"{HARNESS}/venv/bin/python" + + +def _stripe_key() -> str | None: + try: + with open(ENV_FILE) as f: + for line in f: + line = line.strip() + if line.startswith("STRIPE_LIVE_KEY="): + return line.split("=", 1)[1].strip().strip('"').strip("'") + except OSError: + pass + return os.environ.get("STRIPE_LIVE_KEY") + + +def _call(path: str, method: str = "GET", data: dict | None = None) -> dict: + key = _stripe_key() + if not key: + return {"_error": "no STRIPE_LIVE_KEY"} + body = urllib.parse.urlencode(data).encode() if data is not None else None + req = urllib.request.Request( + "https://api.stripe.com/v1/" + path, + headers={"Authorization": f"Bearer {key}"}, + method=method, + data=body, + ) + try: + with urllib.request.urlopen(req, timeout=25) as r: + return json.loads(r.read()) + except urllib.error.HTTPError as e: + return {"_error": e.code, "body": e.read().decode()[:400]} + + +def _read_state() -> str: + try: + with open(STATE) as f: + return f.read().strip() + except OSError: + return "" + + +def _write_state(s: str) -> None: + os.makedirs(os.path.dirname(STATE), exist_ok=True) + with open(STATE, "w") as f: + f.write(s) + + +def main() -> int: + if _read_state() == "done": + return 0 # ссылка создана и доставлена — ватчер отработал + + cfgs = _call("payment_method_configurations?limit=100") + for c in cfgs.get("data", []): + alipay = c.get("alipay") or {} + if alipay.get("available"): + # capability активна — создаём линк card+alipay + sess = _call("checkout/sessions", "POST", { + "mode": "payment", + "line_items[0][price]": PRICE, + "line_items[0][quantity]": "1", + "success_url": "https://cil.by/thanks", + "cancel_url": "https://cil.by/", + "payment_method_types[0]": "card", + "payment_method_types[1]": "alipay", + }) + url = sess.get("url") + if not url: + # доступна, но сессия не собирается — оставить ждущим, + # причина в журнале таймера; следующий прогон повторит + _write_state("available-but-failed") + print("alipay available, session failed:", + sess.get("_error"), sess.get("body", "")[:200]) + return 0 + text = (f"Alipay активирован на live-счёте Stripe. " + f"Боевая ссылка $100 (card + Alipay): {url}") + r = subprocess.run( + [VENV_PY, f"{HARNESS}/cli.py", "tg-notify", text, + "-k", NOTIFY_KEY], + capture_output=True, text=True, timeout=60, + ) + out = (r.stdout or "").strip() + print("notify:", out) + if out.startswith("OK") or "SKIP" in out: + _write_state("done") + return 0 + _write_state("notify-failed") + return 0 + print(f"waiting: alipay preference={alipay.get('display_preference', {}).get('value')} " + f"available={alipay.get('available')} (cfg {c.get('id')})") + _write_state("waiting") + return 0 + print("no live payment_method_configuration found:", cfgs.get("_error", "?")) + return 3 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/nightly-selfimprove.sh b/scripts/nightly-selfimprove.sh new file mode 100755 index 0000000..fb5a716 --- /dev/null +++ b/scripts/nightly-selfimprove.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Ночная самодоработка харнеса (директива Alex m-516173718, эскалация e-261005074512): +# подаёт задачу [M] в очередь харнеса — ближайший hour-тик подбирает и исполняет. +# Журнал итераций: artifacts/self-improvement-log.md +cd /root/simbiot-harness || exit 1 +TITLE="Ночная самодоработка харнеса: ретро + эксперимент" +# идемпотентность: не дублируем, если задача уже ждёт/выполняется +if ./venv/bin/python cli.py tasks 2>/dev/null | grep -F "$TITLE" | grep -qE ' (ready|running)'; then + echo "$(date -u +%FT%TZ) skip: задача уже в очереди" >> /var/log/simbiot-selfimprove.log + exit 0 +fi +./venv/bin/python cli.py task "$TITLE" \ + -d "Директива Alex m-516173718 (e-261005074512). 1) Ретро суток: cli.py events, cli.py status, cli.py aum — что сработало/что сломалось (упавшие тики, эскалации, закрытые задачи, burn). 2) Один эксперимент над собой: гипотеза вида «метрика X -> изменение -> ожидание»; лёгкий провести сейчас в пределах контекста, тяжёлый оформить задачей на день. 3) Журнал: artifacts/self-improvement-log.md — дописать строку по формату (#N | дата | гипотеза | метрика до->после | вывод). 4) Ночь — тишина в чате (кроме критичного), результат уходит в ответ задачи, его поднимет утренний дайджест. Фильтр AUM: эксперимент служит +1% к AUM в год+." \ + -m 30 diff --git a/~/.bubblewrap/config.json b/~/.bubblewrap/config.json new file mode 100644 index 0000000..9808941 --- /dev/null +++ b/~/.bubblewrap/config.json @@ -0,0 +1,4 @@ +{ + "jdkPath": "/usr/lib/jvm/java-17-openjdk-amd64", + "androidSdkPath": "/opt/android-sdk" +} diff --git a/~/.config/opencode/opencode.json b/~/.config/opencode/opencode.json new file mode 100644 index 0000000..eb893ca --- /dev/null +++ b/~/.config/opencode/opencode.json @@ -0,0 +1,8 @@ +{ + "permission": { + "edit": "allow", + "bash": "allow", + "webfetch": "allow" + }, + "share": "disabled" +}