AUM v1: метрика цели измерима (t-261004000834-ed84f5, day-20731): balances.json + авто-refresh из API (OpenRouter credits, Stripe live; livemode-only), AUM=26.7 (было None), aum --record обновил aum_target для всех тиков; тесты test_aum +4 (mock API, livemode=False→None) — ВСЕ ОК; hour-тик: task list без клонов-задач + tg_inbox env-fallback (e-261003125707); дневной отчёт day-2026-10-04

This commit is contained in:
Simbiot Harness 2026-10-04 00:16:09 +00:00
parent f1c6aa85af
commit f286f1b8ed
6 changed files with 327 additions and 7 deletions

16
cli.py
View file

@ -14,7 +14,12 @@ def main():
sub = ap.add_subparsers(dest="cmd", required=True) sub = ap.add_subparsers(dest="cmd", required=True)
p_task = sub.add_parser("task", help="подать задачу") p_task = sub.add_parser("task", help="подать задачу")
p_task.add_argument("title") # t-261003125513/e-261003125707: 'task list' раньше создавал задачу-клон
# 'list' — теперь показывает доску (внутри 'list' как слово не валидируем,
# чтобы не породить рекурсию справки)
p_task.add_argument("title", nargs="?")
p_task.add_argument("--list", dest="list_board", action="store_true",
help="показать доску задач (не создавать)")
p_task.add_argument("-d", "--details", default="") p_task.add_argument("-d", "--details", default="")
p_task.add_argument("-m", "--minutes", type=int, default=5, p_task.add_argument("-m", "--minutes", type=int, default=5,
help="оценка длительности (класс S≤5м M≤60м L≤8ч XL)") help="оценка длительности (класс S≤5м M≤60м L≤8ч XL)")
@ -160,6 +165,15 @@ def main():
elif args.cmd == "wizard": elif args.cmd == "wizard":
_run_wizard(args) _run_wizard(args)
elif args.cmd == "task": elif args.cmd == "task":
# t-261003125513/e-261003125707: голое 'list'/'tasks'/'task list' —
# показать доску, а не плодить задачи-клоны
if getattr(args, "list_board", False) or (args.title or "").strip() in ("list", "tasks", "task list"):
es.init_db()
for t in commands.list_tasks():
print(f"{t['id']} [{t['size']:2}] {t['status']:7} {t['title'][:70]}")
elif not args.title:
p_task.print_help()
else:
tid, size = commands.submit_task(args.title, args.details, args.minutes) tid, size = commands.submit_task(args.title, args.details, args.minutes)
if args.await_delivery: if args.await_delivery:
commands.mark_awaiting_delivery(tid, reason="подана из TG с автодоставкой") commands.mark_awaiting_delivery(tid, reason="подана из TG с автодоставкой")

32
docs/aum/balances.json Normal file
View file

@ -0,0 +1,32 @@
{
"_comment": "Замер AUM кооператива (t-261004000834-ed84f5, day-тик 2026-10-04). amount_usd — USD-эквивалент; null — требует ручного ввода Alex (в total не входит). Авто-источники помечены src; обновлять по мере изменения балансов.",
"as_of": "2026-10-04",
"accounts": [
{
"name": "OpenRouter кредиты коопа (доступно, key из simbiot-harness/.env)",
"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"
},
{
"name": "US LLC Delaware (операционный)",
"amount_usd": null,
"src": "ручной ввод Alex (API-источник неизвестен)"
},
{
"name": "Крипто-кошелёк кооператива",
"amount_usd": null,
"src": "ручной ввод Alex"
},
{
"name": "Stripe live balance (available)",
"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"
}
],
"debts": [],
"notes": "Первый непустой замер: total = 26.70 USD. Что НЕ вошло и почему: Stripe test-balance (livemode=False, pending $335.24) — тестовые деньги, в AUM не актив; 9router prepay — величина предоплаты не замерена (нужен источник); выручка presale — нулевая на момент замера (live available=0, pending=0). Для Alex: заполнить null-строки (LLC-счёт, крипто-кошелёк, 9router prepay) — после этого AUM станет полным. Методика: docs/aum.md."
}

View file

