Compare commits

...

4 commits

6 changed files with 563 additions and 1 deletions

View file

@ -0,0 +1,15 @@
{
"weeks": [
{
"week_start": "2026-10-19T01:06:57.981175+00:00",
"start_day": "2026-10-19",
"burn_usd": 7.7078,
"reqs": 243,
"days_seen": 2,
"src": "недоступен",
"active": true,
"updated_at": "2026-10-19T01:08:13.330295+00:00"
}
],
"method": "BudgetSpent стора × блендед-ставка 9.43e-07 у.е./токен; окно 7д, накопительно в рамках недели (t-261019001211)"
}

View file

@ -0,0 +1,136 @@
# CloudCLI-кабинеты $99/мес: онбординг-ретеншн первой недели
> t-261019001255-fc7eeb [M] · часовой поток · 2026-10-19
> Продукт: Simbiot UI (доступ к развёрнутому cloudcli/claudecodeui из браузера),
> $99.9/мес claude seat included, ключи наши, подключение — вручную через
> Alex (решение A7, out/simbiot_ui_product_plan.md v0.2).
> Подписка = долгосрочный AUM-поток; ретеншн первой недели = удержание
> всего LTV. Цель: 3 живых клиента довести до «первого полезного тика».
---
## 0. Кто сейчас в воронке (честный срез, 2026-10-19)
| # | Кто | Статус | Источник |
|---|---|---|---|
| 1 | presale-платник #1 | **paid** ($99.90, Stripe live) | cil.by/presale, seats API paid=1 |
| 2–3 | забронировавшие места | taken=2/100, оплата не подтверждена | presale-вейтист |
| — | очередь ручного онбординга | заявок в админке: 0 | ui.cil.by пока без трафика |
Вывод: **живых подключённых кабинетов пока 0, платник 1, тёплых брони 2** —
это и есть «3 живых клиента» для онбординга. Чек-лист ниже рассчитан на
ручной флоу A7 (пропускная способность 3–5/нед).
---
## 1. Чек-лист онбординга — первая неделя клиента
### День 0 — подключение (Alex + оркестратор)
- [ ] Подтвердить заявку в админке (статус: заявка → подтверждена)
- [ ] Поднять изолированный контейнер клиента (vCPU/RAM-лимиты, свой том)
- [ ] Создать мэйл агента `agent.<client>@cil.by` (Postfix+Dovecot)
- [ ] Авторизовать claude seat, выдать agt_-токен через шлюз (наш, не клиентский)
- [ ] Креды → vault, запись в KEYS-REGISTRY (без значений)
- [ ] Письмо клиенту: логин, URL кабинета, что дальше (§CLAUDE.md-онбординг)
- [ ] Статус в админке: подключается → **активна**
- [ ] spend-лимит клиента в 9router-шлюзе выставлен ($99.9 включено, алерт на 80%)
### День 1 — «первый вход» (цель: клиент зашёл и что-то запустил)
- [ ] Клиент залогинился в UI (маркер: первый запрос через шлюз в логах spend)
- [ ] Первый чат с Claude Code в кабинете (любой вопрос)
- [ ] Клиенту отправлен короткий «first 5 minutes» (3 скриншота: вход → чат → файл)
- [ ] 📊 метрика: **activation-вход** = зашёл в первые 24ч — да/нет
### День 2–3 — «первый агент» (цель: не просто чат, а работа)
- [ ] Вместе с клиентом (звонок 15 мин или асинхронно): первая реальная задача
клиента оформлена как задача/скрипт в кабинете
- [ ] Подключён общий студийный RAG (read-only MCP) — «агент знает методологию»
- [ ] Клиент увидел дифф/результат первого запуска в UI
- [ ] 📊 метрика: **first-agent** = запустил реальную задачу — да/нет
### День 4–7 — «первый полезный тик» (цель: ценность без нас)
- [ ] Задача клиента поставлена на повтор (cron/повтор в кабинете) — агент
работает сам, без запроса клиента
- [ ] Клиент получил первый автономный результат (письмо/дайджест в кабинет)
- [ ] Чек-ин звонок/сообщение: что мешало? (лог → §3)
- [ ] 📊 метрика: **first-useful-tick** = автономный тик принёс результат,
который клиент использовал — да/нет
- [ ] Если все три 📊 = да → статус **retained-week-1**, предложение годового плана
**Анти-паттерн недели:** клиент пользуется кабинетом как «чатом с Claude» —
это отвал на 2-й месяц (ценность $20, цена $99.9). Чек-лист дня 4–7
специально толкает к автономности — там наша цена обоснована.
---
## 2. Письма-чекины (drafts, отправка — Alex)
### 2.1. День 1 — всем новым (первый вход)
> **Тема:** твой CloudCLI-кабинет жив — 3 шага на 5 минут
>
> Привет! Кабинет готов: <URL>, логин <email>, пароль — в предыдущем письме.
>
> 5 минут, чтобы поймать ритм:
> 1. Зайди и задай любой вопрос в чате — это уже Claude Code в браузере.
> 2. Пришли мне (просто ответом) одну задачу, которую делаешь руками каждую
> неделю — поставим её агенту на повтор.
> 3. Загляни в раздел задач: там агент будет копить твою историю.
>
> Если что-то не зашлось на первом шаге — скажи сразу, поправим за час.
> — Alex, Simbiot
### 2.2. День 3 — если НЕ дошёл до «первого агента» (застрял)
> **Тема:** застряли? 15 минут — и агент работает сам
>
> Вижу, кабинет активен, но до первой задачи пока не дошло. Обычно это
> значит: непонятно, с чего начать. Давай так: 15-минутный звонок — покажу
> на твоём примере, как задача ставится на повтор (клинок: слоты на cil.by
> или просто ответь «давай»).
>
> Или асинхронно: пришли 1–2 предложения «что бы делегировал» — соберу
> агента сам и покажу результат.
### 2.3. День 6 — если дошёл (закрепление + годовой план)
> **Тема:** агент уже работает без тебя — следующий шаг
>
> За эту неделю агент сделал <N> автономных запусков, из них <M> ты
> использовал. Это и есть режим $99.9: работа идёт, пока ты занимаешься
> своим.
>
> Два шага дальше: 1) добавим вторую регулярную задачу (пришли список —
> расставим приоритеты); 2) годовой план — 10 месяцев по цене 11, фиксация
> цены навсегда. Хочешь — включу.
---
## 3. Метрика недели: кто дошёл и что мешает остальным
| Клиент | activation-вход | first-agent | first-useful-tick | Блокер |
|---|---|---|---|---|
| presale paid #1 | — замер | — | — | — |
| бронь места #2 | не подключён (ждёт оплату/очередь) | — | — | активация брони |
| бронь места #3 | не подключён | — | — | активация брони |
**Гипотезы блокеров (проверить чек-ином дня 4–7):**
1. «Не понимаю, что делегировать» → решается 2.2-письмом + примерами задач.
2. «Кабинет — это просто чат» → чек-лист дня 4–7, повтор-задачи; мера: доля
клиентов с ≥1 cron-задачей к дню 7 (цель 100% retained).
3. Неактивированные брони → письмо-напоминание о месте (лимит 100 —
срочность честная), драфт добавляется после первых 2 подключений.
**Ретеншн-цель недели:** 3/3 до first-useful-tick. Если 2/3 — норма рынка
(67%); если ≤1/3 — стоп продажам, сначала починить онбординг (фильтр AUM:
подписка без ретеншна — отрицательный поток после первого месяца).
---
## 4. Следующие шаги (кто и когда)
- Alex: подключить платника #1 по чек-листу дня 0 (всё ручное, A7); два
письма 2.1 отправить в день подключения.
- Харнес (часовой): после первого подключения — замерить 📊-метрики из
spend-логов шлюза и заполнить §3 (это станет S-задачей).
- Повтор задачи-замера: +1 неделя, метрика ретеншн-30д после 3 клиентов.

