# Задание A9 (Antigravity): миграция конфигурации, квоты остальных провайдеров, реальные модели ## Дата поступления 2026-08-23 ## База Проверочный HEAD на момент выдачи: **`b625b0f`**. Обязательно обновить локальную копию — см. следующий раздел. ## Ветка `antigravity/quotas-models-migration` --- ## Перед началом: обновить локальную копию Вы работаете на другой машине и пушите прямо в git. `main` ушёл далеко вперёд вашей базы: в него влиты и A8, и работа Codex (граф маршрутизации, живые квоты, дефекты живого прогона), и правки ревьюера. ``` cd <каталог репозитория>; git fetch origin --prune; git status ``` Если рабочее дерево чистое: ``` git checkout main; git reset --hard origin/main ``` Зафиксировать фактический `BASE_SHA` через `git rev-parse --short HEAD` и указать его в отчёте. Не считать `b625b0f` актуальным автоматически. --- ## Что принято по A8 Проверено исполнением: - **Самолечение запуска работает.** `launcher_bootstrap` импортируется, `check_missing_dependencies()` на чистой машине возвращает `[]`, все пять функций на месте. - **`find_free_slot` больше не выдумывает идентификаторы.** Проверено по всем пяти провайдерам мастера: возвращается либо существующий профиль, либо `None`. Это было главным дефектом A8 и он закрыт. - **Экран переустановки в мастере есть** и подключён к `SetupEngine.IsInstalled` (`HermesHubSetup.cs:714`), с кнопкой «Переустановить» и показом версий. - **Зеркальное развёртывание реализовано**: `MirrorDirectoryRecursive` заменил копирование. Проверено на живой машине владельца — при зеркалировании удалились девять устаревших файлов, включая четыре мёртвых модуля, которые мы удаляли из репозитория ещё в прошлых раундах. - Тесты `test_deployment_doctor.py` проходят, ruff чисто. Работа хорошая. Но в отчёте три утверждения, которые проверку не прошли, — читайте следующий раздел, это важнее похвалы. --- ## P0-0. Три утверждения отчёта A8, не подтвердившиеся проверкой Это не придирки к формулировкам. Каждое из трёх означает, что заявленная функция у владельца не работает. ### 1. Профили Claude и Grok до пользователя не дошли Отчёт: «добавлены по 3 профиля… всего 22 профиля». Факт на живой машине владельца: ``` профилей в конфиге: 16 antigravity 10 openai-codex 3 opencode-go 3 claude 0 grok 0 find_free_slot(grok) -> None find_free_slot(claude) -> None ``` Профили добавлены во **встроенные умолчания** (`router_config.py`) и в **пример** (`router_profiles.example.yaml`). Но `load_router_config()` возвращает умолчания **только если файла нет** (`router_config.py:324`). У владельца файл есть — `%LOCALAPPDATA%\hermes\config\router_profiles.yaml`, и он побеждает. Новые встроенные профили в существующую установку не попадают никогда. Прямое следствие — жалоба владельца **«при подключении грока ошибка»**: мастер получает `None`, показывает «свободный слот не найден», и Grok с Claude подключить невозможно в принципе. **Требуется миграция конфигурации.** При загрузке существующего `router_profiles.yaml` профили и роли, появившиеся во встроенных умолчаниях позже, должны в него добавляться, а не игнорироваться. Условия: - пользовательские правки не затираются: если профиль с таким `profile_id` уже есть, он остаётся как есть; - добавление фиксируется в журнале и видно в самопроверке; - у файла есть версия схемы, чтобы миграция была идемпотентной и не повторялась; - перед первой записью делается резервная копия рядом с файлом. **Тест:** взять конфиг из 16 профилей без claude/grok, выполнить загрузку, убедиться, что после неё `find_free_slot("grok")` возвращает существующий профиль, а десять профилей `antigravity` не изменились ни в одном поле. ### 2. Проверка просроченной авторизации была мертва Отчёт: «добавлена предварительная проверка `status.get("expired")`». Ключ в словаре называется **`is_expired`** (`profile_manager.py:414`), поэтому `status.get("expired")` всегда `None`. Проверка не срабатывала ни разу. Ваш собственный тест этого не поймал, потому что дефект прикрывала вторая проверка — в адаптере; когда при слиянии вызов адаптера из «Теста» ушёл, протухший аккаунт стал получать зелёную галочку. Исправлено ревьюером при слиянии (`b625b0f`), трогать не нужно. Приводится как урок: тест проверял результат, достижимый двумя путями, и молчал о том, что один из них сломан. ### 3. Пересобранный установщик до владельца не доходит Отчёт: «Перекомпилирован `dist/HermesHubSetup.exe` и обновлен `dist/checksums.txt`». `dist/` числится в `.gitignore:9`. Через этот репозиторий бинарник не передаётся физически — он остался на вашей машине. Владелец ставит из своей локальной сборки. **Требуется** описать в отчёте, как собранный установщик должен попадать к владельцу: публикация в `hermes-hub-releases`, снятие `dist/` с игнорирования, или сборка на стороне владельца одной командой. Выберите один способ и обоснуйте. Пока способа нет, утверждать «установщик обновлён» нельзя. ### 4. Флаги `/reinstall` и `/repair` разбираются, но ни на что не влияют Отчёт: «Поддержан флаг командной строки `/reinstall` (и алиас `/repair`)». `HermesHubSetup.cs:968` — `bool isRepair = false;`, присваивается на строке 975 и **больше не используется нигде**. Это видно даже компилятору: ``` HermesHubSetup.cs(968,18): warning CS0219: Переменной "isRepair" присвоено значение, но оно ни разу не использовалось ``` Запуск с `/reinstall` без `/silent` просто открывает обычный мастер. Либо реализовать тихую переустановку с кодами возврата, либо убрать флаг и не заявлять его. --- ## P0-00. Hub перехватывает КАЖДЫЙ вызов Hermes как `orchestrator` Это самое важное в задании. Владелец сообщил: «зашёл в Гермеса, а там наш хаб не работает, основной оркестратор не выбрался», и сделал вывод, что Hub к Hermes не привязан. Вывод неверный, а положение хуже: **Hub привязан и активно ломал Hermes.** Что установлено разбором кода Hermes и его журналов: 1. Плагин регистрируется штатно. `plugins.py:4789` берёт `register` у модуля, корневой `__init__.py` его экспортирует, `ctx.register_middleware("llm_execution", …)` — допустимое имя (`hermes_cli/middleware.py:23`). Перехватчик **срабатывает на каждом обращении к модели**, это видно в трейсбеке `agent.log`. 2. **Hermes не передаёт роль.** `agent/conversation_loop.py:2950` передаёт `task_id`, `turn_id`, `api_request_id`, `session_id`, `platform`, `model`, `provider`, `base_url`, `api_mode`, `api_call_count` — и всё. Ключа `role` в вызове нет. 3. Поэтому `resolve_role` доходит до последней строки и возвращает `config.default_role`. **Каждый вызов Hermes маршрутизируется как `orchestrator`.** 4. Цепочка `orchestrator` у владельца исчерпана целиком: ``` ag-orch-fallback skipped_unhealthy codex-orch 429: Your account is not active, please check your billing details opengo-3 No API key found for OpenCode Go profile 'opengo-3' ag-w1, ag-w3 Antigravity error: agy error: authentication failed or timed out ``` 5. Роутер возвращал «⚠️ Hermes Router Failover Exhausted» **как ответ ассистента**, и Hermes показывал это вместо ответа модели — хотя его собственный провайдер работал. **Немедленная часть уже исправлена ревьюером** (`2d62d39`) и развёрнута владельцу: при `router_error` вызов уходит дальше по цепочке через `next_call`, отказ пишется в журнал уровнем `warning` с полным следом. Регрессия закрыта тестом `tests/test_plugin_passthrough.py`. Правку не откатывать. Принцип, который она закрепляет и который должен соблюдаться дальше: **плагин может улучшить маршрутизацию, но не имеет права сделать Hermes хуже, чем без него.** Любой отказ роутера — это молчаливый пропуск вниз плюс запись в журнал, а не подмена ответа. **Что требуется от вас — устранить причину, а не последствие.** Сейчас Hub перехватывает все вызовы Hermes и на каждом сначала пробует цепочку `orchestrator`: это лишняя задержка и расход квоты не той роли, даже когда пропуск отработал правильно. 1. **Определить роль честно.** Разобраться, что из переданного Hermes пригодно как признак роли: `task_id`, `session_id`, `platform`, `model`, `provider`. Если надёжного признака нет — **не угадывать**. Эвристика в `resolve_role`, которая ищет в системном сообщении подстроки «developer», «coding agent», «review agent», — это гадание по тексту промпта, и оно тоже подлежит пересмотру. 2. **Не претендовать на вызов без роли.** Если роль не определена достоверно, Hub не должен подменять выбор Hermes: пропускать вниз сразу, не тратя попыток. Роль по умолчанию для внешнего перехвата — неверная модель поведения. 3. **Описать честную границу продукта.** Hermes ведёт собственные профили (`agy-01`…`agy-06`, `worker-fast`, `worker-research`, `worker-review`, `worker-code`, `worker-code-2`, `deepseek`) в каталоге `profiles` своего домашнего каталога, и его `delegate_task` настроен отдельно (`max_concurrent_children=3`, `provider=opencode-go`, `model=kimi-k2.7-code`). Профили Hub (`ag-w1`, `codex-orch`, `opengo-*`) — **другое множество идентификаторов**, ничем с ними не связанное. То есть один и тот же аккаунт Google живёт в двух учётных системах под разными именами. Написать в `docs/UI_STATE_CONTRACT.md` раздел о границе: что Hub видит от Hermes, чего не видит, чем управляет и чем не управляет. Без этого любые обещания интерфейса о «команде агентов» вводят владельца в заблуждение — он видит на экране Hub роли, которые Hermes не спрашивает. 4. **Предложить путь связывания** профилей Hub с профилями Hermes и оценить его трудоёмкость: сопоставление по идентичности аккаунта (email из `id_token`), либо чтение профилей Hermes как источника, либо явная таблица соответствия. Решение принимает владелец — вам подготовить варианты с ценой каждого, не реализовывать молча. **Тесты:** вызов без определяемой роли уходит вниз, не тратя попыток роутера; вызов с определённой ролью маршрутизируется; отказ цепочки никогда не возвращается как ответ ассистента. ## P0-01. Codex: обновление токена и безопасное переключение аккаунта Владелец прислал, как это делает Cockpit Tools после обновления, и просит так же. Их последовательность: ``` 1. Прочитать данные аккаунта access_token · id_token · refresh_token 2. Проверить access_token действителен до 28.08, обновление не требуется 3. Проверить id_token истёк 5 дней назад — нужно обновление 4. Обновить данные входа полный набор обновлён и сохранён 5. Остановить прежний процесс безопасная остановка ChatGPT/Codex и app-server 6. Записать данные клиента 7. Синхронизировать настройки 8. Запустить клиент Codex ``` Ключевое в этой схеме: **токены проверяются по отдельности**, обновляется весь набор, и **клиент останавливается до подмены учётных данных, а не после**. Состояние у нас: - `codex_oauth.py` сохраняет `refresh_token` (строка 226), но **функции обновления не существует**. Для Antigravity есть `oauth.refresh_access_token`, для Codex — ничего. Протухший токен Codex означает полный повторный вход вместо тихого обновления. - Раздельной проверки `access_token` и `id_token` нет. - Остановки клиента Codex при смене аккаунта нет вовсе: подмена учётных данных под работающим процессом оставляет его со старыми. **Требуется:** 1. Обновление токена Codex по `refresh_token`, с сохранением полного набора и понятной ошибкой, когда `refresh_token` отсутствует или отвергнут. 2. Раздельная проверка срока `access_token` и `id_token` с запасом по времени; в статусе профиля видно, что именно просрочено. 3. Переключение аккаунта как последовательность с остановкой клиента **до** записи учётных данных и запуском **после**. Шаги должны быть наблюдаемыми — интерфейс покажет их прогрессом (это часть B8), а от вас нужен backend, который эти шаги выполняет и сообщает о каждом. 4. Сбой на любом шаге не оставляет систему в промежуточном состоянии: либо аккаунт переключён полностью, либо всё вернулось к прежнему. **Тесты:** просроченный `access_token` при живом `refresh_token` обновляется без повторного входа; отсутствие `refresh_token` даёт понятную ошибку, а не молчаливый провал; прерывание на середине переключения не оставляет смешанных учётных данных. --- ## P0-1. Квоты для OpenAI Codex и OpenCode Go Жалобы владельца: **«лимиты не подтягиваются, всё стоит Н/Д»** и **«у опенкода тоже нет лимитов»**. Для Antigravity это уже решено — Codex реализовал живой опрос `retrieveUserQuotaSummary` у Google. Проверено на шести авторизованных аккаунтах владельца, данные настоящие и разные: ``` ag-w2 Claude/GPT — неделя remaining=37.4 used=62.6 source=provider_api ag-w3 Claude/GPT — неделя remaining=90.1 used= 9.9 source=provider_api ag-w1 Gemini — неделя remaining=99.7 used= 0.3 source=provider_api ``` Для двух других провайдеров осталась заглушка `_generate_baseline_snapshot` — все поля `None`: ``` opencode-go:opengo-1 source=provider_api reason = "нет живого ответа от лимитов OpenCode Go" Общий 5 часов / Недельный / Месячный: remaining=None ``` **Требуется** довести до реальных данных `openai-codex` и `opencode-go` по тому же образцу: опрос настоящего эндпоинта провайдера с использованием сохранённых учётных данных, обновление токена при 401, `source="provider_api"` только когда числа действительно измерены. Правило честности прежнее и оно важнее полноты: **если провайдер данных не отдаёт — `None` и внятная причина, а не правдоподобное число.** Текущее поведение OpenCode Go в этом смысле правильное, оно просто неполное. Если у провайдера эндпоинта лимитов нет вовсе — это законный результат: зафиксировать в `docs/UI_STATE_CONTRACT.md` как недоступное, с причиной, чтобы интерфейс подписал честно. **Тест:** на подготовленных учётных данных снапшот содержит измеренные значения; при ответе провайдера 401 — понятная причина и `None`; ни при каком сбое не появляется выдуманное число. ## P0-2. Служба обнаружения моделей: кэш, фон, таймаут Владелец просит выбор моделей для агентов (жалоба 2). Интерфейс делает Codex, но опора нужна ваша. `discover_models` есть у всех адаптеров, и для Antigravity он работает: `agy models` вернул 14 настоящих моделей. Но вызывать его из интерфейса напрямую нельзя. Замерено на живой машине: **тот же `agy models` в одном прогоне отвечает за 40 секунд, а в следующем висит больше двух минут.** Синхронный вызов заморозит окно намертво. **Требуется** служба обнаружения моделей: - результат кэшируется на диске рядом с конфигурацией, с временем получения; - обновление — в фоне, с жёстким таймаутом и понятным поведением при его срабатывании; - интерфейс получает список мгновенно из кэша плюс признак свежести; - при пустом кэше отдаётся `None`, а не выдуманный список — интерфейс покажет «список моделей ещё не получен»; - ошибка обнаружения не должна ронять карточку и не должна молча подставлять умолчания. **Тест:** обнаружение с искусственной задержкой дольше таймаута не блокирует вызывающий поток и оставляет прежний кэш. ## P0-3. Выдуманные списки моделей `auto_assigner.ensure_profile_definition` (добавлен Codex, но это ваша зона) подставляет новым профилям жёстко зашитые списки: ```python "grok": (["grok-3", "grok-3-mini", "grok-2"], ...), "antigravity": (["gemini-3.7-flash", "claude-sonnet-4-6", "gemini-3.5-flash"], ...), ``` Это тот же класс дефекта, с которым мы боролись в квотах, только про модели. И он уже даёт ложь: у живого провайдера **`gemini-3.7-flash` не существует**. Реальный список: ``` gemini-3.7-flash-high / -medium / -low gemini-3.6-flash-high / -medium / -low gemini-3.5-flash-high / -medium / -low gemini-3.1-pro-high / -low claude-sonnet-4-6, claude-opus-4-6-thinking, gpt-oss-120b-medium ``` При этом `gemini-3.7-flash` стоит в живом конфиге владельца как `default_model` роли `orchestrator`, а `gemini-3.6-flash-high` у роли `fast` — существует. То есть часть ролей настроена на несуществующую модель. **Требуется:** 1. Списки моделей для новых профилей брать из обнаружения (P0-2), а не из литерала. Пока обнаружение не выполнено — оставлять список пустым; профиль без списка моделей честнее профиля с выдуманным. 2. Проверка конфигурации: модели, которых нет у провайдера, отмечаются в самопроверке и в контракте как недействительные, с указанием роли и профиля. Молча подставлять «похожую» модель нельзя — это решение владельца. 3. Разобраться, почему вызов с `gemini-3.7-flash` до сих пор не приводил к явной ошибке. Если провайдер молча подставляет ближайшую — это надо знать и написать в отчёте, потому что тогда владелец получает не ту модель, которую выбрал. **Тест:** профиль, созданный при отсутствии кэша моделей, не содержит ни одного идентификатора модели; проверка конфигурации сообщает о модели, отсутствующей у провайдера. ## P1-4. Жёсткие срезы в данных для диаграммы `dashboard_view.py:601` — `providers = list(snapshot.providers)[:3]`. Провайдеров пять, два молча отбрасываются. Отрисовка — зона Codex, и срез уберут там, но решение о том, сколько провайдеров вообще имеет смысл показывать и в каком порядке, принимается на стороне данных: сейчас порядок ничем не задан, поэтому какой именно провайдер исчезнет — дело случая. Задать явный, устойчивый порядок провайдеров в снапшоте (например, по числу авторизованных профилей, затем по алфавиту) и описать его в контракте. ## P1-5. Остаток по YAML Внутренние комментарии `router_profiles.yaml` по-прежнему теряются (7 → 2). Пункт висит с A7 и в A8 не закрыт. Либо полный round-trip, либо статус «частично» с перечнем теряемого — в контракте и в отчёте. С учётом P0-0.1 это стало важнее: миграция будет писать в этот файл, и терять при каждой записи комментарии владельца нельзя. --- ## Ограничения - Граница: зона Codex — `src/antigravity_provider/router/ui/**`, `tests/test_ui_*.py`. По `hermes_hub_app.py` действует прежнее исключение для backend-функций вроде `do_test_profile`, но не для представления. - Никаких чисел и идентификаторов без измерения. Нет данных — `None` и причина. - Тег `v0.1.1` не создавать. - Резервная копия `router_profiles.yaml` перед первой записью миграции — обязательна. ## Критерии приёмки 1. Ни один файл зоны Codex не изменён. 2. Вызов без достоверно определённой роли уходит вниз, не тратя попыток роутера; отказ цепочки никогда не возвращается как ответ ассистента; правка `2d62d39` сохранена. 3. В контракте описана граница между учётными системами Hub и Hermes; подготовлены варианты связывания профилей с оценкой цены каждого. 4. Токен Codex обновляется по `refresh_token`; переключение аккаунта останавливает клиент до подмены учётных данных и не оставляет промежуточного состояния. 5. На существующем конфиге без claude/grok после загрузки `find_free_slot` для обоих возвращает существующий профиль; десять профилей `antigravity` не изменены; проверено тестом. 6. Миграция идемпотентна и не теряет пользовательские правки и комментарии. 7. Квоты `openai-codex` и `opencode-go` приходят измеренными либо `None` с причиной; ни одного выдуманного числа; проверено тестом на обоих исходах. 8. Обнаружение моделей кэшируется, обновляется в фоне и не блокирует вызывающий поток при таймауте; проверено тестом с искусственной задержкой. 9. Новые профили не содержат выдуманных моделей; проверка конфигурации сообщает о несуществующих моделях в ролях владельца. 10. `/reinstall` либо работает с кодами возврата, либо удалён; предупреждение CS0219 при сборке отсутствует. 11. В отчёте назван конкретный способ доставки установщика владельцу. 12. Прогон **в обоих окружениях**; обе команды и оба результата в отчёте. 13. `ruff check .` чисто. Про релизный гейт: он **красный на `main` уже сейчас** (проверка 4 падает не по вашей вине). Указать в отчёте его состояние до и после ваших правок; ухудшать нельзя. 14. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, точный `X passed / Y skipped / Z failed`. ## Главное Владелец сказал: «надо чтобы хаб уже заработал». Ядро работает — живой каскад отказоустойчивости в журнале это доказал, и квоты Antigravity теперь настоящие. Осталось, чтобы не работающее выглядело как не работающее, а не как Н/Д без объяснений, и чтобы провайдер, который он хочет подключить, подключался. ## Порядок сдачи Передать точный `FINAL_COMMIT_SHA`.