@ -0,0 +1,84 @@
# Дневной отчёт Simbiot Harness — 2026-10-04
Дневной тик (day-20731, 00:00 UTC). Манифест дня: разбор эскалаций → верификация
системы → план → L-задачи. Одна L-задача закрыта в тике (AUM v1), вторая в очереди
на часовой.
## План дня
1. **AUM v1 — сделать метрику цели измеримой** (t-261004000834-ed84f5, [L]) —
✅ закрыто дневным тиком, см. ниже. Мотивация: северная звезда (t-260929161138)
была None с момента фиксации — управлять долгосрочной базой без числа нельзя.
2. **Первый канал трафика на presale** (t-261004000834-4bbc6f, [L]) — launch-kit
органического запуска (Show HN / Reddit / X-тред, EN-черновики) + верификация
клик-трекинга на presale.cil.by; публикация — только через Alex (график
3-через-3, docs/alex-schedule.md). По docs/SALES-PLAN-1000-US.md: воронка,
лендинг, живой Stripe и счётчик мест готовы — трафика ~0, без канала 1000
предзаказов недостижимы. Фильтр AUM: трафик → предзаказы $99.9/мес → revenue.
3. Фон: M-задача t-261003162607-aa8abf (HTML-план с ИИ-маскотами, результат
обещан Alex в TG) возвращена в очередь — заберёт часовой тик.
## Эскалации
- **e-261003170542-389f29 (warn, hour→day) — закрыт.** Hour-тик упал на
транзиентном «Request timed out» (стрим роутера умер в event_loop) при
выполнении aa8abf. Причина инфраструктурная, не код: тик повторяется сам на
следующем часу; задача requeue'd с сохранением delivery-await (Alex получит
результат в TG автоматически). Пятый падёж за сутки на таймаутах —
контрмеры уже в проде (tick_timeout, изолированный runner с killpg,
авто-схлопывание failed>24ч), наблюдение продолжается.
- Открытых эскалаций на конец тика: **0**.
## Сделано в тике: AUM v1 (t-261004000834-ed84f5, [L]) ✅
**AUM впервые измерен: 26.70 USD (было None).**
1. `docs/aum/balances.json` заведён по шаблону: OpenRouter кредиты коопа
**26.70$** (API: total=163, usage=136.30), Stripe live available **0.0$**
(livemode=True, available=0/pending=0 — presale-выручки ещё нет), LLC-счёт и
крипто-кошелёк — `null` (ждут ручного ввода Alex; в total не входят — метрика
честная, не раздутая).
2. Авто-обновление балансов в `harness/aum.py`: `refresh_auto_balances()` —
счета с `auto:true` обновляются из API перед каждым снапшотом `measure()`
(включая дневной `AumMeasured`): OpenRouter `/api/v1/credits`, Stripe
`/v1/balance`. Только read-only GET, ключи из локальных .env (в стор/логи не
попадают), сетевая ошибка → остаётся прошлое значение (stale честнее
выдуманного), запись атомарная (tmp+rename), выключатель
`SIMBIOT_AUM_AUTO_REFRESH=0`.
3. Защита от фальшивых денег: Stripe test-balance (livemode=False, pending
$335.24) в AUM **не** актив — проверено обоими ключами; тест livemode=False
→ None.
4. `cli.py aum --record` записан: `policy.aum_target` обновлён — **цель в
контексте каждого тика min/hour/day теперь с числом**: «AUM=26.7 у.е.». Приёмка
из постановки выполнена: `cli.py aum` → 26.7, дневной `AumMeasured` завтра
уйдёт в стор уже не с None.
Осталось для полного AUM (нужно Alex): суммы LLC-счёта, крипто-кошелька,
9router prepay — заполнение null-строк поднимет total автоматически.
## Верификация системы
- Тесты репо: **17/17 файлов зелёные** (прямой запуск; unittest-discover по
этим файлам неприменим — часть тестов скриптового стиля с sys.exit на уровне
модуля, это не регрессия логики).
- Аудит стора `cli.py verify`: 828 518 событий, подписи ed25519 + тампер-цепочка
**валидны** (ключ faf8d19d9aed71fe).
- Доска: открытых задач 1 (aa8abf, ready, M — на часовой), failed 0.
- Коммит: часы-наработка (task list без клонов, tg_inbox env-fallback) + AUM v1
вынесены в git.
## Метрики
- **AUM: 26.70 у.е.** (первый непустой замер; полный — после ввода Alex).
- burn 9router: 24ч=1.64 · 7д=2291.81 у.е. Тренд благоприятный: 7д-окно
снизилось с 2521.59 (манифест вчера) до ~2292 — экономия после переключения
конфигураций держится.
- Стикerness уровней: min/hour под гейтом тихо (критичных сигналов нет) —
расходы на тики экономятся; day-тик выполнил манифест и закрыл L-задачу.
## На завтра
- Заполнение balances.json Alex'ом (LLC, крипта, 9router prepay) → полный AUM.
- 4bbc6f: launch-kit → публикация с аккаунтов Alex (учесть 3-через-3), первые
UTM-метрики → цикл «канал → клики → checkout».
- Мониторить второй заход aa8abf на часовом (результат должен уйти в TG).

View file