View file

@ -0,0 +1,146 @@
# Пресейл-пуш «Агент-на-таймере» → первые разговоры
> t-261019001218-ad76fd [M] · часовой поток · 2026-10-19
> Основа: MVP-упаковка (t-261018193640-c4369f) + живой дашборд
> **https://agent-dashboard.cil.by** — главный аргумент: «агент, который
> показывает свою работу сам». Пресейл-харнес 5k = пилотный контракт
> развёртывания (+90% к текущей AUM — прямое попадание в цель).
> Отправка писем — ТОЛЬКО через Alex (директива: публичные репрезентации
> кооператива и исходящий контакт — его решение). Здесь — драфты.
---
## 1. Оффер «Агент-на-таймере» — пилот 5k
**Одно предложение:** дежурный ИИ-ассистент команды, который работает по
расписанию сам — не ждёт промпта: следит за задачами, копит память, пишет
в ваш чат, эскалирует важное. Всё, что он делает, видно вживую на его
собственном дашборде.
**Что входит в пилот за $5,000 (4 недели):**
| Неделя | Что получаете |
|---|---|
| 1 | Развёртывание харнеса на вашей инфраструктуре (или нашей), подключение чата (Telegram/портал), первый агент на минутном пульсе |
| 2 | Перенос ваших текущих задач/проектов в доску агента, память команды (RAG по вашим докам) |
| 3 | Настройка лестницы ритмов под ваши процессы: сек-пульс → минута → час → день; правила эскалаций |
| 4 | Публичный дашборд вашей команды (как наш agent-dashboard.cil.by), дайджесты, передача документации |
**Гарантия:** если к концу недели 2 агент не сделал ни одного полезного
тика без вашего запроса — возврат 50%.
**Доказательства (живые, не презентация):**
- https://agent-dashboard.cil.by — наш собственный агент в проде: такты,
задачи, эскалации, AUM-спарклайн. Обновляется сам, cron.
- Харнес ведёт 7+ продуктов кооператива (42/studio): портал cil.by,
биллинг со Stripe, MCP-инструменты, этот дашборд.
- Крипто-аудит каждого действия (ed25519-подписи, тампер-цепочка) — safe-by-design.
**Ценообразование после пилота:** подписка $100/мес/место (presale-директива
$100/mo, первые 100 мест; cil.by/presale) — пилот 5k конвертируется
в годовую подписку со скидкой, равной 2 месяцам (не ломает директиву:
скидка на конверсию пилота, не на полку).
**Куда ведём:** оффер-страница = сам дашборд + кнопка «Book a call»
(booking-портал cil.by уже умеет слать письма гостю). Отдельный лендинг
не нужен до первых 2 разговоров — дашборд сильнее лендинга.
---
## 2. Десять лидов (тёплый контур, из базы кооператива и звонков)
| # | Лид | Контекст | Почему сейчас | Приоритет |
|---|---|---|---|---|
| 1 | **Vlad** (совладелец) | ReelsUp, xSaleBot, Avatar — 3 продукта с ботами; сам платит за LLM-инфраструктуру | Его боты без ритма: наш харнес даст им тики+память; dogfooding внутри коопа → кейс наружу | A |
| 2 | **Yura** (совладелец) | Vybirai, Монетикс (финмаркетплейс) | 14 лет продуктов; агент на таймере = дежурный аналитик данных банков | A |
| 3 | **Дмитрий Сясицкий** (Siasitski, AI Rebuild Consulting, США) | Продаёт AI-внедрения $1k–$20k малому бизнесу, первые платящие клиенты | НЕ клиент — **канал**: его клиентам нужен продукт, который «живёт и виден»; партнёрство = его аудитория + наш движок | A+ |
| 4 | **Юлия** (встреча #136, 03.10) | Детский центр → онлайн; интерактивные AI-видео-уроки; подрядчики просят 10k€/урок | У неё нет ИИ-инфраструктуры и есть жёсткий бюджет; дежурный агент на контент-конвейер дешевле одного урока подрядчика | B+ |
| 5 | **Надежда** (звонок 02.10) | Приложение для садоводов; сама целевая пользователь; муж-айтишник; опыт Google Play | Мала команда = идеальный профиль «дежурного ассистента»: агент следит за релизами/отзывами/метриками, пока они делают продукт | B |
| 6 | **Стас Дащинский** (кандидат, backend) | Python/Flask/event-sourcing, строил портал 42 | Технически оценит лестницу и эскалации; кандидат в лабу → пилот на своих проектах | B |
| 7 | **Владимир Бугаев** (boogvar, TessariDB) | Экзо-Neon Labs с Alex; мультимодельная БД (11 движков) | Мультидвижковая БД + агент на таймере = семинар/совместный демо-кейс; не платник, но усилитель охвата | C+ |
| 8 | **Neon Labs alumni** (круг Alex) | Бывшие коллеги CTO-уровня | Тёплый intro от Alex; профиль «малая команда, много продуктов» совпадает с нашим | B+ |
| 9 | **Клиенты Сясицкого** (2-я рука) | Малый бизнес США, уже платят за AI-аудиты $1k | Уже поверили в AI-ценность; следующий шаг — «агент, который работает сам» | через №3 |
| 10 | **Аудитория presale.cil.by** (1 paid, 2 taken) | Уже купили место $99.90 | Апсейл-путь: подписчик → пилот 5k (полная развёртка на их инфра) | A |
---
## 3. Три персонализированных письма (drafts, отправка — Alex)
### 3.1. Vlad — «твоим трём ботам не хватает тиков» (RU, внутренний, короткий)
> **Тема:** твоим ботам не хватает одного дня работы
>
> Vlad, привет. Посмотри 2 минуты на https://agent-dashboard.cil.by — это
> наш харнес вживую: агент сам тикает по расписанию, сам берёт задачи, сам
> эскалирует. Не презентация — прод.
>
> У тебя ReelsUp, xSaleBot и Avatar — три бота, которые реагируют, а не
> работают по расписанию. Я хочу поставить им харнес на 4 недели: минутный
> пульс на xSaleBot (радар выпусков — тик = проверка + аутрич-драфт),
> часовой на ReelsUp (очередь нарезок), дневной на Avatar (продакшн-план).
> Стоимость пилота 5k, из кооперативной казны, результат — твой же кейс
> для presale-страницы (нам нужны 2 честные цитаты).
>
> Дашборд твоих ботов будет такой же, как наш — покажем на общем звонке.
> Ставим?
### 3.2. Дмитрий Сясицкий — партнёрство (RU, он в США, продаёт AI-внедрения)
> **Тема:** your clients need the agent that shows its own work
>
> Дмитрий, привет! Ты продаёшь малому бизнесу AI-внедрения от аудита $1k до
> кастома $10–20k. Главный стоп для твоих клиентов — «а как я увижу, что
> агент реально работает, а не жжёт мою подписку?»
>
> Мы решили это радикально: наш агент публикует собственный дашборд в
> реальном времени — https://agent-dashboard.cil.by. Такты, задачи,
> эскалации, даже метрика расходов (AUM-спарклайн). Каждый шаг подписан
> криптографически. Это не демо — это наш прод, он же продающий аргумент.
>
> Предложение: для твоих клиентов, которым нужен не кастом за $20k, а
> работающий дежурный ассистент за неделю — наш пилот $5k (4 недели до
> живого агента на их инфраструктуре) + подписка $100/мес. Твоя марка на
> дашборде клиента, твоя комиссия с каждого пилота. Первые 2 пилота —
> со-демо на звонке с тобой.
>
> 15 минут на этой неделе, покажу изнанку (лестница ритмов, память,
> эскалации)? Выбери слот: cil.by (booking) — или просто ответь «да».
### 3.3. Юлия — «твой конвейер уроков дешевле, чем один урок подрядчика» (RU)
> **Тема:** конвейер твоих AI-уроков — за цену недели подрядчика
>
> Юлия, привет! После нашего разговора 3.10 я всё думал про твои цифры:
> подрядчики просят 10 000 € за ОДИН урок, платформа целиком 150–200k €.
> При этом тебе нужен не разовый продукт, а конвейер: сценарий → видео →
> интерактив → модерация → выпуск. Это ритмичная работа, а не проект.
>
> У нас в студии такой конвейер крутит ИИ-агент на таймере: он сам берёт
> задачи по расписанию, копит память (твои требования к урокам остаются
> в его памяти, не в голове подрядчика), пишет в чат черновики и
> эскалирует тебе только решения. Как это выглядит вживую:
> https://agent-dashboard.cil.by — это наш собственный агент, обновляется сам.
>
> Пилот: 4 недели, $5k — агент на цепочке «сценарий → черновик урока →
> твой ревью», с защитой видео от записи и без соц-механик (твои жёсткие
> требования — первая строка ТЗ, у нас крипто-подписи на каждом шаге).
> Это меньше, чем 10% одного урока подрядчика. Если к концу второй недели
> он не выдаст ни одного готового к ревью черновика сам — вернём половину.
>
> 15 минут показать, как именно он будет работать на твоих темах
> (психология, питание, самопрезентация)?
---
## 4. Метрика успеха и следующие шаги
- **Цель: ≥2 ответов** из 3 писем (плюс Vlad — внутренний, почти гарантирован).
- Замер: ответы фиксируются в inbox харнеса (portal), каждый разговор → задача
на демо-звонок (booking-портал уже умеет jitsi + письмо гостю).
- После первого разговора: собрать оффер-страницу «пилот 5k» как страницу
дашборда с CTA (не раньше — дашборд сильнее).
- Апсейл-письмо существующим presale-подписчикам (paid=1, taken=2) — драфт
после первых 2 разговоров, чтобы сослаться на живые кейсы.
*Отправка всех трёх — только после «ок» Alex (публичные/исходящие
коммуникации кооператива — его решение, SALES-PLAN-1000-US тир-0).*

View file

@ -238,6 +238,128 @@ def read_coop_structure():
return {"title": None, "projects": []} return {"title": None, "projects": []}
# ───────────── живая петля burn-7д: router_usage.json (t-261019001211) ─────────────
# Слепота AUM-метрики: burn живёт только в тексте policy и теряет историю
# при недельном скольжении. Файл docs/aum/router_usage.json — компактный реестр
# «неделя → замер» с ежегодным сбросом окна: каждая запись — сумма расхода
# с момента прошлого замера (окно кратно ~7д, ровно одна активная неделя).
# Идёмпотентность: замер того же дня не создаёт новую запись, а дополняет
# активное окно (докупка ΔB = sum_7d − уже_записанное). Так скользящее окно
# BudgetSpent-стора превращается в стабильный накопительный реестр: виден
# и фактический burn недели, и Δ-тренд неделя-к-неделе.
ROUTER_USAGE_JSON = os.environ.get(
"SIMBIOT_ROUTER_USAGE_JSON",
os.path.join(os.path.dirname(BALANCES_JSON), "router_usage.json"))
def update_router_usage(snap: dict = None, path: str = None):
"""Дополнить реестр router_usage.json свежим замером burn (7д).
Вход — снапшот aum.measure() (или собирается сам). Логика:
- если активной записи нет или её окно старше 21д — новая запись;
- если окно той же недели — досчитать Δburn от уже записанного
(скользящее окно BudgetSpent суммирует 7д заново, реестр — накопительно);
- окно ровно одно активное; история капится 260 записями (5 лет).
Ошибки не бросает (best effort, как и весь AUM-контур) — возвращает dict.
"""
path = path or os.environ.get("SIMBIOT_ROUTER_USAGE_JSON", ROUTER_USAGE_JSON)
snap = snap or measure()
days = snap.get("spend_days") or {}
sum7 = round(sum(v.get("cost", 0) for v in days.values()), 4)
today = snap.get("ts", "")[:10]
try:
from datetime import date
try:
today_d = date.fromisoformat(today)
except ValueError:
today_d = date.today()
try:
with open(path, encoding="utf-8") as f:
reg = json.load(f)
except Exception:
reg = {"weeks": []}
weeks = reg.setdefault("weeks", [])
cur = weeks[-1] if weeks else None
stale = True
if cur and cur.get("active") and cur.get("start_day"):
try:
stale = (today_d - date.fromisoformat(cur["start_day"])).days > 21
except ValueError:
stale = True
fresh = False
if (not cur or not cur.get("active") or stale):
# закрываем прошлое окно и заводим новое (сброс недели)
if cur and cur.get("active"):
cur["active"] = False
cur["closed_at"] = snap.get("ts")
cur = {"week_start": snap.get("ts"), "start_day": today,
"burn_usd": 0.0, "reqs": 0, "days_seen": 0,
"src": snap.get("burn_src"), "active": True}
weeks.append(cur)
fresh = True
# накопительная досумма поверх уже записанного в этом окне
prev = float(cur.get("burn_usd") or 0)
delta = round(sum7 - prev, 4)
if delta < 0: # скользящее окно выпало из старых дней — не отнимаем
delta = 0.0
cur["burn_usd"] = round(prev + delta, 4)
cur["reqs"] = max(int(cur.get("reqs") or 0),
sum(v.get("reqs", 0) for v in days.values()))
cur["days_seen"] = max(int(cur.get("days_seen") or 0), len(days))
cur["src"] = snap.get("burn_src") or cur.get("src")
cur["updated_at"] = snap.get("ts")
if len(weeks) > 260:
reg["weeks"] = weeks[-260:]
reg["method"] = ("BudgetSpent стора × блендед-ставка "
f"{BURN_RATE_PER_TOKEN:.2e} у.е./токен; окно 7д, "
"накопительно в рамках недели (t-261019001211)")
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(reg, f, ensure_ascii=False, indent=2)
f.write("\n")
os.replace(tmp, path) # атомарно: читатели не увидят полуфайл
return {"path": path, "week_start": cur.get("week_start"),
"burn_usd": cur["burn_usd"], "delta_usd": delta,
"fresh_week": fresh}
except Exception as e:
return {"path": path, "error": str(e)[:200]}
# ────────── env-доктор авто-источников AUM (t-261019001211, пункт 2) ──────────
# balances.json сейчас слеп: OPENROUTER_KEY и STRIPE_LIVE_KEY живут на другом
# хосте (KE-SREGISTRY). Чтобы запрос к Alex не был «сделай что-то», доктор
# собирает точный список: каких переменных в каком .env не хватает и что
# именно каждая открывает в метрике.
ENV_KEYS_NEEDED = (
# (переменная, файл, что открывает в AUM-петле)
("OPENROUTER_KEY", "simbiot-harness/.env",
"кредиты OpenRouter (auto-баланс №1, сейчас 26.7 фикс с 2026-10-04)"),
("STRIPE_LIVE_KEY", "/root/coop/.env",
"Stripe live available (auto-баланс, подписки $99/мес)"),
("SIMBIOT_ROUTER_DB", "simbiot-harness/.env",
"живая БД 9router (usageHistory): точный burn вместо блендед-оценки"),
)
def env_doctor(env_path: str = None, coop_env: str = "/root/coop/.env"):
"""Какие ключи отсутствуют + готовый шаблон запроса к Alex (без значений)."""
env_path = env_path or _paths.env_path()
missing = []
for var, where, why in ENV_KEYS_NEEDED:
path = coop_env if var == "STRIPE_LIVE_KEY" else env_path
present = _env_file_value(path, var) is not None
if not present:
missing.append({"var": var, "env_file": path, "opens": why})
request = ("Чтобы петля AUM видела деньги без ручного ввода, добавь в .env:\n"
+ "\n".join(f" {m['var']}=… # в {m['env_file']} — {m['opens']}"
for m in missing)) if missing else \
"Все ключи авто-источников на месте — петля AUM не слепа."
return {"missing": missing, "request_to_alex": request,
"checked_at": datetime.now(timezone.utc).isoformat(timespec="seconds")}
# ────────────────────────── замер ────────────────────────── # ────────────────────────── замер ──────────────────────────
def measure(): def measure():
@ -258,6 +380,15 @@ def measure():
if not spend: # БД роутера недоступна → не молчим, а считаем из стора if not spend: # БД роутера недоступна → не молчим, а считаем из стора
spend = read_store_spend(days=7) spend = read_store_spend(days=7)
spend_src = "store-BudgetSpent-оценка" if spend else "недоступен" spend_src = "store-BudgetSpent-оценка" if spend else "недоступен"
# живая петля burn-7д (t-261019001211): каждый замер дополняет реестр
# router_usage.json (накопительное окно недели) — контур не теряет историю
if os.environ.get("SIMBIOT_AUM_USAGE_REGISTRY", "1") not in ("0", "false", "no"):
_snap = {"ts": datetime.now(timezone.utc).isoformat(),
"spend_days": spend, "burn_src": spend_src}
try:
update_router_usage(_snap)
except Exception:
pass # реестр — best effort: замер важнее и идёт всегда
days_iter = list(spend.values()) days_iter = list(spend.values())
return { return {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
@ -368,6 +499,25 @@ def run_pilot(m: dict) -> dict:
"note": "вход — суточный burn 9router; выход — рекомендации у.е. расхода"} "note": "вход — суточный burn 9router; выход — рекомендации у.е. расхода"}
def weekly_burn_delta(path: str = None):
"""Δ burn последняя-прошлая неделя из router_usage.json (у.е. или None).
Пункт 4 петли (t-261019001211): манифесту дня нужен не только абсолют
burn, но и тренд — растём или падаем относительно прошлой недели.
"""
path = path or os.environ.get("SIMBIOT_ROUTER_USAGE_JSON", ROUTER_USAGE_JSON)
try:
with open(path, encoding="utf-8") as f:
weeks = json.load(f).get("weeks", [])
if len(weeks) < 2:
return None
cur, prev = weeks[-1], weeks[-2]
return round(float(cur.get("burn_usd") or 0)
- float(prev.get("burn_usd") or 0), 2)
except Exception:
return None
def build_policy_aum_summary(m: dict, pilot: dict = None) -> str: def build_policy_aum_summary(m: dict, pilot: dict = None) -> str:
"""Однострочник для policy/контекста всех уровней.""" """Однострочник для policy/контекста всех уровней."""
aum = m.get("aum") aum = m.get("aum")
@ -376,13 +526,16 @@ def build_policy_aum_summary(m: dict, pilot: dict = None) -> str:
"docs/aum/balances.json)") "docs/aum/balances.json)")
ev = (pilot or {}).get("evolution") or {} ev = (pilot or {}).get("evolution") or {}
tail = f"; последний отбор: {ev.get('verdict', 'не запускался')}" if pilot else "" tail = f"; последний отбор: {ev.get('verdict', 'не запускался')}" if pilot else ""
wd = weekly_burn_delta()
trend = (f"; Δburn к прошлой неделе: {'+' if wd >= 0 else ''}{wd} у.е."
if wd is not None else "")
src = m.get("burn_src") or "9router-db" src = m.get("burn_src") or "9router-db"
burn = (f"≈{m.get('burn_7d', 0)} (оценка: BudgetSpent стора × блендед-ставка; " burn = (f"≈{m.get('burn_7d', 0)} (оценка: BudgetSpent стора × блендед-ставка; "
f"живая БД 9router недоступна)" if src.startswith("store") f"живая БД 9router недоступна)" if src.startswith("store")
else f"{m.get('burn_7d', 0)}") else f"{m.get('burn_7d', 0)}")
return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов " return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов "
f"на длинной дистанции). {part}; burn 9router 7д=" f"на длинной дистанции). {part}; burn 9router 7д="
f"{burn} у.е. (прямой минус к AUM). Фильтр любого " f"{burn} у.е.{trend} (прямой минус к AUM). Фильтр любого "
f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; " f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; "
f"короткий доход, съедающий долгосрочную базу — ниже приоритетом." f"короткий доход, съедающий долгосрочную базу — ниже приоритетом."
f"{tail}") f"{tail}")

