diff --git a/.gitignore b/.gitignore index 32092a4..5e64699 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ harness/harness_es.db harness/hg_tg_inbox.db hg_tg_inbox.db store.db +# изоляция памяти (t-261004113351): локальный хом, если запустили без env +.simbiot-data/ +harness/channel_*.json +harness/channel_*.json.tmp diff --git a/README.md b/README.md index 870fca8..e1bbde1 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,44 @@ SIMBIOT_TG_ALLOWED, ROUTER_KEY, SIMBIOT_MODEL и др.); в systemd-режиме (глобальный asyncio-рантайм Strands не переживает 2+ `create_harness`) - `harness/commands.py` — command side - `harness/tg_commands.py` — без-LLM команды бота (сек-рефлекс) +- `harness/paths.py` — карта персистентных путей (SIMBIOT_HOME) - `cli.py` — init/task/task-await/tasks/status/events/requeue +## Изоляция памяти инстанса — SIMBIOT_HOME (t-261004113351) + +Память агента — данные, не код: при переносе инстанса другому владельцу +(мультитенантность) он не должен «думать» чужими проектами. Всё, что живёт +дольше процесса — ES-база, RAG-индекс знаний, `.env`, ключи аудита, выжимки, +штампы каналов, доки инстанса — размещается в одном каталоге-хоме, репо +остаётся чистым кодом. Свежие установки: `~/.simbiot-data` (install.sh +проставляет в обёртку и systemd-юнит автоматически). + +``` +SIMBIOT_HOME=/path/to/home venv/bin/python cli.py status +``` + +| файл | назначение | +|---|---| +| `/db/harness.db` | ES-база (события, проекции, delivery-леджер) | +| `/rag/rag.db` | RAG-индекс знаний инстанса | +| `/env/.env` | секреты (токены, ключи роутера), 0600 | +| `/docs/` | проектные документы владельца (aum/balances.json) | +| `/digests/` | файлы-выжимки уровней | +| `/audit/` | ключи крипто-аудита ed25519 | +| `/inbox/`, `/run/`, `/voice/` | каналы, штампы, кэш ASR | + +Приоритет: точечная env (`SIMBIOT_DB`, `SIMBIOT_RAG_DB`, …) → `SIMBIOT_HOME` +→ legacy-пути из корня репо (обратная совместимость: не задан `SIMBIOT_HOME` +— всё работает по-старому). + +Новый инстанс (например, для Юры): `scripts/init_instance.py` — чистая база + +идентичность владельца + его доки (+RAG-индексация) + опциональный селективный +импорт выжимок/доков из инстанса-источника. Тест изоляции: +`tests/test_instance_isolation.py` (поднять копию с пустой памятью — контекст +тика и RAG не содержат чужих проектов, 14 проверок). Перенос существующего +инстанса в хом: `scripts/migrate_to_home.sh` (ждёт тихое окно без LLM-воркеров, +идемпотентен). + ## CLI ```bash diff --git a/artifacts/presale-tracking-verify-2026-10-04.md b/artifacts/presale-tracking-verify-2026-10-04.md new file mode 100644 index 0000000..95c31c5 --- /dev/null +++ b/artifacts/presale-tracking-verify-2026-10-04.md @@ -0,0 +1,21 @@ +# Верификация трекинга presale.cil.by — 2026-10-04 01:01 UTC (задача t-261004000834) + +## Клик-трекинг CTA — ПОДТВЕРЖДЁН вживую +- Запрос: `POST https://presale.cil.by/api/presale/cta` `{"variant":"HR-VRFY","href":"https://presale.cil.by/ ?utm_source=harness-hourly&utm_campaign=tracking-verify-t-261004000834"}` +- Ответ: `{"ok":true}` (HTTP 200) +- Запись в БД /root/coop/coop.db, таблица presale_cta_events: + id=2 | variant=HR-VRFY | href=<см. выше> | created_at=2026-10-04 01:01:16 +- База событий: обработчик /root/coop/domains/simbiot_billing.py:83 (INSERT в presale_cta_events), лендинг шлёт beacon со всех [data-cta]. + +## Факт по трафику +- До тестового клика в presale_cta_events было 0 записей → органических кликов по CTA не было вообще. Канал трафика критичен. + +## Счётчик мест — ЖИВОЙ из биллинга +- GET https://presale.cil.by/api/presale/seats → {"paid":1,"taken":2,"total":100} + +## Stripe +- Лендинг ссылается на живой checkout buy.stripe.com/aFa28q8h14RpfPYgrK1B60a ✓ + +## Найденные пробелы (не блокируют запуск, рекомендации в launch-kit) +1. UTM сохраняются только внутри href (строкой), атрибуции source/campaign нет — рекомендация: парсить utm_* в отдельные колонки. +2. Нет счётчика визитов (только клики CTA) → конверсию визит→клик мерять нечем. Рекомендация: beacon /api/presale/visit или парсинг access-лога Caddy. diff --git a/bin/simbiot b/bin/simbiot new file mode 100755 index 0000000..90444ea --- /dev/null +++ b/bin/simbiot @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Simbiot Harness: запуск без установки обёртки (cli.py + scheduler-демон). +# simbiot help — справка по CLI-командам (как python cli.py -h) +# simbiot — без аргументов: статус + как управлять демоном/CLI +# simbiot --help — как help (аргумент для argparse-привычки) +DIR="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)" +unset PYTHONPATH +unset PYTHONHOME +cd "$DIR" || exit 1 +PY="$DIR/venv/bin/python" +[ -x "$PY" ] || PY="$(command -v python3)" +_flag=0 +for a in "$@"; do + case "$a" in + help|--help) _flag=1 ;; + esac +done +if [ "${_flag}" = 1 ]; then + exec "$PY" cli.py -h +fi +if [ $# -eq 0 ]; then + # Без аргументов: показать, что живёт и куда тыкать + echo "Simbiot Harness CLI — venv: $PY, репо: $DIR" + if command -v systemctl >/dev/null 2>&1; then + if systemctl is-active --quiet simbiot-harness; then + echo "Демон : active (systemctl status simbiot-harness)" + else + echo "Демон : не активен — $DIR/scripts/install.sh --systemd либо ./venv/bin/python -m harness.scheduler" + fi + elif pgrep -f "harness.scheduler" >/dev/null; then + echo "Демон : жив (pgrep harness.scheduler)" + else + echo "Демон : не запущен — $DIR/venv/bin/python -m harness.scheduler" + fi + echo "Быстрые команды:" + echo " simbiot status — статус харнеса" + echo " simbiot task ... — подать задачу" + echo " simbiot wizard — настройка (TG-бот, 9router, модель)" + echo " simbiot help — все команды CLI" + exit 0 +fi +exec "$PY" cli.py "$@" diff --git a/cli.py b/cli.py index e0d9e63..0fd4c0b 100644 --- a/cli.py +++ b/cli.py @@ -1,12 +1,14 @@ """Simbiot Harness — CLI: таски, статус, ручные тики.""" import argparse import json +import re import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from harness import commands, es +from harness import paths as _paths # t-261004113351-2858c1: .env инстанса def main(): @@ -14,7 +16,12 @@ def main(): sub = ap.add_subparsers(dest="cmd", required=True) 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("-m", "--minutes", type=int, default=5, help="оценка длительности (класс S≤5м M≤60м L≤8ч XL)") @@ -29,6 +36,14 @@ def main(): p_rq.add_argument("task_id") p_rq.add_argument("-r", "--reason", default="manual requeue") + p_cl = sub.add_parser("close", help="закрыть failed-задачу без работы (архив); failed>24ч схлопываются автоматически") + p_cl.add_argument("task_id") + p_cl.add_argument("-r", "--reason", default="manual close") + + p_bl = sub.add_parser("block", help="заблокировать живую задачу (ждёт уточнений); requeue вернёт в очередь") + p_bl.add_argument("task_id") + p_bl.add_argument("-r", "--reason", default="ожидает уточнений") + p_cmp = sub.add_parser("complete", help="закрыть running-задачу с результатом (батч-тик, t-260930101325)") p_cmp.add_argument("task_id") p_cmp.add_argument("-a", "--answer", required=True, help="результат задачи") @@ -140,6 +155,23 @@ def main(): help="модель уровней (по умолчанию auto — как в harness/levels.py)") p_wiz.add_argument("--no-init", action="store_true", help="не инициализировать стор") + # Подключение Telegram без полного визарда (просьба Alex, 2026-10-04): + # токен/чаты в .env + самотест getMe + рестарт TG-поллера + меню команд. + p_tgc2 = sub.add_parser("tg-connect", + help="подключить Telegram: токен бота в .env, самотест, рестарт поллера") + p_tgc2.add_argument("token", nargs="?", default=None, + help="токен TG-бота (SIMBIOT_TG_TOKEN)") + p_tgc2.add_argument("--token", dest="token_opt", default=None, + help="токен TG-бота флагом (то же, что позиционным)") + p_tgc2.add_argument("--allowed", default=None, + help="разрешённые chat-id через запятую (SIMBIOT_TG_ALLOWED)") + p_tgc2.add_argument("--no-restart", action="store_true", + help="не перезапускать simbiot-harness-tg (токен подхватится при следующем рестарте)") + p_tgc2.add_argument("--no-commands", action="store_true", + help="не регистрировать меню команд в чате (setMyCommands)") + p_tgs2 = sub.add_parser("tg-status", + help="состояние Telegram-подключения: токен, чаты, поллер, меню бота") + p_rag = sub.add_parser("rag-query", help="семантический поиск по базе кооператива (вендор /root/rag)") p_rag.add_argument("question") p_rag.add_argument("-k", type=int, default=5, help="сколько чанков (<=5)") @@ -155,17 +187,47 @@ def main(): elif args.cmd == "wizard": _run_wizard(args) + elif args.cmd == "tg-connect": + _run_tg_connect(args) + elif args.cmd == "tg-status": + _run_tg_status(args) elif args.cmd == "task": - try: - tid, size = commands.submit_task(args.title, args.details, args.minutes) - except ValueError as e: - print(f"ERROR: {e}") - sys.exit(1) - if args.await_delivery: - commands.mark_awaiting_delivery(tid, reason="подана из TG с автодоставкой") - print(f"OK: задача {tid} [{size}] подана (результат уйдёт в TG при готовности)") + # t-261003125513/e-261003125707: 'task list' раньше создавал задачу-клон + # 'list' — теперь показывает доску (внутри '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() + elif re.fullmatch(r"t-\d{6,14}-\w+", (args.title or "").strip()): + # e-261004134613-dda90f: title=ID существующей задачи → показать её, + # а не плодить клон (агент звал 'cli.py task t-...' для чтения) + es.init_db() + model = es.run_projections() + tid = args.title.strip() + t = (model.get("tasks") or {}).get(tid) + if t is None: + print(f"ERROR: задача {tid} не найдена — чтение по id, создание клона запрещено", + file=sys.stderr) + sys.exit(2) + print(f"{t['id']} [{t['size']:2}] {t['status']:7} {t.get('title','')}") + print(f" est: {t.get('est_minutes','?')}м создана: {t.get('ts','?')}") + if t.get("details"): + print(f" детали: {t['details']}") else: - print(f"OK: задача {tid} [{size}] подана") + # bc59609: guard мусорных заголовков ('list'/id без деталей) — ValueError наружу + try: + tid, size = commands.submit_task(args.title, args.details, args.minutes) + except ValueError as e: + print(f"ERROR: {e}") + sys.exit(1) + if args.await_delivery: + commands.mark_awaiting_delivery(tid, reason="подана из TG с автодоставкой") + print(f"OK: задача {tid} [{size}] подана (результат уйдёт в TG при готовности)") + else: + print(f"OK: задача {tid} [{size}] подана") elif args.cmd == "task-await": es.init_db() commands.mark_awaiting_delivery(args.task_id, reason=args.reason) @@ -185,6 +247,17 @@ def main(): args.task_id, args.answer, level="manual") print(f"OK: задача {tid} завершена" + (" (доставлено в TG)" if delivered else "")) + elif args.cmd == "close": + # авто-схлопывание старых фейлов (e-261003094432): failed → closed, + # ручное закрытие + демон-архиватор (failed>24ч автоматически) + es.init_db() + tid = commands.close_task(args.task_id, args.reason) + print(f"OK: задача {tid} закрыта (closed — архив; requeue вернёт в очередь)") + elif args.cmd == "block": + # t-261004122019: blocked — задача есть, но не ready (ждёт уточнений) + es.init_db() + tid = commands.block_task(args.task_id, args.reason) + print(f"OK: задача {tid} заблокирована (blocked; requeue вернёт в очередь)") elif args.cmd == "promote": es.init_db() tid, new_size = commands.promote_task(args.task_id, args.reason) @@ -524,8 +597,89 @@ def _tg_selftest(token: str) -> str: return f"ОШИБКА ({e.__class__.__name__}) — токен не принят, но запись в .env сделал" +# ============================================================================ +# Подключение Telegram из CLI (просьба Alex, 2026-10-04): раньше токен был +# только внутри интерактивного wizard'а — теперь точечные команды. +# ============================================================================ +def _tg_restart_poller() -> str: + """Рестарт TG-поллера, если он под systemd (иначе подхватит при след. старте).""" + import subprocess + if not os.path.exists("/run/systemd/system"): + return "systemd не обнаружен — поллер подхватит токен при следующем старте" + r = subprocess.run(["systemctl", "restart", "simbiot-harness-tg"], + capture_output=True, text=True, timeout=30) + if r.returncode == 0: + return "OK: simbiot-harness-tg перезапущен — читает токен из .env" + return f"рестарт pollera не удался: {(r.stderr or r.stdout).strip()[:200]}" + + +def _run_tg_connect(args) -> None: + token = (args.token or getattr(args, "token_opt", None) or "").strip() + env_path = _paths.env_path() + vals = {} + if token: + vals["SIMBIOT_TG_TOKEN"] = token + if args.allowed is not None and args.allowed.strip(): + vals["SIMBIOT_TG_ALLOWED"] = args.allowed.strip() + if not vals: + cur = _dotenv_get(env_path, "SIMBIOT_TG_TOKEN") + print("Токен не передан." if not cur + else f"Токен уже задан ({_mask(cur)}). Передай его аргументом: " + "simbiot tg-connect <ТОКЕН> [--allowed 123,456]") + return + if "SIMBIOT_TG_TOKEN" in vals: + print(f" TG-токен: {_tg_selftest(vals['SIMBIOT_TG_TOKEN'])}") + for key, val in vals.items(): + _dotenv_set(env_path, key, val) + print(f" .env ← {key}={_mask(val) if 'TOKEN' in key else val}") + if not args.no_commands: + from harness import tg_commands + reg = tg_commands.register_bot_commands() + print(" меню команд: " + ({"cyrillic": "зарегистрировано (кириллица)", + "latin": "зарегистрировано (транслит)"}.get(reg, "не удалось")) + if reg else " меню команд: пропущено (нет токена/недоступен TG)") + if not args.no_restart: + print(" " + _tg_restart_poller()) + + +def _run_tg_status(args) -> None: + import subprocess + import urllib.request + + from harness import tg_inbox + tg_inbox._load_env() + tok = tg_inbox.TG_TOKEN + print("🔌 Telegram-подключение") + if not tok: + print(" токен: НЕ задан — simbiot tg-connect <ТОКЕН>") + return + print(f" токен: задан ({_mask(tok)})") + try: + with urllib.request.urlopen(f"https://api.telegram.org/bot{tok}/getMe", timeout=5) as r: + me = json.loads(r.read().decode()).get("result", {}) + print(f" getMe: ок, бот @{me.get('username')} (id {me.get('id')})") + except Exception as e: + print(f" getMe: ОШИБКА ({e.__class__.__name__}) — токен не принимается") + allowed = sorted(tg_inbox.ALLOWED) + print(" разрешённые чаты: " + (", ".join(allowed) if allowed else "не заданы")) + import subprocess + if os.path.exists("/run/systemd/system"): + for unit in ("simbiot-harness-tg.service", "simbiot-harness.service"): + st = subprocess.run(["systemctl", "is-active", unit], + capture_output=True, text=True).stdout.strip() + print(f" {unit}: {st}") + cmds = [("никак", [])] + try: + from harness import tg_commands + cmds = tg_commands.get_my_commands() or [("нет", [])] + except Exception: + pass + names = ", ".join(c[0] for c in cmds) if cmds and cmds[0][0] not in ("никак", "нет") else "нет" + print(f" меню бота: {names}") + + def _run_wizard(args) -> None: - env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env") + env_path = _paths.env_path() vals = { "SIMBIOT_TG_TOKEN": args.tg_token if args.tg_token is not None else None, "SIMBIOT_TG_ALLOWED": args.tg_allowed, diff --git a/design-plans/simbiot-cilby-v2-audit.md b/design-plans/simbiot-cilby-v2-audit.md new file mode 100644 index 0000000..342afbd --- /dev/null +++ b/design-plans/simbiot-cilby-v2-audit.md @@ -0,0 +1,38 @@ +# improve-ui аудит симбиот-лендинга — план фиксов (t-261002151425) + +Аудитор: improve-ui (skills/ui/improve-ui) · поверхность: https://simbiot.cil.by +(одностраничник, /var/www/simbiot/index.html) · evidence: скрины 1440×5200 desktop +и 390×844/3600 mobile (headless chromium prod), dump-dom после JS, статический +разбор CSS/HTML. Критерии: contract + runtime + correction. + +## Design language +- Audited surface: одностраничный лендинг (nav, hero, team/agents, system, loop, math, included, pricing, CTA, footer). +- Дизайн-источники: токены `:root` (paper/ink/line/acc-violet), Inter Tight + JetBrains Mono, uppercase mono-кнопки, секции с sec-idx нумерацией. +- Documented decisions: один сигнальный цвет (--acc:#5b4be0), светлый фон, плоские карточки с 1px линиями. + +## Находки (в порядке приоритета) + +### P0 — контент ниже hero невидим без JS (runtime-ловушка) +`line 177: .reveal{opacity:0;transform:translateY(18px)}` — все секции после hero +рендерятся с opacity:0 и видны только после JS (IntersectionObserver → .vis). +Скрин/DOM: без скриптов страницы нет контента; SEO-краулеры и отказавший JS = +пустая страница продаж. Правка контракта: `