docs/ARCHITECTURE.md: детальная архитектура харнеса (копия живого документа)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Simbiot Harness 2026-10-17 11:53:48 +00:00
parent 61ff48d37d
commit 74c5443244

303
docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,303 @@
# Архитектура Simbiot Harness (Коди)
17.10.2026 · копия документа https://claude.ai/code/artifact/195fd476-44e7-47cc-9093-2d41ea53c8c9 (живая версия — там, со схемой)
## Обзор
Simbiot Harness — ритмичный агент кооператива: он живёт тактами (секунда, минута, час, день), а в Telegram отвечает как Коди (@juno42bot). Главная метрика — долгосрочный AUM кооператива: каждое решение проверяется вопросом «добавит ли это ≥1% к AUM на горизонте год+».
Архитектура держится на пяти принципах:
- **Всё — события.** Единственный источник правды — append-only стор событий; состояние (read model) — проекция. Каждое событие подписано ed25519 и сцеплено с предыдущим.
- **Дешёвое решает раньше дорогого.** Детерминированные правила и локальные/облачные decision-модели отсекают лишнее до запуска LLM: гейт важности, фильтр адресации, выбор глубины памяти.
- **LLM — в изоляции.** Каждый LLM-тик — отдельный процесс с бюджетом шагов, таймаутами и стражем прав на каждом вызове инструмента.
- **Доверие — по личности, не по тексту.** Права тика определяет самый недоверенный вход его контекста.
- **Учится на исходах.** Ядро Коди собирает решения и их последствия, обучает маленьких учеников и допускает их в бой только при честной победе над базовой линией.
Куб 4×4×4 задаёт поведение одной таблицей политик: 4 круга доверия × 4 тика × 4 глубины памяти. Это не 64 программы, а один код и параметры.
```
Telegram ──► TG-поллер ──► Адресация ◄┄┄ Decision-модель (clef:27b → nimble)
Портал/агенты ──────────► │
▼
Гейт важности
▼
Круг доверия и память ◄┄┄ Память и ядро (индекс nomic, ученики)
▼
Человек (cli, TUI) LLM-воркер ──────► 9router и портал MCP
│ │
▼ ▼
Стор событий: подписи ed25519 · проекции · сжатие с архивом · каждый шаг пишет события
```
## Процессы и сервисы
Харнес — два долгоживущих процесса на одном сторе событий плюс короткоживущие LLM-воркеры; код — `/root/simbiot-harness`, данные инстанса — `/var/lib/simbiot-harness` (`SIMBIOT_HOME`).
| Процесс | Запуск | Что делает |
| --- | --- | --- |
| Лестница | `simbiot-harness.service` → `python -m harness.scheduler` | такты sec/min/hour/day в отдельных потоках, демон доставки и сирот, триаж, ночная тренировка ядра |
| TG-поллер | `simbiot-harness-tg.service` → `python -m harness.tg_inbox` | long polling @juno42bot: входящие, файлы, расшифровка голоса, кнопки |
| LLM-воркер | `harness/_llm_worker.py`, сабпроцесс на каждый LLM-тик | агент Strands с инструментами, страж прав, бюджет шагов |
| Дашборд | `simbiot-dashboard.service`, HTTP 8765 | доска задач и тиков, только чтение стора |
| CLI | `cli.py` (и обёртка `simbiot`) | команды агентов и людей: ответы, задачи, эскалации, ядро, память, доверие |
Каталоги инстанса (`/var/lib/simbiot-harness`):
| Путь | Содержимое |
| --- | --- |
| `db/harness.db` | стор событий + read model + журнал доставок (SQLite, WAL) |
| `env/.env` | секреты и флаги (600) |
| `audit/` | ключ ed25519 харнеса |
| `archive/` | сжатые события (jsonl.gz, подписаны) |
| `core/` | чемпионы учеников ядра + кеш эмбеддингов |
| `memory/` | индекс памяти Коди |
| `inbox/` | файлы из чатов |
Внешние зависимости:
| Система | Зачем |
| --- | --- |
| 9router (`127.0.0.1:20128`) | LLM для тиков, модель `auto` |
| Ollama Cloud | decision-модель `clef:27b` |
| Ollama локально (`127.0.0.1:11434`) | запасная decision-модель `nimble`, эмбеддинги `nomic-embed-text` |
| Портал cil.by (MCP) | звонки, реестры, канал агент↔агент |
| Telegram Bot API | чаты Коди |
| Deepgram | расшифровка голосовых |
| RAG `/root/rag` | поиск по базе знаний кооператива |
## Event Sourcing и криптоаудит
Всё состояние харнеса выводится из подписанной цепочки событий (`harness/es.py`, `harness/audit.py`); после сжатия 17.10 стор — 200 MB, полная проверка — около 30 с.
**Стор.** Таблица `events` (aggregate, aggregate_id, event_type, data, timestamp, version, sig, key_id, prev_sig). Уникальность `(aggregate, aggregate_id, version)` делает запись идемпотентной: тик `level:seq` не произойдёт дважды. Клейм задачи — атомарный (`claim_task_if_ready`, BEGIN IMMEDIATE), поэтому два уровня не возьмут одну задачу.
**Проекции.** `run_projections()` инкрементально строит read model (задачи, инбокс, эскалации, дайджесты, круги доверия, политика AUM) и сохраняет снапшот в `read_model`. Перед применением хвост проверяется подписями: подделка останавливает проекции.
**Подписи.** Каждое событие подписано ключом харнеса (ed25519, key_id `faf8d19d9aed71fe`), `prev_sig` ссылается на подпись более раннего. События до внедрения подписей помечены `genesis:<id>`, неподписанные — legacy-границы. Защита от чужого ключа: стор, уже подписанный одним ключом, другим не подписывается. Полная проверка — `cli.py verify`.
**Сжатие с подписью** (`harness/compact.py`, `scripts/compact-store.sh`). Шум сек-уровня старше часа уходит в архив:
1. Строки целиком, со всеми подписями, пишутся в `archive/events-<first>-<last>-<id>.jsonl.gz` (600).
2. Подписи удалённых строк, на которые ссылаются остающиеся, и маркеры `boundary:<id>` для legacy-границ попадают в мост `compaction_bridge`.
3. Подписанное событие `StoreCompacted` фиксирует sha256 архива, строк и моста.
4. Удаление идёт пачками по 20 000 строк без остановки сервисов; VACUUM — под остановленными писателями (секунды).
`verify` принимает мост, только если его хеш совпадает с подписанным `StoreCompacted`; архивы проверяются построчно (`cli.py compact --verify-archives`). Первое сжатие: 2 886 765 строк в архив (246 MB), 1389 → 200 MB.
Сек-уровень больше не шумит: `CycleTicked`/`ReflexObserved` пишутся heartbeat'ом раз в 60 с и в такты с событием (алерт, команды, каналы).
## Тактовая лестница
Четыре уровня тикают независимо, каждый в своём потоке (`harness/scheduler.py`, `harness/levels.py`); дорогой LLM-тик запускается, только если есть событие важнее порога уровня.
| Уровень | Интервал | LLM | Порог гейта | Задачи | Шагов на тик | Таймаут | Память по умолчанию |
| --- | --- | --- | --- | --- | --- | --- | --- |
| sec | 1 с | нет | — | — | — | — | — |
| min | 1 мин + по входящему | effort low, контекст 256k | critical | S | 30 | 480 с | D1 |
| hour | 1 ч | effort medium, 512k | warn | S, M, L | 300 | 2880 с | D2 |
| day | 1 д | effort high, 1M | warn | S, M, L | 10 000 | таймаут уровня | D3 |
**sec — рефлекс без LLM.** Телеметрия и алерт нагрузки, мгновенные команды бота (`/лестница`, `/статус`, `/задачи`), каналы портала и агент↔агент.
**Гейт важности.** Сигналы: входящее без ответа и готовая задача — critical; эскалации — по их severity; высокая нагрузка — warn. Ниже порога тик закрывается детерминированно (`GateSkipped` + заглушка-дайджест). Серая зона (только warn/info) спрашивает decision-модель — сейчас в режиме shadow.
**Тик по входящему.** Демон раз в секунду смотрит хвост стора; через 3 с тишины после `MessageReceived` или `MessageTranscribed` запускает внеочередной мин-тик в слот следующей минуты. Ответ в Telegram приходит за 10–40 с вместо ~80 с медианы до нуджа.
**Батч задач.** Уровень берёт все ready-задачи своих классов в бюджете контекста: из 9 старейших первой ставится самая ценная для AUM (decision-модель), остальные — FIFO. В один батч попадают задачи одного круга доверия, и уровень берёт только обслуживаемые круги.
**Эскалация недоделанных.** «Бюджет исчерпан» или смерть воркера возвращают задачу в очередь; после 2 захватов на уровне она растёт до размера, которого этот уровень не берёт (S → M, к часу). До этого S-задача могла браться минутным уровнем 33 раза подряд. Задачи про внешние системы (портал, сервер, деплой, UI, git) сразу уходят часу — правило головы `task_fit`.
Занятый уровень поглощает тик (`TickAbsorbed`), запущенная задача не прерывается, наверх идут только дайджесты и эскалации (`cli.py escalate`). Демон доставки возвращает задачи-сироты после рестартов и доставляет результаты в чат идемпотентно (журнал доставок).
## LLM-воркер
Каждый LLM-тик — отдельный процесс (`isolated_runner.py` → `_llm_worker.py`) со свежим asyncio-рантаймом Strands: падение или таймаут не роняют лестницу, а вся группа процессов добивается по таймауту уровня.
**Вход воркера (payload):** мандат уровня, собранный контекст тика, модель и effort, флаг shell, круг доверия тика. Результат возвращается JSON-файлом; трассу (`InvocationStarted`, `ToolCalled`, `ToolResult`, `BudgetSpent`) воркер пишет в стор сам и инкрементально, чтобы таймаут не съел учёт расходов.
**Инструменты:**
| Инструмент | Ограничение |
| --- | --- |
| `shell`, `read`, `write`, `edit` | только уровни с `can_shell`; в кругах ≥ 2 — страж прав |
| `rag_lookup` | 1 запрос на тик, k ≤ 5, таймаут 20 с |
| `portal_mcp` | 4 вызова на тик, деньги-тулы запрещены по умолчанию |
| `cli.py …` через shell | основной способ действовать: `tg-reply`, `task`, `complete`, `escalate`, `tg-deliver-file`, `decide` |
**Хуки на каждом вызове** (`BeforeToolCallEvent`, до исполнения):
1. **Страж прав** — `trust.check_tool`: запрещённый вызов отменяется (`cancel_tool`) и пишется `TrustDenied`.
2. **Бюджет шагов** — сверх лимита уровня вызовы отменяются, цикл завершается итоговым текстом; задачи без `complete` возвращаются или эскалируются.
3. **Трасса** — `ToolCalled`/`ToolResult` с trace_id, уровнем и длительностью.
**TTFT-сторож.** Если апстрим 9router принял запрос, но за 45 с не прислал первый токен, стрим закрывается и делается новый вызов (до 3 попыток). Начавший отвечать стрим не трогается.
**Окружение.** `SIMBIOT_TICK_CIRCLE` наследуют shell и `cli.py`: задачи из тика штампуются его кругом, а `cli.py trust set` из тика отказывает.
## Telegram-канал
Коди в Telegram — бот @juno42bot (id 7245454391); поллер пишет входящие в стор, а отвечает минутный (реже часовой) тик через `cli.py tg-reply`.
**Приём** (`harness/tg_inbox.py`). Long polling (`message`, `edited_message`, `callback_query`). Отправитель вне `SIMBIOT_TG_ALLOWED` → `MessageIgnored(not_allowed)`. Документы и фото сразу сохраняются на диск, голосовые расшифровываются. Сохраняется, на чьё сообщение сделан reply и упомянут ли бот. Правка уже принятого сообщения не становится новым входящим.
**Адресация в группах** (`harness/addressing.py`) — до гейта, без LLM:
1. Личка, портал, агенты — всегда адресовано.
2. В группе «да» без модели: `@juno42bot`, reply на сообщение бота, звательное обращение («Коди, …», «…, Коди», «Коди!»).
3. «Нет» без модели: reply на сообщение другого человека.
4. Серая зона — decision-модель: «кому адресовано — боту, человеку или группе», P(бот) ≥ 0.5. Разговор О Коди в третьем лице обращением не считается. Модель недоступна — бот молчит.
Вердикт: `MessageTriaged{addressed}` + 👀 или `MessageIgnored(not_addressed)` — тогда ни один уровень его не видит. В тени рядом работает ученик ядра `addressing`.
**Ответы.** В группе — reply'ем на последнее сообщение батча (`allow_sending_without_reply`), в личке — без цитаты. Сводный ответ на несколько сообщений — `tg-reply m-1,m-2 -c`. Правило «сначала ответ, потом работа»: если нужно больше ~30 с, сначала короткий ответ, итог — `tg-reply <id> --followup` в тот же чат (идемпотентно по тексту).
**Реакции.** 👀 ставится при приёме в личке и триажем в группе — только на адресованное боту. Параметр Bot API — `reaction` (массив); до 17.10 ошибка в имени параметра делала реакцию невидимой.
**Журнал исходящих.** Каждое сообщение бота → `OutboundMessage` (chat:message_id → полный текст). Reply на сообщение бота получает `reply_to_full` (до 4000 символов вместо 300 от Telegram), а тик видит блок «ЭТО ОТВЕТ НА ТВОЁ СООБЩЕНИЕ». Собственные рассылки бота за 48 ч попадают в память тика.
**Кнопки.** `core:…` — «Это было мне?» для ученика ядра; `trust:…` — «Разрешить полные права», нажать может только владелец.
## Decision-модель
Решения «выбрать один из нескольких» и «да/нет» принимает не LLM, а decision-модель Ollama `/v1/systemone` (`harness/decision.py`): облачная `clef:27b` отвечает за 0.6–0.9 с, запасная локальная `nimble` (qwen3.5-9B Q8_0, CPU) — за 10–30 с.
**Контракт.** `choice` — варианты объектом `{ключ: описание}`, ответ — ключ и распределение вероятностей; `noul` — P(«да»). Шортлист строго ≤ 9 вариантов: по замерам systemone-bench точность падает со 100% на 6 до 50% на 10. `confidence` — концентрация распределения, а не вероятность правильности.
**Надёжность.** Бэкенды опрашиваются по порядку, у каждого свой предохранитель: облако — таймаут 30 с, после сбоя пауза 60 с; локальная — 90 с и 5 минут. Ответы детерминированы, поэтому кешируются на час по хешу вопроса и состояния. Каждый реальный вызов → `DecisionMade` (бэкенд, модель, латентность, применено ли). Модель недоступна — вызывающий берёт свой детерминированный фолбэк.
| Потребитель | Вопрос | Режим | Фолбэк |
| --- | --- | --- | --- |
| адресация в группах | кому адресовано сообщение | on | молчать |
| порядок батча задач | какая из 9 старейших ценнее для AUM | on | FIFO |
| тема инициативы | что сейчас полезнее предложить | on | по кругу |
| серая зона гейта | нужен ли дорогой LLM-тик | shadow | порог |
| агент в тике | `cli.py decide -q … -o ключ=описание` | on | — |
Режимы: `SIMBIOT_DECISION=off|shadow|on`, точечно `SIMBIOT_DECISION_<ПОТРЕБИТЕЛЬ>`; ключ облака — `SIMBIOT_DECISION_API_KEY` в `.env` инстанса. Статус и статистика — `cli.py decide`, `cli.py decide --stats`.
## Ядро Коди
Ядро (`harness/kodi_core.py`) — цикл самообучения по образцу systemone-bench (engine42/router42): учитель и правила решают, маленький ученик учится на их решениях и реальных исходах и получает право решать сам только после честной проверки. Сейчас у ядра три головы.
| Голова | Решение | Метки | Режим | Состояние на 17.10 |
| --- | --- | --- | --- | --- |
| `addressing` | отвечать ли в группе | учитель (вес 1), реакция чата (3), кнопки владельца (5) | shadow | чемпион на 262 примерах |
| `task_fit` | влезет ли S-задача в мин-тик | исходы задач из истории | on (правило «внешние системы») | ML-чемпион снят: не бил базу; действует правило (точность 0.74) |
| `depth` | нужна ли глубокая память | решения правил (1), нехватка памяти по трассе (3) | shadow | копит данные, нужно 40 примеров |
**Ученик.** Признаки — эмбеддинг `nomic-embed-text` (локально, ~70 мс, с дисковым кешем) плюс флаги головы. Модель — логистическая регрессия на numpy (Adam, L2): на сотнях примеров она устойчивее MLP. kNN-память даёт «новизну»; её пороги калибруются при обучении.
**Роутер.** Знакомая ситуация и |p − 0.5| ≥ 0.4 → решает ученик; незнакомая и учитель сомневается → вопрос владельцу кнопками (≤ 5 в сутки); иначе — учитель. В shadow ученик только пишет `CorePrediction`.
**Честный допуск** (ночная тренировка в дневной тик, `cli.py core train`):
1. Чемпион помнит `trained_upto`; с претендентом сравнивается только на свежих данных (≥ 10), которых он не видел.
2. Претендент обязан бить базовую линию — константу, равную доле класса.
3. Чемпион, который на свежих данных хуже базы, снимается (`CoreModelRetired`) — ученик молчит.
4. Смена чемпиона — подписанное `CoreModelPromoted` с sha256 файла; подменённый файл не грузится; откат — `cli.py core rollback --head <голова>`.
**Рефлексия.** Раз в сутки: ≥ 3 расхождений адресации с метками человека/чата или ≥ 3 S-задач, не влезших в мин-тик, → задача часу с автодоставкой: разобрать случаи и предложить владельцу правило. Первая рефлексия (17.10) привела к правилу `task_fit` «внешние системы → сразу к часу».
**Недельный отчёт.** По понедельникам владельцу в личку, без LLM, идемпотентно по неделе (`cli.py core report`).
## Куб 4×4×4: круги доверия
Поведение задаёт одна таблица `POLICY` (`harness/trust.py`): круг доверия определяет инструменты, уровни времени и потолок глубины памяти. Вторая ось — тики (sec/min/hour/day), третья — глубина D0–D3.
| Круг | Кто | Shell | Файлы | Портал MCP | Уровни | Память до |
| --- | --- | --- | --- | --- | --- | --- |
| 1 владелец | Alex, локальный терминал, система (тик без внешних входов) | полный | полный доступ | всё (деньги — по флагу) | все | D3 |
| 2 правление | Vlad, Yura | `cli.py`: ответы, задачи, эскалации, доставка файлов, обзор доски | чтение docs/out/sites/artifacts и присланного | звонки, реестры, фичи; без казны и member_create | min, hour, day | D3 |
| 3 свои | участники портала, MCP-агенты, партнёры | `cli.py`: ответы, задачи, эскалации | нет | запись на звонок, проекты, фичи | min, hour | D2 |
| 4 остальные | прочие Telegram-пользователи | `cli.py`: только ответ и эскалация | нет | нет | min | D1 |
**Как определяется круг.** Только по проверенной личности: Telegram user_id, канал MCP-агента или портала, локальный терминал. Текст сообщения прав не даёт. Меняет круг только человек на сервере (`cli.py trust set <субъект> <1-4>`) — из LLM-тика это запрещено и стражем, и самим CLI.
**Круг тика** = самый недоверенный вход его контекста (входящие + задачи батча). Задачи наследуют круг тика, в котором поданы: просьба правления не исполняется позже часовым уровнем с правами владельца.
**Страж** проверяет каждый вызов инструмента до исполнения. Shell в кругах 2–4 — одна команда `cli.py` из белого списка круга, без `;` `&&` `|` `>` `$()` и обратных кавычек; файлы с секретами закрыты всегда. Задача правления упёрлась в права → владельцу приходят кнопки «Разрешить / Нет»; «Разрешить» даёт задаче круг 1.
Вживую проверено на реальном воркере в круге 2: `cat /etc/hostname` отменён до исполнения, `cli.py decide --stats` выполнился.
## Динамическая память D0–D3
Перед каждым LLM-тиком харнес выбирает, сколько собственной истории подгрузить (`harness/memory.py`, `SIMBIOT_MEMORY=on`); индекс на 17.10 — 1650 разговоров, 270 итогов задач, 91 дайджест.
| Глубина | Что в контексте | Когда |
| --- | --- | --- |
| D0 | только текущие входящие | все реплики односложные: «ок», «спасибо» |
| D1 | + 12 последних реплик чата + свои сообщения за 48 ч | минутный тик по умолчанию |
| D2 | + до 8 эпизодов из своей истории по смыслу (≤ 5000 символов) | час; «помнишь», «вчера», «как договаривались»; задачи в батче; reply на сообщение бота старше 48 ч |
| D3 | + до 15 записей архива (≤ 12 000 символов) | день; «за месяц», «что ты делал», «итоги», «план на» |
Потолок задаёт круг: «свои» — до D2, «остальные» — до D1.
**Индекс** (`<home>/memory/index.sqlite`) инкрементально догоняет стор перед поиском (~3.5 с, только чтение):
- `conv` — сообщение + ответ Коди (в батче — ответ головы);
- `out` — собственные рассылки бота, не ответы;
- `task` — задача и её итог;
- `digest` — содержательные дайджесты часа и дня (заглушки отсеиваются).
**Приватность.** У каждой записи — круг: сказанное в чате видно всем его участникам, поэтому круг записи = самый недоверенный участник чата (личка владельца — 1, «Правление» — 2); задача — её круг; дайджест — 1. Поиск отдаёт только записи с кругом ≥ круга тика: тик с сообщением правления не поднимет личку владельца. Круги читаются прямо из событий `TrustSet`, чтобы отставшая проекция не дала записи слишком широкий круг.
**Релевантность.** Эмбеддинги `nomic-embed-text` (`search_document` / `search_query`). Калибровка на прод-индексе: релевантное — 0.78–0.88, посторонний запрос — до 0.75. Порог 0.76 и окно 0.07 от лучшего совпадения; свежий разговор того же чата (уже в D1) не дублируется.
**Обучение выбора.** Решение о глубине → `DepthChosen` с причинами и признаками нехватки памяти по трассе: агент полез в базу знаний или в свой стор, ответил «не помню». Это сырьё для головы ядра `depth`.
## Конфигурация, CLI, деплой и тесты
Настройки читаются из env процесса, затем из `/var/lib/simbiot-harness/env/.env`; новые подсистемы по умолчанию выключены или в shadow, а на проде включаются строкой в `.env`.
| Переменная | Прод | Что делает |
| --- | --- | --- |
| `SIMBIOT_DECISION` | on | decision-модель (гейт — shadow) |
| `SIMBIOT_DECISION_API_KEY` | задан | облако `clef:27b`, запасная — локальная `nimble` |
| `SIMBIOT_MEMORY` | on | индекс памяти и блоки D2/D3 |
| `SIMBIOT_CORE_ADDRESSING` | shadow | ученик адресации |
| `SIMBIOT_CORE_TASK_FIT` | on | сразу к часу задачи про внешние системы |
| `SIMBIOT_CORE_DEPTH` | shadow | ученик глубины памяти |
| `SIMBIOT_TG_ALLOWED` | Alex, Yura, Vlad | кто вообще обрабатывается |
| `SIMBIOT_MIN_NUDGE` | on | тик по входящему |
| `SIMBIOT_TASK_ATTEMPTS_PER_LEVEL` | 2 | попыток до эскалации размера |
| `SIMBIOT_MAX_TOOL_CALLS_<УРОВЕНЬ>` | min 30, hour 300 | бюджет шагов тика |
**CLI** (`cli.py`, для людей и агентов в тике):
| Группа | Команды |
| --- | --- |
| чат | `tg-reply [--followup]`, `tg-notify -k`, `tg-deliver-file`, `tg-list`, `tg-seen` |
| задачи | `task`, `tasks`, `complete`, `block`, `promote`, `requeue`, `close` |
| уровни | `ladder`, `status`, `escalate`, `escalations`, `escalation-ack` |
| решения | `decide`, `decide --stats` |
| ядро | `core status / train / backfill / feedback / reflect / report / rollback / retire [--head]` |
| память | `memory sync [--all] / stats / search "…" --circle N` |
| доверие | `trust list`, `trust set <субъект> <1-4>` (только человек) |
| стор | `events`, `verify`, `compact [--dry-run / --vacuum / --verify-archives]` |
**Деплой.** Код — ветка `master` в `git.cil.by/symbiot/simbiot-harness`. Перезапуск лестницы — только `scripts/safe-restart.sh`: он ждёт, пока закончится идущий LLM-воркер, чтобы задача не застряла в running. TG-поллер перезапускается напрямую: Telegram копит апдейты. Сжатие стора — `scripts/compact-store.sh`.
**Тесты.** `tests/test_*.py` — скрипты (`./venv/bin/python tests/<имя>.py`), каждый со своим временным стором (`SIMBIOT_DB`). Внешние системы подменены: фейковый `/v1/systemone`, эмбеддинги «мешок слов», заглушки Telegram. Перед каждым коммитом гоняется полный набор и сравнивается с предыдущим прогоном. Несколько старых тестов (`test_tg_commands`, `test_reply_routing`, `test_portal_mcp`, `test_tool_budget`) падают и на чистом коде — они зависят от живого состояния.
## Известные ограничения и дорожная карта
Главное открытое место — косвенные каналы между кругами и автономность часового уровня в круге 1.
**Ограничения:**
- **Выжимки нижних уровней считаются внутренними.** Пересказ сообщений правления в дайджесте попадает в часовой и дневной тик с кругом 1.
- **Часовой уровень в круге 1 может менять код сам.** Рефлексия 17.10 закоммитила правило вопреки «код не меняй».
- **Адресация ошибается на коротких репликах без контекста.** «Выбирай любую.», сказанное боту, фильтр пропустит; выход — «Коди, …» или reply.
- **Ученики пока слабые.** `addressing` на свежих данных совпадает с учителем в 71% и уверен в ~10% случаев; ML-версия `task_fit` не бьёт базу; `depth` ещё не обучался.
- **Расход токенов часового уровня** — в среднем 46k токенов на вызов, до 205 вызовов инструментов на тик. Лимит 300 оставлен по решению владельца.
- **Уровни неделя/месяц/квартал/год** пока не работают; куб сознательно сжат до 4 тиков.
**Дальше:**
- [ ] Рефлексия и другие задачи «только предложить» — в ограниченном круге, внедрение после «да» владельца.
- [ ] Круг приватности для дайджестов — закрыть косвенный канал.
- [ ] Команда в боте для смены кругов (только владелец), чтобы не ходить на сервер.
- [ ] Решение по ученику адресации по цифрам недельного отчёта.
- [ ] Режим on для серой зоны гейта — по статистике shadow.
- [ ] Уровни неделя и месяц на основе D3 и дайджестов дня.