@ -69,6 +69,111 @@ def read_balances():
return {"total": None, "accounts": [], "src": "balances.json (нет файла)"} return {"total": None, "accounts": [], "src": "balances.json (нет файла)"}
# ────────────────── авто-источники балансов (t-261004000834-ed84f5) ──────────────────
# AUM не должен жить только на ручном вводе: что измеряется API — измеряем сами.
# Принцип: read-only GET, ключи только из локальных .env (в сток/логи не попадают),
# ошибка сети/ключа → старое значение остаётся (stale-факт честнее выдуманного).
AUTO_REFRESH_TIMEOUT = 15 # сек на API-запрос
def _env_file_value(path: str, prefix: str):
"""Значение VAR=… из .env-файла (первое вхождение) или None."""
try:
with open(path, encoding="utf-8") as f:
for line in f:
if line.startswith(prefix + "="):
return line.split("=", 1)[1].strip().strip('"').strip("'").strip() or None
except OSError:
pass
return None
def _http_json(url: str, headers: dict):
import urllib.request
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=AUTO_REFRESH_TIMEOUT) as resp:
return json.load(resp)
def refresh_openrouter_credits(env_path: str = None):
"""Доступные кредиты OpenRouter коопа (USD) или None (нет ключа/ошибка)."""
env_path = env_path or os.path.join(HARNESS_ROOT, ".env")
key = _env_file_value(env_path, "OPENROUTER_KEY")
if not key:
return None
try:
d = _http_json("https://openrouter.ai/api/v1/credits",
{"Authorization": f"Bearer {key}"})
inf = d.get("data", {})
total, usage = inf.get("total_credits"), inf.get("total_usage")
if total is None:
return None
return round(float(total) - float(usage or 0.0), 2)
except Exception:
return None
def refresh_stripe_available(coop_env: str = "/root/coop/.env"):
"""Stripe live available USD или None. Только livemode=True: тестовые
деньги (livemode=False) в AUM не актив — иначе метрика врёт."""
key = _env_file_value(coop_env, "STRIPE_LIVE_KEY")
if not key:
return None
try:
d = _http_json("https://api.stripe.com/v1/balance",
{"Authorization": f"Bearer {key}"})
if not d.get("livemode"):
return None
for b in d.get("available", []):
if b.get("currency") == "usd":
return round(b["amount"] / 100.0, 2)
return 0.0
except Exception:
return None
def refresh_auto_balances(path: str = None, coop_env: str = "/root/coop/.env"):
"""Обновить accounts с auto:true в balances.json значениями из API.
Роутинг по имени счёта (openrouter → кредиты, stripe → live available).
Файла нет / ни одно auto-поле не обновилось → ничего не пишем.
Возвращает список имён обновлённых счетов."""
path = path or os.environ.get("SIMBIOT_BALANCES_JSON", BALANCES_JSON)
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
except Exception:
return []
updated = []
for acc in data.get("accounts", []):
if not acc.get("auto"):
continue
name = (acc.get("name") or "").lower()
val = None
if "openrouter" in name:
val = refresh_openrouter_credits()
elif "stripe" in name:
val = refresh_stripe_available(coop_env)
if val is None:
continue
acc["amount_usd"] = val
acc["measured_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
updated.append(acc.get("name"))
if not updated:
return []
data["as_of"] = datetime.now(timezone.utc).date().isoformat()
try:
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n")
os.replace(tmp, path) # атомарно: читатели не увидят полуфайл
except Exception:
return []
return updated
def read_router_spend(days: int = 7): def read_router_spend(days: int = 7):
"""Суточный расход 9router за N дней: {date: {cost, reqs}}. Ошибка → {}.""" """Суточный расход 9router за N дней: {date: {cost, reqs}}. Ошибка → {}."""
try: try:
@ -99,7 +204,17 @@ def read_coop_structure():
# ────────────────────────── замер ────────────────────────── # ────────────────────────── замер ──────────────────────────
def measure(): def measure():
"""Снапшот метрики: AUM (если замерен) + burn 24ч/7д + структура кооператива.""" """Снапшот метрики: AUM (если замерен) + burn 24ч/7д + структура кооператива.
Авто-балансы (accounts c auto:true) перед замером обновляются из API
(t-261004000834-ed84f5). SIMBIOT_AUM_AUTO_REFRESH=0 выключает (тесты/
офлайн-режим); сетевая ошибка не валит снапшот — останется прошлое
значение (stale честнее выдуманного)."""
if os.environ.get("SIMBIOT_AUM_AUTO_REFRESH", "1") not in ("0", "false", "no"):
try:
refresh_auto_balances()
except Exception:
pass # обновление — best effort: замер burn важнее и идёт всегда
bal = read_balances() bal = read_balances()
spend = read_router_spend(days=7) spend = read_router_spend(days=7)
days_iter = list(spend.values()) days_iter = list(spend.values())

View file

@ -35,6 +35,12 @@ def _load_env():
TG_TOKEN = line.split("=", 1)[1].strip() TG_TOKEN = line.split("=", 1)[1].strip()
elif line.startswith("SIMBIOT_TG_ALLOWED="): elif line.startswith("SIMBIOT_TG_ALLOWED="):
ALLOWED = {x.strip() for x in line.split("=", 1)[1].split(",") if x.strip()} ALLOWED = {x.strip() for x in line.split("=", 1)[1].split(",") if x.strip()}
_tok = os.environ.get("SIMBIOT_TG_TOKEN", "").strip() # systemd EnvironmentFile задаёт переменные, но не пишет .env
if not TG_TOKEN and _tok:
TG_TOKEN = _tok
_allow = os.environ.get("SIMBIOT_TG_ALLOWED", "").strip()
if TG_TOKEN and not ALLOWED and _allow:
ALLOWED = {x.strip() for x in _allow.split(",") if x.strip()}
def _api(method: str, params: dict, timeout: int = 35): def _api(method: str, params: dict, timeout: int = 35):

View file

@ -127,6 +127,75 @@ r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harn
capture_output=True, text=True, timeout=120, env=env) capture_output=True, text=True, timeout=120, env=env)
check("cli aum-pilot ok", r.returncode == 0 and "вердикт" in r.stdout, r.stderr[:200]) check("cli aum-pilot ok", r.returncode == 0 and "вердикт" in r.stdout, r.stderr[:200])
# ── 9. авто-обновление балансов из API (t-261004000834-ed84f5) — hermetic ──
import tempfile
tmpdir = tempfile.mkdtemp(prefix="simbiot-aum-")
# 9a: файла нет → [] без падения
check("refresh_auto: нет файла → []",
aum.refresh_auto_balances(path="/nonexistent-balances.json") == [])
# 9b: auto-счёт, но ключей нет → значение не пишется, ошибок нет
bal_nokey = os.path.join(tmpdir, "b1.json")
with open(bal_nokey, "w") as f:
json.dump({"accounts": [{"name": "Stripe live balance (available)",
"amount_usd": 0.0, "auto": True}]}, f)
_saved_efv = aum._env_file_value
aum._env_file_value = lambda *a, **k: None # ключей нет
try:
got = aum.refresh_auto_balances(path=bal_nokey, coop_env=os.path.join(tmpdir, "no.env"))
finally:
aum._env_file_value = _saved_efv
check("refresh_auto: без ключа → [] и файл не тронут",
got == [] and json.load(open(bal_nokey))["accounts"][0]["amount_usd"] == 0.0)
# 9c: _http_json мокается → суммы пишутся, measured_at/as_of обновляются,
# ручные счета не трогаются
bal_mock = os.path.join(tmpdir, "b2.json")
with open(bal_mock, "w") as f:
json.dump({"accounts": [
{"name": "OpenRouter кредиты", "amount_usd": None, "auto": True},
{"name": "Stripe live balance", "amount_usd": None, "auto": True},
{"name": "ручной счёт Alex", "amount_usd": 5.0}],
"as_of": "2000-01-01"}, f)
with open(os.path.join(tmpdir, "stripe.env"), "w") as f:
f.write("STRIPE_LIVE_KEY=sk_test_fake\n")
def _fake_http(url, headers):
if "openrouter" in url:
return {"data": {"total_credits": 100, "total_usage": 40}}
return {"livemode": True, "available": [{"currency": "usd", "amount": 12345}]}
_saved_http = aum._http_json
aum._http_json = _fake_http
try:
got2 = aum.refresh_auto_balances(path=bal_mock,
coop_env=os.path.join(tmpdir, "stripe.env"))
finally:
aum._http_json = _saved_http
d2 = json.load(open(bal_mock))
check("refresh_auto: mock API → 60.0 и 123.45 записаны",
len(got2) == 2 and abs(d2["accounts"][0]["amount_usd"] - 60.0) < 0.001
and abs(d2["accounts"][1]["amount_usd"] - 123.45) < 0.001,
f"got2={got2} d2={d2}")
check("refresh_auto: measured_at у auto, ручной не тронут, as_of обновлён",
bool(d2["accounts"][0].get("measured_at"))
and d2["accounts"][2]["amount_usd"] == 5.0 and d2["as_of"] != "2000-01-01")
# 9d: Stripe livemode=False → None: тестовые деньги в AUM не актив
def _fake_http_testmode(url, headers):
return {"livemode": False, "available": [{"currency": "usd", "amount": 33524}]}
aum._http_json = _fake_http_testmode
try:
v_test = aum.refresh_stripe_available(coop_env=os.path.join(tmpdir, "stripe.env"))
finally:
aum._http_json = _saved_http
check("stripe: livemode=False → None (не актив)", v_test is None)
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)