View file

@ -1256,6 +1256,26 @@ def process_level(level_cfg, model: dict, seq: int = None):
except Exception: except Exception:
pass # замер — best effort, не валить тик pass # замер — best effort, не валить тик
# живая петля AUM-метрики (t-261019001211): дневной снапшот — раз в сутки
# (см. выше), часовой дописывает внутрисуточные точки в aum_history —
# спарклайн дашборда оживает, а не обновляется раз в день. Идемпотентно
# по aggregate_id=aum:hour:<UTC-час>: повторный тик часа не задублирует.
if level_cfg.name == "hour":
try:
from . import aum as _aum
snap = _aum.measure()
hour_key = snap["ts"][:13] # 2026-10-19T01
ev = es.append_event("harness", f"hour:{hour_key}", "AumMeasured", {
"ts": snap["ts"], "aum": snap["aum"],
"burn_24h": snap["burn_24h"], "burn_7d": snap["burn_7d"],
"policy": {"aum_target": _aum.build_policy_aum_summary(snap)},
})
if ev:
_log(f"aum: часовая точка {hour_key} записана "
f"(burn_24h={snap['burn_24h']})")
except Exception:
pass # замер — best effort, не валить тик
# ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается # ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается
# только если есть событие ≥ порога уровня; тихий такт отрабатывает # только если есть событие ≥ порога уровня; тихий такт отрабатывает
# детерминированной механикой (как sec) — без расхода кредитов роутера. # детерминированной механикой (как sec) — без расхода кредитов роутера.

