Профиль агента: studio (Коди, мандаты без изменений) | client — агент клиента Shkor Labs Autopilot со своим именем, владельцем и инструментами тарифа (SIMBIOT_PROFILE, SIMBIOT_AGENT_DISPLAY_NAME, SIMBIOT_OWNER_NAME, SIMBIOT_COMPANY) + тест

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Simbiot Harness 2026-10-18 20:14:26 +00:00
parent 3d8b743d7a
commit 578856081e
3 changed files with 136 additions and 2 deletions

View file

@ -2,7 +2,7 @@
Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель. Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель.
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field, replace
import os import os
SEC = 1 SEC = 1
@ -15,6 +15,22 @@ QUARTER = 91 * DAY
YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания 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: def _cfg_model(default: str = "auto") -> str:
"""SIMBIOT_MODEL (визард первого запуска, t-261002193628-1d1e5f): """SIMBIOT_MODEL (визард первого запуска, t-261002193628-1d1e5f):
одна модель для всех LLM-уровней вместо auto — env сильнее .env одна модель для всех 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 — поддомен <slug>.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. Рядом в панели <slug>.ui.shkor.com "
"владельцу доступны OpenCode и Claude Code — для разработки и правок кода "
"предлагай их. ")
return {
"min": (f"Ты — минутный пульс харнеса Simbiot. {ident}"
"Все неотвеченные сообщения владельца приходят тебе ОДНИМ батчем (секция "
"входящих) — обработай его целиком за тик: один сводный ответ "
"(tg-reply <id1>,<id2>,… -c \"текст\") или отдельные реплаи, но ни одно "
"сообщение не должно остаться без ответа. Тон: кратко, тепло, на языке "
"владельца. СНАЧАЛА ОТВЕТ, ПОТОМ РАБОТА: если работа дольше ~30 с — первым "
"делом короткий tg-reply («понял, делаю: …»), итог — cli.py tg-reply <id> "
"--followup \"итог\". Мелкие задачи до 5 минут делай сам; большие подавай "
"задачей (cli.py task -a — результат сам придёт в чат) и скажи об этом. "
"Не создавай задачи из служебных сообщений («тест», «привет»). "
+ _CLIENT_COMMON),
"hour": (f"Ты — часовой поток харнеса Simbiot. {ident}"
"Дайджест часа: что сделано по проекту владельца, статусы задач, батч "
"задач S/M/L строго в порядке очереди. Эскалации нижних уровней (секция "
"ЭСКАЛАЦИИ) — твоя зона: разберись и закрой cli.py escalation-ack <id>. "
"Выбор из 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 — включаются по мере готовности # v0.2+: week/month/quarter/year — включаются по мере готовности
FUTURE_LEVELS = ("week", "month", "quarter", "year") FUTURE_LEVELS = ("week", "month", "quarter", "year")

View file

@ -18,6 +18,7 @@ from datetime import datetime, timezone, timedelta
from . import es from . import es
from .levels import LEVELS from .levels import LEVELS
from . import levels as _levels
from . import paths as _paths from . import paths as _paths
from . import runtime from . import runtime
from . import commands 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 [] own = _own_messages(chat_ids, replied_hist) if depth >= 1 else []
if own: if own:
parts.append("ТВОИ НЕДАВНИЕ СООБЩЕНИЯ В ЭТОТ ЧАТ (сам отправил: итоги задач, " parts.append("ТВОИ НЕДАВНИЕ СООБЩЕНИЯ В ЭТОТ ЧАТ (сам отправил: итоги задач, "
"уведомления, предложения — Alex может отвечать на них):\n" f"уведомления, предложения — {_levels.OWNER} может отвечать на них):\n"
+ "\n".join(own)) + "\n".join(own))
if replied_hist: if replied_hist:
replied_hist = sorted(replied_hist, key=lambda x: x.get("ts") or "")[-12:] replied_hist = sorted(replied_hist, key=lambda x: x.get("ts") or "")[-12:]

51
tests/test_profile.py Normal file
View file

@ -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()