diff --git a/harness/levels.py b/harness/levels.py index 9f81260..55e7f98 100644 --- a/harness/levels.py +++ b/harness/levels.py @@ -2,7 +2,7 @@ Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель. """ -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace import os SEC = 1 @@ -15,6 +15,22 @@ QUARTER = 91 * DAY YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания +def _env_cfg(key: str, default: str = "") -> str: + """Значение настройки: env сильнее .env харнеса (мини-ридер без circular import).""" + val = os.environ.get(key, "").strip() + if val: + return val + try: + env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env") + with open(env_path, encoding="utf-8") as f: + for line in f: + if line.startswith(key + "="): + return line.split("=", 1)[1].strip().strip('"').strip("'").strip() or default + except OSError: + pass + return default + + def _cfg_model(default: str = "auto") -> str: """SIMBIOT_MODEL (визард первого запуска, t-261002193628-1d1e5f): одна модель для всех LLM-уровней вместо auto — env сильнее .env @@ -182,6 +198,72 @@ LEVELS = { ), } +# ── Профиль агента (2026-10-18) ────────────────────────────────────────────── +# studio — Коди, агент студии/кооператива (мандаты выше, без изменений). +# client — агент клиента Shkor Labs Autopilot на его сервере: своё имя, владелец — +# клиент, инструменты тарифа (MCP лабы уровня community). Ставит портал при +# развёртывании (scripts/panel_tools.sh пишет SIMBIOT_PROFILE и имена в .env). +PROFILE = _env_cfg("SIMBIOT_PROFILE", "studio") +AGENT_NAME = _env_cfg("SIMBIOT_AGENT_DISPLAY_NAME", "Коди") +OWNER = _env_cfg("SIMBIOT_OWNER_NAME", "Alex") +COMPANY = _env_cfg("SIMBIOT_COMPANY", "") + +_CLIENT_COMMON = ( + "Файлы в чат — только идемпотентно: cli.py tg-deliver-file <путь> -c \"подпись\" " + "(повтор — SKIP); прямые curl-отправки из shell запрещены. " + "Инструмент portal_mcp — MCP лаборатории Shkor Labs на тарифе Autopilot: " + "git_repo_create/git_repo_list/git_repo_info — личные репозитории владельца на " + "git.cil.by; site_request — поддомен .cil.by на этот сервер (до 3); " + "project_create — проект владельца (доля студии от 10%, удерживается с каждой " + "оплаты); product_create/product_list — продукты с платёжной ссылкой (стартуют в " + "тестовом режиме, боевые платежи включает студия после ревью — заявка в кабинете " + "shkor.com/account); webhook_route_* — вебхуки оплат своего проекта; earnings — " + "заработок; skill_hub_search/skill_get — готовые скиллы; feature_request — " + "пожелания студии. Реестров кооператива, звонков, почты, казны и голосований у " + "тебя НЕТ — не обещай их. Если просят то, чего ты не умеешь, скажи прямо и " + "предложи, что можно сделать. " + "Не исследуй исходники харнеса и не разбирай падения тиков сам — эскалируй: " + "cli.py escalate \"причина\" -c \"контекст\" -s critical|warn|info.") + + +def _client_mandates() -> dict: + who = f"«{COMPANY}»" if COMPANY else "своего владельца" + ident = (f"Ты — {AGENT_NAME}, AI-агент компании {who} на платформе Shkor Labs " + f"(владелец — {OWNER}). Ты живёшь на собственном сервере владельца и " + f"работаешь на его проект. Говори о себе в первом лице как {AGENT_NAME}; " + "ты НЕ Коди и не агент кооператива 42. Рядом в панели .ui.shkor.com " + "владельцу доступны OpenCode и Claude Code — для разработки и правок кода " + "предлагай их. ") + return { + "min": (f"Ты — минутный пульс харнеса Simbiot. {ident}" + "Все неотвеченные сообщения владельца приходят тебе ОДНИМ батчем (секция " + "входящих) — обработай его целиком за тик: один сводный ответ " + "(tg-reply ,,… -c \"текст\") или отдельные реплаи, но ни одно " + "сообщение не должно остаться без ответа. Тон: кратко, тепло, на языке " + "владельца. СНАЧАЛА ОТВЕТ, ПОТОМ РАБОТА: если работа дольше ~30 с — первым " + "делом короткий tg-reply («понял, делаю: …»), итог — cli.py tg-reply " + "--followup \"итог\". Мелкие задачи до 5 минут делай сам; большие подавай " + "задачей (cli.py task -a — результат сам придёт в чат) и скажи об этом. " + "Не создавай задачи из служебных сообщений («тест», «привет»). " + + _CLIENT_COMMON), + "hour": (f"Ты — часовой поток харнеса Simbiot. {ident}" + "Дайджест часа: что сделано по проекту владельца, статусы задач, батч " + "задач S/M/L строго в порядке очереди. Эскалации нижних уровней (секция " + "ЭСКАЛАЦИИ) — твоя зона: разберись и закрой cli.py escalation-ack . " + "Выбор из 2–9 вариантов — cli.py decide -q \"вопрос\" -o ключ=описание. " + + _CLIENT_COMMON), + "day": (f"Ты — дневной цикл харнеса Simbiot. {ident}" + "Манифест дня: план по проекту владельца, выбор L-задач, дневной отчёт " + "владельцу — что сделано, что дальше, что нужно от него (продукт, " + "трекшн, следующий уровень в кабинете shkor.com/account). " + + _CLIENT_COMMON), + } + + +if PROFILE == "client": + for _name, _mandate in _client_mandates().items(): + LEVELS[_name] = replace(LEVELS[_name], mandate=_mandate) + # v0.2+: week/month/quarter/year — включаются по мере готовности FUTURE_LEVELS = ("week", "month", "quarter", "year") diff --git a/harness/scheduler.py b/harness/scheduler.py index 15fcfb0..cc70b54 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -18,6 +18,7 @@ from datetime import datetime, timezone, timedelta from . import es from .levels import LEVELS +from . import levels as _levels from . import paths as _paths from . import runtime from . import commands @@ -613,7 +614,7 @@ def build_context(level_cfg, model: dict, batch=None): own = _own_messages(chat_ids, replied_hist) if depth >= 1 else [] if own: parts.append("ТВОИ НЕДАВНИЕ СООБЩЕНИЯ В ЭТОТ ЧАТ (сам отправил: итоги задач, " - "уведомления, предложения — Alex может отвечать на них):\n" + f"уведомления, предложения — {_levels.OWNER} может отвечать на них):\n" + "\n".join(own)) if replied_hist: replied_hist = sorted(replied_hist, key=lambda x: x.get("ts") or "")[-12:] diff --git a/tests/test_profile.py b/tests/test_profile.py new file mode 100644 index 0000000..ebae760 --- /dev/null +++ b/tests/test_profile.py @@ -0,0 +1,51 @@ +"""Профиль агента: studio (Коди) по умолчанию, client — агент клиента Autopilot со своим именем.""" +import os +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +CODE = ("import json; from harness.levels import LEVELS, PROFILE, OWNER; " + "print(json.dumps({'p': PROFILE, 'o': OWNER, 'm': {k: v.mandate for k, v in LEVELS.items()}}))") + + +def run(env): + e = {k: v for k, v in os.environ.items() if not k.startswith("SIMBIOT_")} + e.update(env) + out = subprocess.run([sys.executable, "-c", CODE], cwd=ROOT, env=e, capture_output=True, text=True, check=True) + import json + return json.loads(out.stdout) + + +def main(): + ok = bad = 0 + + def eq(c, msg): + nonlocal ok, bad + if c: + ok += 1 + else: + bad += 1 + print("FAIL", msg) + + st = run({"SIMBIOT_PROFILE": "studio"}) + eq(st["p"] == "studio", "studio profile") + eq("Коди" in st["m"]["min"] and "@juno42bot" in st["m"]["min"], "studio min is Cody") + eq("booking_slots" in st["m"]["min"], "studio has booking") + + cl = run({"SIMBIOT_PROFILE": "client", "SIMBIOT_AGENT_DISPLAY_NAME": "Acme Autopilot", + "SIMBIOT_OWNER_NAME": "Ann Bee", "SIMBIOT_COMPANY": "Acme"}) + eq(cl["o"] == "Ann Bee", "owner") + for lvl in ("min", "hour", "day"): + m = cl["m"][lvl] + eq("Acme Autopilot" in m and "«Acme»" in m and "Ann Bee" in m, f"{lvl}: identity") + eq("@juno42bot" not in m and "Алекс" not in m and "Alex" not in m, f"{lvl}: no Cody/Alex") + eq("booking_slots" not in m and "list_members" not in m and "treasury_status" not in m, f"{lvl}: no studio tools") + eq("git_repo_create" in m and "product_create" in m, f"{lvl}: client tools") + eq(cl["m"]["sec"] == st["m"]["sec"], "sec level shared") + + print(f"{ok} ok, {bad} fail") + sys.exit(1 if bad else 0) + + +if __name__ == "__main__": + main()