View file

@ -206,6 +206,98 @@ finally:
aum._http_json = _saved_http aum._http_json = _saved_http
check("stripe: livemode=False → None (не актив)", v_test is None) check("stripe: livemode=False → None (не актив)", v_test is None)
# ── 10. живая петля burn-7д: router_usage.json (t-261019001211) ──
RU = os.path.join(tmpdir, "router_usage.json")
for p in (RU,):
if os.path.exists(p):
os.unlink(p)
# 10a: первый замер → новая активная неделя
r1 = aum.update_router_usage({"ts": "2026-10-12T00:00:00+00:00",
"burn_src": "store-BudgetSpent-оценка",
"spend_days": {"2026-10-11": {"cost": 2.0, "reqs": 10},
"2026-10-12": {"cost": 1.0, "reqs": 5}}}, path=RU)
w = json.load(open(RU))["weeks"]
check("router_usage: первая неделя создана",
r1.get("fresh_week") and len(w) == 1 and w[0]["active"]
and abs(w[0]["burn_usd"] - 3.0) < 0.001, str(r1))
# 10b: тот же день, окно подросло → накопительная досумма (не дубль полной суммы)
r2 = aum.update_router_usage({"ts": "2026-10-12T12:00:00+00:00",
"burn_src": "store-BudgetSpent-оценка",
"spend_days": {"2026-10-11": {"cost": 2.0, "reqs": 10},
"2026-10-12": {"cost": 2.5, "reqs": 12}}}, path=RU)
w = json.load(open(RU))["weeks"]
check("router_usage: досумма Δ в той же неделе",
not r2.get("fresh_week") and abs(r2.get("delta_usd", -1) - 1.5) < 0.001
and abs(w[0]["burn_usd"] - 4.5) < 0.001, f"r2={r2}")
# 10c: скользящее окно сжалось (выпал старый день) — не отнимаем
r3 = aum.update_router_usage({"ts": "2026-10-13T00:00:00+00:00",
"burn_src": "store-BudgetSpent-оценка",
"spend_days": {"2026-10-12": {"cost": 2.5, "reqs": 12},
"2026-10-13": {"cost": 1.0, "reqs": 4}}}, path=RU)
w = json.load(open(RU))["weeks"]
check("router_usage: сжатие окна не отнимает burn",
abs(r3.get("delta_usd", -1)) < 0.001 and abs(w[0]["burn_usd"] - 4.5) < 0.001,
f"r3={r3}")
# 10d: окно старше 21д → сброс, новое окно, старое закрыто
r4 = aum.update_router_usage({"ts": "2026-11-15T00:00:00+00:00",
"burn_src": "store-BudgetSpent-оценка",
"spend_days": {"2026-11-14": {"cost": 7.0, "reqs": 30}}}, path=RU)
w = json.load(open(RU))["weeks"]
check("router_usage: недельный сброс окна",
r4.get("fresh_week") and len(w) == 2 and not w[0]["active"]
and w[1]["active"] and abs(w[1]["burn_usd"] - 7.0) < 0.001, str(r4))
# 10e: битый путь → ошибка в dict, без исключения
r5 = aum.update_router_usage({"ts": "2026-11-15T01:00:00+00:00",
"spend_days": {"2026-11-14": {"cost": 1.0, "reqs": 3}}},
path="/nonexistent-dir/ru.json")
check("router_usage: ошибка тихо в dict", "error" in r5)
# 10f: measure() ведёт реестр сам (интеграция: hermetic, реестр в tmp)
os.environ["SIMBIOT_ROUTER_USAGE_JSON"] = os.path.join(tmpdir, "ru2.json")
_m = aum.measure()
check("measure: router_usage пишется из замера",
os.path.exists(os.environ["SIMBIOT_ROUTER_USAGE_JSON"]))
# ── 11. env-доктор авто-источников (t-261019001211) ──
_empty = os.path.join(tmpdir, "empty.env")
with open(_empty, "w") as f:
f.write("# пусто\n")
doc = aum.env_doctor(env_path=_empty, coop_env=os.path.join(tmpdir, "no-coop.env"))
check("env_doctor: все три ключа отсутствуют на пустых .env",
[m["var"] for m in doc["missing"]] ==
["OPENROUTER_KEY", "STRIPE_LIVE_KEY", "SIMBIOT_ROUTER_DB"], str(doc["missing"]))
check("env_doctor: шаблон запроса к Alex готов",
"OPENROUTER_KEY" in doc["request_to_alex"]
and "STRIPE_LIVE_KEY" in doc["request_to_alex"])
_full = os.path.join(tmpdir, "full.env")
with open(_full, "w") as f:
f.write("OPENROUTER_KEY=sk-test\nSIMBIOT_ROUTER_DB=/tmp/x.sqlite\n")
_coop_full = os.path.join(tmpdir, "coop.env")
with open(_coop_full, "w") as f:
f.write("STRIPE_LIVE_KEY=sk-live-test\n")
doc2 = aum.env_doctor(env_path=_full, coop_env=_coop_full)
check("env_doctor: полный .env → missing=[]",
doc2["missing"] == [] and "не слепа" in doc2["request_to_alex"])
# ── 12. Δ burn неделя-к-неделе (t-261019001211, пункт 4) ──
RU2 = os.path.join(tmpdir, "ru_delta.json")
with open(RU2, "w") as f:
json.dump({"weeks": [{"burn_usd": 10.0, "active": False},
{"burn_usd": 7.5, "active": True}]}, f)
check("weekly_burn_delta: −2.5", aum.weekly_burn_delta(path=RU2) == -2.5)
os.environ["SIMBIOT_ROUTER_USAGE_JSON"] = RU2
check("weekly_burn_delta: одна неделя → None",
aum.weekly_burn_delta(path=os.path.join(tmpdir, "ru-one.json")) is None)
s_wd = aum.build_policy_aum_summary({"aum": 26.7, "burn_7d": 7.5,
"burn_src": "store-BudgetSpent-оценка"})
check("summary: Δburn неделя-к-неделе в контексте", "Δburn" in s_wd and "-2.5" in s_wd,
s_wd)
print() print()
print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}") print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}")
sys.exit(1 if fails else 0) sys.exit(1 if fails else 0)