From 46a185395c0142ab162518c5697fb91019a67d4c Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Thu, 20 Aug 2026 22:22:57 +0700 Subject: [PATCH] docs(review): add independent audit, five review rounds, and remediation task Full audit of the project (93 findings) plus the review record for rounds 1-5: P0 release blockers, 65482e8, 42dfe2c, the v0.1.1 candidate verdict, and the review of the previously unreviewed OAuth/wizard/quota work. Includes the remediation task with acceptance criteria that 42eddb3 was verified against. Co-Authored-By: Claude Opus 5 --- ...8-20-full-audit-and-improvement-backlog.md | 256 ++++++++++++++++++ .../2026-08-20-review-p0-release-blockers.md | 150 ++++++++++ .../inbox/2026-08-20-review-round2-65482e8.md | 104 +++++++ .../inbox/2026-08-20-review-round3-42dfe2c.md | 100 +++++++ .../inbox/2026-08-20-review-round4-verdict.md | 120 ++++++++ ...08-20-review-round5-unreviewed-features.md | 123 +++++++++ ...-21-truthful-data-and-release-readiness.md | 87 ++++++ 7 files changed, 940 insertions(+) create mode 100644 agents/inbox/2026-08-20-full-audit-and-improvement-backlog.md create mode 100644 agents/inbox/2026-08-20-review-p0-release-blockers.md create mode 100644 agents/inbox/2026-08-20-review-round2-65482e8.md create mode 100644 agents/inbox/2026-08-20-review-round3-42dfe2c.md create mode 100644 agents/inbox/2026-08-20-review-round4-verdict.md create mode 100644 agents/inbox/2026-08-20-review-round5-unreviewed-features.md create mode 100644 agents/inbox/2026-08-21-truthful-data-and-release-readiness.md diff --git a/agents/inbox/2026-08-20-full-audit-and-improvement-backlog.md b/agents/inbox/2026-08-20-full-audit-and-improvement-backlog.md new file mode 100644 index 0000000..1a2d563 --- /dev/null +++ b/agents/inbox/2026-08-20-full-audit-and-improvement-backlog.md @@ -0,0 +1,256 @@ +# Полный аудит Hermes Hub и бэклог доработок + +**Дата аудита:** 2026-08-20 +**Проверенная ревизия:** `origin/main` @ `5ccfd48` (github.com/ochenstarik-ui/hermes-hub, private) +**Метод:** статический разбор всех исходников + прогон тестов на чистом checkout `origin/main` в отдельном worktree +**Версия:** 0.1.0 (`pyproject.toml`, `compatibility.json`) против 1.3.0 (`about_view.py`, `gui_server.py`) +**Объём:** ~19 500 строк (Python ~15k, C# 0.8k, HTML 0.85k, PS1/BAT 0.25k, docs/config ~2.5k) + +> Первая редакция этого документа делалась по локальному снимку `fdf9ecc` (на 3 коммита старше). Все пункты ниже перепроверены по `origin/main`; раздел 2 фиксирует, что именно уже исправлено в `7609ad8`. + +--- + +## 1. Общая оценка + +Ядро маршрутизации (`router_engine`, `health_tracker`, `session_affinity`, адаптеры) спроектировано грамотно: чистые слои, dataclass-модели, потокобезопасность, детерминированные тесты с моками. Коммит `7609ad8` («product stabilization») закрыл заметную часть проблем презентационного слоя — появился кэш сканирования, строгий resolver статусов, идентичность приложения в Windows, предпрогрев экранов. + +Тем не менее **продукт остаётся неработоспособным как приложение**: + +- нативное приложение не запускается на чистой машине — ни один установщик не ставит зависимости UI; +- failover для основного провайдера (Antigravity) не срабатывает никогда из-за формата ошибок `agy_generate`; +- три пользовательских сценария падают с `AttributeError`/`NameError`: удаление credentials, подключение аккаунта по API-ключу, перераспределение ролей; +- настройки не сохраняются и ни на что не влияют; +- «roadmap»-модули (+819 строк) не подключены ни к одному кодовому пути. + +**Фактический прогон тестов на чистом checkout `origin/main`:** +`1 failed, 28 passed, 2 skipped` + 1 ошибка сбора (`test_ui_refinement.py` не импортируется без `customtkinter`). +Отчёт `agents/done/…stabilization…md` заявляет «35/35 PASSED» — это верно только на машине, где `dist/` существует локально и зависимости доустановлены руками. + +**Зрелость:** ядро — Beta, оболочка — Alpha, упаковка — не готова к дистрибуции. + +--- + +## 2. Что уже исправлено в `7609ad8` (не требует работы) + +| Было в аудите | Статус в `origin/main` | +|---|---| +| `TeamView.update_data(app_state)` требует аргумент → главный экран не обновляется | **Исправлено** — сигнатура унифицирована (`app_state=None`) во всех view | +| `scan_all()` вызывается 9–10 раз за один refresh | **Исправлено** — TTL-кэш 30 с (`unified_health.py:248`) | +| Ложные `HEALTHY`/`QUOTA_EXHAUSTED` у неподключённых слотов | **Исправлено** — введён `STATUS_NOT_CONFIGURED`, устаревшие записи квот чистятся | +| Иконка Python в панели задач | **Исправлено** — `SetCurrentProcessExplicitAppUserModelID("HermesHub.Desktop")` + multi-res `.ico` | +| Лаг переключения вкладок | **Исправлено** — предпрогрев всех 8 экранов в `__init__`, переключение через `pack_forget()` | +| Установщик не копирует `assets/branding` | **Исправлено** в новом `installer/HermesHubSetup.py` (в `.cs` и `install.ps1` — нет) | +| Нет документации по разработке/безопасности/производительности | **Исправлено** — добавлены `docs/AUTH.md`, `DEVELOPMENT.md`, `PERFORMANCE.md`, `ROUTER.md`, `SECURITY.md` | +| Нет адаптивности окна | **Исправлено** — debounce-обработчик resize | + +--- + +## 3. P0 — Блокеры (перепроверено на `origin/main`, всё воспроизводится) + +| # | Дефект | Файл | Последствие | +|---|---|---|---| +| 1 | `customtkinter` и `pillow` не устанавливаются ни `HermesHubSetup.py`, ни `HermesHubSetup.cs`, ни `install.ps1` | все три установщика | Запуск через `pythonw.exe` завершается молча: ни окна, ни ошибки. Проверено: в `%LOCALAPPDATA%\hermes\hermes-agent\venv` пакета нет | +| 2 | `ProfileAuthManager.get_profile_dir` не существует — это модульная функция, а не метод класса | `profile_manager.py:79` vs `gui_server.py:216`, `hermes_hub_app.py:120`, `add_account_wizard.py:200` | `AttributeError` при удалении credentials и при сохранении API-ключа | +| 3 | `json` не импортирован, но используется | `ui/add_account_wizard.py:200` | `NameError` → **Codex и OpenCode Go нельзя подключить через UI вообще** | +| 4 | `AutoAssigner.auto_assign_all` не существует | `hermes_hub_app.py:411`, `team_view.py:188,243` | Кнопки «Перераспределить роли» и «⋮» падают | +| 5 | `agy_generate` возвращает ошибки как успешный `chat.completion`: текст ошибки в `content`, ключа `error` нет | `agy_subprocess.py:584` + `router_engine.py` | `RouterEngine` проверяет `if "error" in response` → не срабатывает → `mark_success`, **failover для Antigravity не работает никогда**, пользователь получает текст ошибки вместо ответа модели | +| 6 | Мастер ждёт `session.status == "error"`, сессия ставит `"failed"` | `add_account_wizard.py:243` vs `profile_oauth.py` | При ошибке OAuth мастер молча висит 120 с | +| 7 | Кнопка «Назначить» шлёт action `assign_role`, обработчика нет | `accounts_view.py:244` vs `hermes_hub_app._handle_action` | Кнопка не делает ничего, без обратной связи | +| 8 | Шаг 4 мастера не применяет выбранную роль — только пишет в журнал | `add_account_wizard.py:_finish` | Выбор пользователя игнорируется | +| 9 | Шаг 3 показывает «✓ Аккаунт успешно проверен» без всякой проверки для API-ключей; список моделей захардкожен (`gpt-4.1`, `qwen-coder`, `deepseek-v2`) | `add_account_wizard.py` | Ложноположительная валидация, вымышленные модели | + +--- + +## 4. P1 — Производительность и отзывчивость UI + +| # | Проблема | +|---|---| +| 10 | `_restore_status()` вызывает `get_system_readiness()` **в UI-потоке** через `after(6000, …)`. При холодном/истёкшем кэше — блокировка окна на время сетевого скана 16 профилей | +| 11 | При промахе кэша `scan_all()` опрашивает 16 профилей последовательно (таймаут 10 с на профиль) — нет параллелизма и общего бюджета времени | +| 12 | 5 экранов (`accounts`, `health`, `providers`, `routing`, `logs`) полностью уничтожают и пересоздают виджеты через `winfo_children()` + `destroy()` — мерцание, потеря позиции скролла | +| 13 | Нет периодического автообновления: данные устаревают до ручного нажатия «Обновить» (таймер только для resize) | +| 14 | Тост затирает строку состояния и восстанавливается блокирующим вызовом | + +--- + +## 5. P1 — Логика роутера и корректность состояний + +Файлы `router_engine.py`, `health_tracker.py`, `agy_subprocess.py`, `profile_manager.py`, `router_config.py`, `session_affinity.py`, `hermes_plugin.py` в `7609ad8` **не менялись** — все находки в силе. + +| # | Проблема | Файл | +|---|---|---| +| 15 | `_CM_LOCK` удерживается всё время subprocess `agy` (до 180+ с) → все Antigravity-профили сериализуются, параллельная работа команды невозможна | `antigravity_adapter.py` | +| 16 | Профильные токены пишутся в **глобальную** запись `gemini:antigravity` Credential Manager и не восстанавливаются → «основной аккаунт Hermes» меняется как побочный эффект маршрутизации; гонки с самим `agy` и Hermes CLI | `antigravity_adapter.py` | +| 17 | `overall_state` не сбрасывается при истечении cooldown (только в `mark_success`) → профиль остаётся «Квота исчерпана» бессрочно | `health_tracker.py:is_healthy` | +| 18 | Роутер решает по состоянию семейства моделей, UI — по `overall_state`: роутер шлёт запросы в профиль, который UI показывает мёртвым | `health_tracker.py` / `unified_health.py` | +| 19 | Session affinity без TTL и вытеснения — неограниченный рост памяти; `session_affinity_ttl_seconds` из шаблона конфига не реализован | `session_affinity.py` | +| 20 | `router_state.json` пишется без межпроцессной блокировки — GUI и процесс Hermes затирают состояние друг друга | `health_tracker.py` | +| 21 | `ProfileHealthRecord.active_leases` всегда 0, но выводится в CLI и API | `health_tracker.py`, `cli_commands.py` | +| 22 | Категория `TRANSIENT` классифицируется, но обрабатывается как обычный отказ — без повтора и задержки | `router_engine.py` | +| 23 | Классификация ошибок по подстрокам (`"429" in err_lower`) — ложные срабатывания на любом тексте с «429» | все адаптеры | +| 24 | `_safe_env()` не используется: `custom_env = dict(os.environ)` передаёт в subprocess `agy` все ключи (`OPENAI_API_KEY` и пр.) | `agy_subprocess.py`, `antigravity_adapter.py` | +| 25 | Нет стриминга ответов — Hermes получает только полный ответ | `router_engine.py` | +| 26 | Нет учёта токенов, стоимости и latency между вызовами | — | +| 27 | В шаблоне `router_profiles.example.yaml` блок `router:` полностью игнорируется загрузчиком (ожидаются ключи верхнего уровня); `max_failover_attempts`, `cooldown_base_seconds`, `cooldown_max_seconds`, `session_affinity_ttl_seconds` отсутствуют в схеме | `router_config.py:load_router_config` | +| 28 | `save_router_config` пишет другую структуру, чем шаблон → первое сохранение («назначить оркестратором») уничтожает комментарии и структуру пользовательского YAML | `router_config.py` | +| 29 | `max_concurrency=1` для Codex и OpenCode во встроенном дефолте, хотя это stateless REST (в шаблоне 2 и 3) — искусственное ограничение пропускной способности | `router_config.py` | +| 30 | `antigravity_llm_execution` перехватывает **все** вызовы `llm_execution` независимо от провайдера и никогда не вызывает `next_call`; ошибка роутера логируется на уровне `debug` | `hermes_plugin.py` | +| 31 | Каталог моделей захардкожен (`gemini-3.7-flash`, `qwen3.8-max`, `kimi-k2.7-code`…) и не сверяется с реальным | `router_config.py` | +| 32 | `discover_models()` определяет каталог, вызывая `agy` с заведомо невалидной моделью и парся текст ошибки | `agy_subprocess.py` | +| 33 | `AutoAssigner.check_duplicate_identity` сравнивает `auth_data["email"]`, но `profile_oauth` сохраняет только `{"token", "auth_method"}` → **детектор дубликатов не срабатывает никогда** | `auto_assigner.py` | + +--- + +## 6. P1 — Безопасность + +| # | Проблема | +|---|---| +| 34 | `CLIENT_ID`/`CLIENT_SECRET` Google зашиты в код и **склеены из фрагментов строк** (`"GOC" + "SPX" + …`, `oauth.py:26-28`) — приём обхода сканеров секретов, чужой OAuth-клиент. Требует явного решения: собственный клиент или документированное заимствование. Отчёты «0 секретов при сканировании» проходят именно из-за склейки | +| 35 | `ProfileAuthManager.save_profile_auth` пишет токены plaintext без ограничения ACL. `docs/SECURITY_MODEL.md` заявляет изоляцию «правами текущего пользователя ОС», но код прав не выставляет (`chmod` есть только в `CredentialStore` и на Windows не работает) | +| 36 | `access_token` передаётся в query-строке при верификации (`tokeninfo?access_token=…`) — попадает в логи | +| 37 | `CORSMiddleware(allow_origins=["*"], allow_credentials=True)` на локальном API с эндпоинтами `delete`, `set-main`, `oauth/start` (`gui_server.py`) | +| 38 | Лаунчер генерирует и исполняет Python-скрипт `%LOCALAPPDATA%\hermes\hermes_hub_entry.py` при каждом запуске — вектор подмены между записью и стартом | +| 39 | Нет подписи установщика и лаунчера; `dist/checksums.txt` формируется вручную и не проверяется при установке | +| 40 | В `hermes-hub.log` пишутся тексты ошибок провайдеров без санитизации (могут содержать фрагменты токенов) | + +--- + +## 7. P1 — Тесты и качество + +| # | Проблема | +|---|---| +| 41 | `pytest tests/` **падает на сборе**: `test_ui_refinement.py` → `hermes_hub_app` → `customtkinter` | +| 42 | `test_setup_exe_exists` **падает на чистом checkout**: `dist/` в `.gitignore`, ещё 2 теста уходят в skip. Реальный результат — `1 failed, 28 passed, 2 skipped`, а не заявленные 35/35 | +| 43 | Тесты мутируют реальные пользовательские данные: `do_set_main` пишет `router_active_profile.json`, `do_set_orchestrator` перезаписывает `router_profiles.yaml`, `get_profile_env_dir` создаёт каталоги профилей. Нет фикстуры с временным `HERMES_HOME` | +| 44 | `test_installer.py` **запускает реальный установщик**: копирование в `%LOCALAPPDATA%`, ярлык, запись в реестр | +| 45 | Тесты ходят в сеть (Google tokeninfo, OpenCode `/models`) — недетерминированность | +| 46 | Нет тестов на: `classify_error` всех адаптеров, мастер подключения, YAML round-trip `load↔save`, CLI, логику установщиков | +| 47 | Нет CI (`.github/workflows` отсутствует, репозиторий приватный) | +| 48 | `ruff` в dev-зависимостях, но нет секции `[tool.ruff]` и запуска; нет `mypy` | + +--- + +## 8. P1 — Упаковка, установка, дистрибуция + +| # | Проблема | +|---|---| +| 49 | **Два параллельных установщика**: `installer/HermesHubSetup.cs` (711 строк, GUI-мастер, silent-режим, реестр, коды 0/10/11/12) и `installer/HermesHubSetup.py` (101 строка, копирует assets, но без реестра, ярлыка, silent-кодов). Неясно, какой канонический; `dist/HermesHubSetup.exe` собран из `.cs` | +| 50 | Проверка совместимости фиктивна: `compatibility.json` не читается, `MIN_HERMES_VERSION` ни с чем не сравнивается, `IsHermesCompatible = true` всегда при наличии Python → **код возврата 11 недостижим**. То же в `install.ps1` (версия печатается, но не проверяется) | +| 51 | `installer/HermesHubSetup.cs` и `scripts/install.ps1` не копируют `assets/branding` (исправлено только в `.py`-варианте) | +| 52 | `ui/assets.py:39` — захардкоженный fallback `Path("E:/Agent projects/hermes-hub")` в исходниках | +| 53 | `build_launcher.ps1` не передаёт `/win32icon:HermesHub.ico` → у самого `.exe` нет иконки (иконка ставится только в рантайме через `iconbitmap`) | +| 54 | `install.ps1` копирует `__pycache__` | +| 55 | `launcher/HermesHub.exe` — собранный бинарник в git; собранный установщик, наоборот, не отслеживается | +| 56 | Нет single-instance guard — повторный клик по ярлыку открывает второе окно | +| 57 | Нет логирования запуска: при `pythonw.exe` любая ошибка импорта = полная тишина | +| 58 | Нет механизма обновления в самом приложении | +| 59 | Версия расходится: `0.1.0` (pyproject, compatibility.json, installer) против `1.3.0` (About, FastAPI) | +| 60 | `scripts/launch_hermes_hub.bat` запускает несуществующий веб-режим на `:8765`; CLI-аргументы `hub --port/--no-browser` парсятся и игнорируются | + +--- + +## 9. P2 — Мёртвый и неподключённый код + +| # | Объект | Строк | Статус | +|---|---|---|---| +| 61 | `supervisor/lifecycle_supervisor.py`, `supervisor/policies.py`, `capability/capability_matrix.py`, `skills/skill_registry.py`, `scheduler/task_safety.py`, `adapters/deepseek_adapter.py` | 819 | **Roadmap-модули из `7609ad8` не подключены ни к одному кодовому пути.** Импортируются только тестами (`task_safety` и `deepseek_adapter` — вообще ничем). `DeepSeekResponsesAdapter` не зарегистрирован в `_ADAPTERS` → недостижим через `get_adapter()` | +| 62 | `gui_server.py` + `gui_cockpit.html` | 1177 | Параллельный веб-стек, не запускается ниоткуда; `fastapi`/`uvicorn` остаются обязательными зависимостями | +| 63 | `runtime.generate_chat_completion`, `antigravity_client.py`, `transform.py`, `openai_compat.py`, `cloudcode.py` | ~590 | Прямой API-путь к Antigravity, не используется (весь трафик через `agy` CLI) | +| 64 | `ui/views/dashboard_view.py` («Главная») | 141 | Не подключён к навигации | +| 65 | `ui/splash.py` (`SplashScreen`) | 77 | Не используется | +| 66 | `HealthTracker.get_status_summary()` | — | Не вызывается ниоткуда | +| 67 | Резолвинг `hermes_home` | — | Продублирован в 5 файлах с тремя разными реализациями (`HERMES_HOME` / `LOCALAPPDATA` / `~/.hermes`) | +| 68 | `do_set_main`/`do_test_profile`/`do_delete_credentials` | — | Дублируются между `gui_server.py` и `hermes_hub_app.py` | + +--- + +## 10. P2 — Документация + +| # | Проблема | +|---|---| +| 69 | `docs/ARCHITECTURE.md` и `CHANGELOG.md` описывают «Edge App Mode», «FastAPI Backend :8765», «health check gate (HTTP 200)» — этой архитектуры больше нет | +| 70 | `agents/AGENTS.md` и отчёты в `agents/done/` указывают Source of Truth `E:\Agent projects\hermes-hub`; ссылки вида `file:///E:/Agent%20projects/...` мертвы | +| 71 | `docs/SECURITY_MODEL.md` не описывает фактическую подмену глобальной записи `gemini:antigravity` при маршрутизации | +| 72 | Нет описания схемы `router_profiles.yaml` (единственный источник истины — код) и справочника CLI | +| 73 | Отчёты в `agents/done/` содержат утверждения, не подтверждаемые кодом: «35/35 PASSED», «интерактивные переключатели параметров», «9/9 экранов», «иконка вшита в лаунчер». Требуется выравнивание с фактами | +| 74 | UI жёстко на русском, CLI на английском, строки не вынесены — i18n невозможен | + +--- + +## 11. P2–P3 — Функциональные пробелы + +| # | Пробел | +|---|---| +| 75 | Настройки не сохраняются: `CTkSwitch`/`CTkOptionMenu` не привязаны к `self.settings` (нет ни одного `command=`), `_save_settings()` пишет неизменённый словарь | +| 76 | Ни один параметр не влияет на роутер: `failover_attempts`, `session_affinity`, `auto_failover`, `model_timeout_sec`, `monitoring_interval_min` нигде не читаются | +| 77 | Пути в «Настройках» указывают на `hermes\router_profiles.yaml` и `hermes\auth.json`; реальные — `hermes\config\router_profiles.yaml` и `hermes\agy_profiles\\auth.json` | +| 78 | Нет ручного назначения профиля на роль (только автоматическое) | +| 79 | Экран «Маршрутизация» read-only: нельзя изменить цепочку failover | +| 80 | Нельзя добавить или удалить слот — 16 профилей жёстко зашиты в коде | +| 81 | Кнопка «Обновить модели» не вызывает `adapter.discover_models()` — показываются `preferred_models` из конфига | +| 82 | Фоновый мониторинг здоровья отсутствует, хотя переключатель для него есть | +| 83 | Нет уведомлений (tray/toast) об исчерпании квоты и переключении на резерв | +| 84 | Нет экспорта/импорта конфигурации и резервного копирования профилей | +| 85 | Нет истории квот и использования | +| 86 | Журнал живёт в памяти (200 записей) и обнуляется при перезапуске; `hermes-hub.log` пишется, но не читается обратно | +| 87 | В журнале нет поиска и фильтра по уровню (только по категории) | +| 88 | Тема жёстко тёмная, нет клавиатурной навигации и доступности | +| 89 | `compatibility.json` заявляет `linux`/`darwin`, но Credential Manager, пути и лаунчер — только Windows | + +--- + +## 12. Рекомендуемый план работ + +### Этап 1 — Вернуть работоспособность (P0, ~2 дня) +1. Устанавливать `customtkinter`/`pillow` в venv Hermes из установщика; проверять импорт после установки и падать с внятной ошибкой. +2. Исправить `get_profile_dir` (сделать `@staticmethod` либо импортировать функцию), добавить `import json` в мастер. +3. Реализовать `AutoAssigner.auto_assign_all()` либо убрать кнопки. +4. Ввести маркер ошибки в `agy_generate` (`{"error": …}` или исключение) и восстановить failover для Antigravity; покрыть регрессионным тестом. +5. Починить мастер: статус `"failed"`, реальная валидация API-ключа, реальные модели, применение выбранной роли, обработчик `assign_role`. +6. Прогонять `pytest` в изолированном `HERMES_HOME`; убрать запуск установщика и сетевые вызовы из тестов; сделать `test_setup_exe_exists` skip-ом при отсутствии `dist/`. + +### Этап 2 — Корректность состояний и отзывчивость (P1, ~3 дня) +7. Все сетевые операции — только в фоне; `after(0, …)` получает готовые данные (включая `_restore_status`). +8. Параллельный опрос профилей с общим бюджетом времени вместо последовательного. +9. Обновление виджетов in-place вместо `destroy()`/пересоздания в 5 экранах. +10. Сброс `overall_state` при истечении cooldown; единый источник истины по здоровью для роутера и UI. +11. TTL и вытеснение в `SessionAffinityTracker`; файловая блокировка `router_state.json`. +12. Сузить `_CM_LOCK` до операции подмены credential и восстанавливать прежнюю запись — либо перейти на изоляцию только через `USERPROFILE`. +13. Связать `settings_view` с реальными параметрами роутера; выровнять пути. +14. Починить `check_duplicate_identity` (сохранять `email` в `auth.json` при OAuth). + +### Этап 3 — Безопасность и упаковка (P1, ~2 дня) +15. Решение по OAuth-клиенту; прекратить склейку секрета из фрагментов. +16. ACL/DPAPI для `auth.json`; санитизация логов; убрать токен из query-строки. +17. Убрать `allow_origins=["*"]` либо удалить веб-стек целиком. +18. Выбрать один установщик; реальная проверка версии из `compatibility.json` (сделать код 11 достижимым); исключить `__pycache__`; убрать `E:/…` fallback; встроить иконку через `/win32icon`. +19. Логирование запуска в файл до инициализации UI; single-instance guard. + +### Этап 4 — Долг и документация (P2, ~2 дня) +20. Подключить roadmap-модули к реальным кодовым путям (как минимум зарегистрировать `DeepSeekResponsesAdapter` в `_ADAPTERS`) либо явно пометить их как экспериментальные. +21. Удалить или вынести в `legacy/` веб-стек и прямой API-путь; убрать `fastapi`/`uvicorn` из обязательных зависимостей. +22. Единый модуль `paths.py`; устранить дублирование `do_*`. +23. Синхронизировать версию (0.1.0), обновить `ARCHITECTURE.md`/`CHANGELOG.md` под нативную архитектуру, исправить пути `E:\`. +24. Выровнять отчёты в `agents/done/` с фактическим состоянием. +25. Добавить CI (pytest + ruff) и конфигурацию ruff. + +### Этап 5 — Развитие (P3) +26. Ручное назначение ролей и редактор цепочек failover. +27. Динамические слоты, реальный `discover_models`. +28. Фоновый мониторинг + уведомления о квотах. +29. Персистентный журнал с поиском; статистика использования. +30. Стриминг ответов, учёт токенов и стоимости. +31. Экспорт/импорт конфигурации, бэкап профилей. + +--- + +## 13. Сводка + +| Приоритет | Пунктов | Область | +|---|---|---| +| **Исправлено в `7609ad8`** | 8 | Кэш сканирования, resolver статусов, идентичность Windows, предпрогрев, assets, docs | +| **P0** — блокеры | 9 | Запуск приложения, failover Antigravity, падения UI, мастер подключения | +| **P1** — критично | 51 | Отзывчивость, корректность состояний, безопасность, тесты, упаковка | +| **P2** — важно | 20 | Неподключённый код, архитектурный долг, документация | +| **P3** — развитие | 9 | Новая функциональность | +| **Итого к работе** | **89** | | diff --git a/agents/inbox/2026-08-20-review-p0-release-blockers.md b/agents/inbox/2026-08-20-review-p0-release-blockers.md new file mode 100644 index 0000000..5d1601b --- /dev/null +++ b/agents/inbox/2026-08-20-review-p0-release-blockers.md @@ -0,0 +1,150 @@ +# Ревью: `2a97e80` + `7926de9` (закрытие P0 и релизный конвейер) + +**Ревьюер:** Claude (роль «Ревьюер») +**База:** `5ccfd48` → **проверено:** `7926de9` +**Метод:** построчный разбор диффа + исполняемая проверка каждого утверждения в изолированном `HERMES_HOME` + +**Вердикт: ⚠️ Принять нельзя — 4 блокирующих замечания.** Из 9 P0 фактически закрыто 8; один закрыт с регрессией. Дополнительно найдено 4 дефекта в новом коде. + +--- + +## 1. Подтверждено закрытым (проверено исполнением) + +| P0 | Проверка | Итог | +|---|---|---| +| №2 `get_profile_dir` | `hasattr(ProfileAuthManager,'get_profile_dir')` → `True`, поддержаны обе сигнатуры | ✅ | +| №3 `json` в мастере | импорт присутствует, `_save_key` отрабатывает | ✅ | +| №4 `auto_assign_all` | метод существует, выполняется | ✅ (см. B2) | +| №6 статусы OAuth | `("error","failed","cancelled")` + `"timeout"` — `add_account_wizard.py:265` | ✅ | +| №7 `assign_role` | обработчик + модалка `hermes_hub_app.py:408,419` | ✅ | +| №8 роль из шага 4 | вызывается `assign_profile_to_role` | ✅ (см. B2) | +| №9 валидация ключа | реальные `verify_codex_token` / `verify_opencode_token`, честная метка «НЕ ПРОВЕРЕН» | ✅ | +| №1 зависимости UI | `HermesHubSetup.py:52` ставит пакеты в venv Hermes + проверка импорта. Пины `customtkinter>=6.0.0`, `pillow>=12.3.0` сверены с PyPI — версии существуют | ✅ | + +**Сверх P0 — зачтено:** единая версия `0.1.1` (`version.py` → pyproject → compatibility.json → About); `paths.py` вместо пяти реализаций резолвинга и захардкоженного `E:/Agent projects`; сброс `overall_state` по истечении cooldown (проверено: `overall=healthy` при `family=quota-exhausted`, после истечения — оба healthy); типизированные исключения вместо угадывания по подстрокам на уровне роутера; `conftest.py` с изоляцией `HERMES_HOME` (проверено — работает); маркеры pytest и `addopts` с отключением `installer/live/network`; CI и `release_gate.py`. + +**Тесты:** `41 passed, 1 failed, 3 deselected`. Единственный failed — `test_p0_1_installer_dependencies`, потому что в venv Hermes на этой машине `customtkinter` ещё не установлен (установщик не запускался). Регрессионный тест на failover `test_p0_5_antigravity_failover_on_quota` присутствует и проходит. + +--- + +## 2. Блокирующие замечания + +### B1. Регрессия: не-роутерный путь падает с `IndexError` + +`agy_subprocess.py:584` теперь возвращает `{"error": {...}}` без `choices`. Для роутера это правильно, но `_error_completion` обслуживает **два** пути, а `hermes_plugin.py` не менялся. + +Путь 2 (`hermes_plugin.antigravity_llm_execution`, когда роутер отключён `config.enabled=false` или выбросил исключение): +``` +completion = agy_generate(request) # -> {"error": {...}} +return openai_completion_object(completion) # -> choices = [] +``` + +Проверено исполнением: +``` +после openai_completion_object: choices = [] +обращение Hermes к choices[0]: IndexError: list index out of range +``` + +Было: пользователь видел текст ошибки как ответ ассистента. Стало: падение транспорта Hermes. + +**Требуется:** обработать `{"error": …}` в `antigravity_llm_execution` — либо конвертировать в completion с текстом ошибки, либо пробрасывать типизированное исключение. Тест на путь с `enabled=false` обязателен. + +--- + +### B2. `assign_profile_to_role` создаёт фиктивные роли и не меняет реальные + +`auto_assigner.py:assign_profile_to_role` вызывает `config.get_role_policy(role_name)`, а тот для неизвестной роли возвращает **generic-политику с `preferred_chain = list(self.profiles.keys())`** — все 16 профилей. Результат сохраняется в конфиг. + +При этом `auto_assign_all` раздаёт роли из списка `["orchestrator","coder","reviewer","researcher","tester","general"]`, а мастер — `coder/researcher/general/spare`. Реальные роли роутера: `orchestrator`, `coder-primary`, `coder-secondary`, `reviewer`, `research`, `fast`. Совпадают только `orchestrator` и `reviewer`. + +Проверено в изолированном окружении: +``` +роли ДО : [coder-primary, coder-secondary, fast, orchestrator, research, reviewer] +роли ПОСЛЕ: [coder, coder-primary, coder-secondary, fast, orchestrator, research, reviewer] + +длина цепочки роли coder : 16 +цепочка coder : [ag-w1, codex-orch, ..., ag-cold-1, ag-cold-2, ag-cold-3, opengo-1, ...] +цепочка coder-primary : [codex-worker-1, ag-w1, opengo-3] <- НЕ ИЗМЕНИЛАСЬ +``` + +Итог: назначение роли не влияет на маршрутизацию, а конфиг засоряется мусорными ролями с цепочками из 16 профилей, включая отключённые холодные резервы. + +`test_p0_4` и `test_p0_7` этого не ловят, потому что проверяют механику («выполнилось без `AttributeError`», «цепочка изменилась и сохранилась»), а не семантику — что имя роли маршрутизируемое. + +**Требуется:** маппинг человекочитаемых имён на реальные (`coder` → `coder-primary`, `researcher` → `research`, `tester`/`general` → `fast` или отдельная роль), отказ при неизвестной роли, и запрет на persist generic-политики со всеми профилями. В тесты — проверка, что `role_name in config.roles` **до** назначения. + +--- + +### B3. Каждое назначение роли уничтожает пользовательский YAML + +`save_router_config` пишет схему, отличную от шаблона. Раньше это срабатывало редко, теперь — при **каждом** подключении аккаунта через мастер и при каждом `auto_assign_all`. + +Проверено на копии `config/router_profiles.example.yaml`: +``` +шаблон: 5 строк комментариев, блок router: 1 +после одного назначения роли: 0 строк комментариев, блок router: 0 +``` + +Блок `router:` (`max_failover_attempts`, `cooldown_base_seconds`, `cooldown_max_seconds`, `session_affinity_ttl_seconds`) загрузчиком и так игнорируется — но пользователь, отредактировавший его, молча теряет правки. + +**Требуется:** либо привести `load/save` к одной схеме и покрыть тестом round-trip (`load → save → load` без потерь), либо перейти на редактирование с сохранением структуры. + +--- + +### B4. Rate limit классифицируется как исчерпание квоты — 30 минут вместо 60 секунд + +`antigravity_adapter.py:invoke` — первая ветка ловит `("quota","resource_exhausted","429","limit","exhausted")`, ветка `"rate"` идёт **после** и для реального 429 недостижима: сообщение «429 Too Many Requests: rate limit exceeded» содержит и `429`, и `limit`. + +Проверено исполнением: +``` +вход : Antigravity error: agy error: 429 Too Many Requests: rate limit exceeded +итог : QuotaExceededError -> category=quota-exhausted cooldown=1800s +``` + +Профиль паркуется на 30 минут вместо 60 секунд. При цепочке из трёх профилей серия временных 429 выводит роль в «Failover Exhausted». + +**Требуется:** проверять `rate`/`too many requests`/`429` **до** квотной ветки, либо различать по коду ответа, а не по подстроке. + +--- + +## 3. Существенные замечания (не блокируют, но лучше в этом же заходе) + +**S1. Потеряно время сброса квоты.** Типизированные исключения обрабатываются в начале `classify_error` и используют `exc.reset_in_sec or 1800`, но `reset_in_sec` при выбросе в `invoke` не заполняется. Regex-разбор («resets in 2h» → 7200 s) остался ниже и для типизированных исключений недостижим. Проверено: «individual quota reached, resets in 2h» → `cooldown=1800s`. Раньше было 7200. Прокидывайте распарсенную длительность в конструктор исключения. + +**S2. Встроенный апдейтер нерабочий.** `update_manager.py:82` тянет манифест с `raw.githubusercontent.com/ochenstarik-ui/hermes-hub/main/dist/update_manifest.json`. Файл не отслеживается git (`dist/` в `.gitignore`), репозиторий приватный. Проверено: `HTTP 404`. Проверка обновлений всегда будет падать. Нужен публичный канал (GitHub Releases API) или отдельная ветка/репозиторий манифестов. + +**S3. Апдейтер проверяет целостность, но не подлинность.** SHA-256 берётся из того же манифеста, что и `package_url`, — при подмене манифеста хеш подменяется вместе с пакетом. Плюс `HERMES_HUB_UPDATE_URL` переопределяется переменной окружения, а `package_url` скачивается без allowlist хостов. Для механизма, который распаковывает код в рабочий каталог и затем исполняется, нужна подпись пакета либо жёсткая привязка к домену релизов. (`extractall` от Zip Slip защищён — CPython нормализует пути, здесь претензий нет.) + +**S4. `release_gate.check_security_zero_secrets` даёт ложное «0 секретов».** Проверка ищет отслеживаемые **файлы**-секреты и не видит `CLIENT_SECRET` в `oauth.py:28`, склеенный из фрагментов строк специально для обхода сканеров. Гейт будет зелёным при наличии секрета в коде. + +**S5. `py_compile` по `dest.rglob("*.py")`** (`update_manager.py:194`) пройдёт по `.venv`, если апдейт применяется к каталогу разработки, — это тысячи файлов и вероятные ложные сбои с откатом. Ограничьте обход каталогом `src`. + +**S6. Настройки по-прежнему не сохраняются.** Ни один `CTkSwitch`/`CTkOptionMenu` в `settings_view.py` (строки 78, 87, 96, 116, 124) не имеет `command=`/`variable=`, `_save_settings()` пишет неизменённый словарь дефолтов. Кнопка «💾 Сохранить» ничего не сохраняет. В отчёте это заявлено как «интерактивные переключатели» — расхождение. + +**S7. `test_installer` всё ещё меняет систему.** Песочница через `LOCALAPPDATA`/`HERMES_HOME` в env закрывает файловое дерево, но `CreateStartMenuShortcut` пишет в `%APPDATA%\...\Start Menu`, а `RegisterInWindowsUninstall` — в `HKCU`. Ни то, ни другое не перенаправляется. Смягчено маркером `installer` и `addopts`, но при `-m installer` система снова будет изменена. + +**S8. Отсутствие UI-зависимости роняет весь прогон.** `test_ui_refinement.py:11` импортирует `hermes_hub_app` на уровне модуля → `ModuleNotFoundError: customtkinter` прерывает сбор всей сессии («Interrupted: 1 error during collection»), а не пропускает один модуль. Нужен `pytest.importorskip("customtkinter")`. + +**S9. Мастер не останавливает опрос при закрытии окна.** `_polling_active` сбрасывается только при переходе на шаг 3 (`add_account_wizard.py:279`). Закрытие модалки во время OAuth оставляет поток жить до 120 с, и он вызовет `self.after(0, …)` на уничтоженном виджете. Нужен `destroy()`/`WM_DELETE_WINDOW` со сбросом флага. + +--- + +## 4. Осталось открытым из аудита (вне scope этого коммита) + +Не претензия к коммиту — фиксирую, что эти пункты не затронуты: + +- `_CM_LOCK` по-прежнему удерживается на всё время subprocess `agy` (`antigravity_adapter.py:invoke`) → профили Antigravity сериализуются. +- Глобальная запись `gemini:antigravity` перезаписывается без восстановления → «основной аккаунт Hermes» меняется как побочный эффект маршрутизации. +- Session affinity без TTL; `router_state.json` без межпроцессной блокировки. +- `hermes_plugin` перехватывает все вызовы `llm_execution` и не вызывает `next_call`. +- 5 экранов пересоздают виджеты через `destroy()`; нет периодического автообновления. +- Roadmap-модули (`supervisor`, `capability`, `skills`, `scheduler`, `deepseek_adapter`) по-прежнему не подключены; `DeepSeekResponsesAdapter` не зарегистрирован в `_ADAPTERS`. +- Веб-стек `gui_server.py` + `gui_cockpit.html` и прямой API-путь остаются мёртвыми; `fastapi`/`uvicorn` — в обязательных зависимостях. + +--- + +## 5. Резюме для исполнителя + +Порядок правок: **B1** (регрессия, ломает работающий сценарий) → **B4** (однострочная перестановка веток) → **B2** + **B3** (связаны: маппинг ролей и сохранение конфига) → S1, S8, S9 (дёшево) → S2–S5 (апдейтер, до объявления релиза). + +Релизным гейтом это пропускать нельзя: `check_p0_release_gate` рапортует «9/9 P0 verified», хотя P0-5 закрыт с регрессией, а P0-4/P0-7 проходят на фиктивных ролях. diff --git a/agents/inbox/2026-08-20-review-round2-65482e8.md b/agents/inbox/2026-08-20-review-round2-65482e8.md new file mode 100644 index 0000000..5f13224 --- /dev/null +++ b/agents/inbox/2026-08-20-review-round2-65482e8.md @@ -0,0 +1,104 @@ +# Ревью, раунд 2: `65482e8` (ответ на замечания B1–B4, S1–S9) + +**База:** `7926de9` → **проверено:** `65482e8` +**Метод:** повтор тех же исполняемых проверок, что выявили дефекты в раунде 1 + +**Вердикт: ✅ Блокеры закрыты, коммит принимается в `main`.** +**⚠️ Релиз v0.1.1 объявлять рано** — канал обновлений не работает, а релизный гейт даёт ложно-зелёный результат по секретам. + +--- + +## 1. Блокеры — все 4 закрыты (проверено повтором) + +**B1 — не-роутерный путь.** Защита в двух местах: guard в `hermes_plugin.antigravity_llm_execution` (обе ветки) и в самом `openai_completion_object`. Повтор моей проверки: +``` +choices: 1 +choices[0].message.content: Antigravity error: ... individual quota reached +``` +Раньше — `IndexError`. Закрыто. + +**B2 — фиктивные роли.** Введён `CANONICAL_ROLE_MAP`, неизвестные роли отклоняются: +``` +coder -> ok=True назначен на роль 'coder-primary' +researcher -> ok=True назначен на роль 'research' +tester -> ok=True назначен на роль 'fast' +general -> ok=True назначен на роль 'fast' +spare -> ok=False Неизвестная роль маршрутизатора: 'spare' + +роли ПОСЛЕ: без изменений (мусорные роли не создаются) +цепочка coder-primary: ['ag-w1', 'codex-worker-1', 'opengo-3'] <- реально обновилась +макс. длина цепочки: 4 (было 16) +``` +Закрыто. + +**B3 — разрушение YAML.** Схема `load`/`save` сведена. Блок `router:` сохраняется целиком со всеми значениями (`enabled`, `default_role`, `max_failover_attempts`, `cooldown_base_seconds`, `cooldown_max_seconds`, `session_affinity_ttl_seconds`); профилей 16/16, ролей 6/6, cooldown'ы не поплыли. Закрыто по существу — см. остаточное R3. + +**B4 — 429 как квота.** Порядок веток исправлен: +``` +429 Too Many Requests: rate limit exceeded -> RateLimitedError rate-limited retry_delay=60s +individual quota reached, resets in 2h -> QuotaExceededError quota-exhausted cooldown=7200s +``` +Закрыто (заодно S1 — длительность сброса снова парсится: 7200 вместо 1800). + +--- + +## 2. Второстепенные — закрыто 5 из 8 + +| | Статус | +|---|---| +| S1 время сброса квоты | ✅ 7200 s для «resets in 2h» | +| S5 `py_compile` по `.venv` | ✅ обход ограничен `target_src` | +| S6 настройки не сохранялись | ✅ `_save_settings` читает состояние виджетов (`aff_sw.get()` и др.) | +| S8 сбор pytest падал целиком | ✅ `pytest.importorskip("customtkinter")`; прогон: **44 passed, 2 skipped, 3 deselected** | +| S9 опрос OAuth после закрытия | ✅ переопределён `destroy()` со сбросом `_polling_active` | +| **S2 канал обновлений** | ❌ не исправлено | +| **S3 подлинность пакета** | ❌ не адресовано | +| **S4 слепая проверка секретов** | ❌ не исправлено | +| S7 тест установщика меняет систему | ❌ не адресовано (риск низкий: `installer` deselected по умолчанию) | + +--- + +## 3. Новое, найденное в этом раунде + +### N1. Мастер: «Только резерв (Spare)» молча не срабатывает, а журнал рапортует успех + +`add_account_wizard.py:373` предлагает вариант `spare`, строка 394 маппит его в `"spare"`, строка 401 вызывает `assign_profile_to_role(..., "spare", ...)`. После правки B2 эта роль **отклоняется** (`ok=False`), но возвращаемое значение игнорируется, и следом безусловно пишется: + +```python +EventLogService.get().log("account", f"... Назначен на роль: {target_role}.", level="success") +``` + +Пользователь выбирает «Только резерв», видит успех — назначения не происходит. До B2 создавалась мусорная роль, теперь не делается ничего; в обоих случаях функция не работала, но раньше отказ был хотя бы виден в конфиге. + +**Нужно:** либо добавить `spare` в `CANONICAL_ROLE_MAP` с осмысленной семантикой (добавление в хвост цепочки нескольких ролей), либо проверять результат и показывать пользователю отказ. `auto_assign_all` возвращаемое значение проверяет (`if ok:`) — здесь надо так же. + +### N2. Двойной префикс в тексте ошибки (косметика) + +`_error_completion` уже добавляет `"Antigravity error: "`, guard в `hermes_plugin`/`runtime` добавляет его повторно → `Antigravity error: Antigravity error: individual quota reached`. + +### N3. Релизный гейт зелёный на непроверенных вещах + +- Пункт «3. Auto-Updater & Rollback → PASS»: ни один тест в `test_updater.py` не обращается к реальному манифесту — все работают на инъецированном словаре или локальных файлах. Реальный канал при этом отдаёт **HTTP 404** (проверено). Гейт подтверждает механику, но не работоспособность обновления. +- Пункт «6. Zero Credentials & Secrets → PASS»: проверка ищет файлы (`auth.json`, `*.key`, `.env*`) и живые ключи по regex `sk-…`/`opencode-…`. `CLIENT_SECRET` в `oauth.py:29`, склеенный из фрагментов именно для обхода сканеров, не обнаруживается. Гейт будет зелёным при секрете в коде. + +--- + +## 4. Остаточное (не блокирует merge, блокирует релиз) + +**R1 (S2). Встроенный апдейтер нерабочий.** `update_manager.py:82` по-прежнему указывает на `raw.githubusercontent.com/ochenstarik-ui/hermes-hub/main/dist/update_manifest.json`. Проверено сейчас: `HTTP 404`, файл в git не отслеживается (`dist/` в `.gitignore`), репозиторий приватный. Пока не появится публичный канал (GitHub Releases API или отдельный репозиторий манифестов), «Проверить обновления» всегда будет падать. + +**R2 (S3). Только целостность, без подлинности.** SHA-256 берётся из того же манифеста, что и `package_url`; `HERMES_HUB_UPDATE_URL` переопределяется переменной окружения; хост `package_url` не ограничен allowlist'ом. Для механизма, распаковывающего исполняемый код в рабочий каталог, нужна подпись пакета либо жёсткая привязка к домену релизов. + +**R3 (из B3). Комментарии в YAML по-прежнему стираются:** 5 строк → 0 при первом сохранении. Данные и структура больше не теряются, но пользовательские пояснения — да. Решается либо переходом на `ruamel.yaml` с сохранением комментариев, либо явным предупреждением в документации, что файл перезаписывается приложением. + +**R4.** Сохранённые настройки (`hub_settings.json`) по-прежнему никем не читаются: `failover_attempts`, `session_affinity`, `auto_failover`, `model_timeout_sec`, `monitoring_interval_min` не влияют на роутер. Переключатели теперь честно сохраняются — но эффекта не дают. + +**Из аудита без изменений:** `_CM_LOCK` на всё время subprocess; глобальная запись `gemini:antigravity` без восстановления; session affinity без TTL; `router_state.json` без межпроцессной блокировки; roadmap-модули не подключены; веб-стек мёртв. + +--- + +## 5. Рекомендация + +1. **Merge `65482e8` в `main`** — блокеры закрыты, регрессий не внесено, тесты зелёные. +2. **До объявления v0.1.1:** закрыть N1 (одна строка + проверка результата), поднять реальный канал обновлений (R1) либо скрыть кнопку «Проверить обновления», научить гейт видеть склеенные секреты (N3/S4) — иначе «RELEASE GATE: PASSED» вводит в заблуждение. +3. N2, R3, R4 — в обычную очередь. diff --git a/agents/inbox/2026-08-20-review-round3-42dfe2c.md b/agents/inbox/2026-08-20-review-round3-42dfe2c.md new file mode 100644 index 0000000..ab75046 --- /dev/null +++ b/agents/inbox/2026-08-20-review-round3-42dfe2c.md @@ -0,0 +1,100 @@ +# Ревью, раунд 3: `925e6c6` → `42dfe2c` (N1–N3, S2–S4, R1–R4) + +**База:** `65482e8` → **проверено:** `42dfe2c` (4 коммита) +**Тесты:** 50 passed, 2 skipped, 3 deselected. **Гейт:** PASSED (Candidate v0.1.1). + +**Вердикт: ✅ Заявленное сделано и проверено. ⛔ Найден один новый блокер релиза — канонический установщик не содержит исправления P0-1.** + +--- + +## 1. Закрыто (проверено исполнением) + +**N1 — «Только резерв».** У `spare` появилась настоящая семантика, мастер проверяет результат: +``` +ag-w1 в цепочках ДО : ['coder-primary'] +spare -> ok=True Профиль 'ag-w1' сохранен в пуле резерва (spare) +ag-w1 в цепочках ПОСЛЕ: [] +мусорная роль -> ok=False Неизвестная роль маршрутизатора +``` +Мелочь: при `ok=False` мастер делает `return` без закрытия окна и без сообщения в самом окне — предупреждение уходит только в журнал, кнопка выглядит «мёртвой». Лучше показать текст ошибки в модалке. + +**N2 — двойной префикс.** `Antigravity error: individual quota reached` — один префикс. + +**N3 / S4 — сканер секретов.** AST-обход с раскрытием конкатенаций и `''.join(...)`, allowlist для документированных публичных констант. Проверил синтетическим файлом: +``` +CLIENT_SECRET = "".join(("GOC","SPX","-","someothersecret123")) -> Obfuscated secret assignment ✅ +API_KEY = "hardcoded-live-value-xyz" -> Hardcoded sensitive secret ✅ +реальный oauth.py -> чисто (константы в allowlist) +``` +Сканер больше не даёт ложного «zero secrets». + +**OAuth — деобфускация.** Склейка из фрагментов убрана, константы записаны прямо, решение задокументировано в `docs/OAUTH_CLIENT.md` с моделью угроз по RFC 8252/7636 и явным признанием, что используется стандартный клиент Google CloudCode Desktop. Это ровно то, что запрашивалось: «собственный клиент либо документированное заимствование». + +**S3 — allowlist хостов.** HTTPS-only + список доменов GitHub, применяется и к манифесту (`:117`), и к `package_url` при скачивании (`:174`). + +**R1 / S2 — канал обновлений.** Фид переехал на отдельный публичный репозиторий, 404 обрабатывается мягко («Канал обновлений пока не настроен»), гейт честно печатает `[NOT PUBLISHED YET]` вместо голого PASS. Формулировка сменилась на «Ready for Candidate». + +**R4 — настройки влияют на роутер.** `settings_service.py` + чтение в `route_request`. Проверил поведением: +``` +auto_failover=false -> router_error=True, попыток в trail: 1 (failover действительно отключён) +``` +Работают `auto_failover`, `session_affinity`, `failover_attempts`, `auto_return_primary`. + +--- + +## 2. ⛔ Новый блокер релиза: исправление P0-1 не попало в канонический установщик + +`installer/README.md` (новый файл) объявляет: + +> **Canonical Binary:** `HermesHubSetup.exe` (built from `HermesHubSetup.cs`) +> `HermesHubSetup.py`: Internal development helper script… **Not distributed** as a primary installer artifact. +> `install.ps1`: Deprecated legacy bootstrap script. + +При этом проверка кода: + +| | `HermesHubSetup.cs` (канонический) | `HermesHubSetup.py` (объявлен вспомогательным) | +|---|---|---| +| Установка `customtkinter`/`pillow` в venv Hermes | **нет ни одного упоминания** | есть (6 упоминаний, `pip install` + проверка импорта) | +| Копирование `assets/branding` | **нет** | есть | + +`build_installer.ps1` собирает `dist/HermesHubSetup.exe` именно из `.cs`. + +Итог: архитектурное решение объявило каноническим тот установщик, где P0-1 **не исправлен**, а тот, где исправлен, — недистрибутируемым. По канонической цепочке установки приложение по-прежнему не запустится (`pythonw.exe` молча завершится на `import customtkinter`), и логотипов не будет. + +Тест `test_p0_1_installer_dependencies` этого не ловит: он проверяет импортируемость `customtkinter` в текущем runtime, а не то, что канонический установщик его ставит. + +**Нужно одно из двух:** перенести установку зависимостей и копирование assets в `HermesHubSetup.cs`, либо объявить каноническим `.py` и пересобрать конвейер под него. Плюс тест, проверяющий именно канонический артефакт. + +--- + +## 3. Заявлено, но кодом не подтверждено + +**S7.** В таблице отчёта: «unit tests isolated from Windows Registry/Start Menu». Фактически `tests/test_installer.py` **не менялся** с раунда 1 и по-прежнему делает `subprocess.run([SETUP_EXE, "/silent"])` (строки 53, 68). Песочница через `env` закрывает только файловое дерево; `CreateStartMenuShortcut` пишет в `%APPDATA%\…\Start Menu`, `RegisterInWindowsUninstall` — в `HKCU`. Это политика в `installer/README.md`, а не изоляция в коде. Риск низкий (маркер `installer` deselected по умолчанию), но формулировку в отчёте стоит поправить. + +--- + +## 4. Мелкие замечания + +**M1.** `is_allowed_update_host(url, allow_dev_local=True)` вызывается с `allow_dev_local=True` в обоих местах (`:117`, `:174`). Внутри: `if url.startswith("file://") or Path(url).exists(): return allow_dev_local or os.environ.get("HERMES_HUB_DEV_MODE") == "1"`. При `allow_dev_local=True` условие по переменной окружения не проверяется вообще — гейт dev-режима, который вы написали, обходится собственными вызовами. Локальный путь в `package_url` пройдёт allowlist в продакшене. + +**M2.** Гейт: заголовок «2. P0 Release Blockers (**16/16**)», сообщение — «**12/12** P0 release blockers verified». Косметическое расхождение. + +**M3.** `get_hub_settings()` читается с диска на **каждый** `route_request` — файловый I/O на запрос. Стоит кэшировать с инвалидацией по mtime. + +**M4.** Репозиторий `ochenstarik-ui/hermes-hub-releases` пока не существует (проверено: `gh repo view` → not found, манифест 404). Ожидаемо для кандидата, но до релиза его нужно создать и опубликовать манифест. + +--- + +## 5. Остаточное из прошлых раундов + +- **R3.** Комментарии в `router_profiles.yaml` по-прежнему стираются при сохранении (5 → 0). Данные и структура целы. +- **R4 частично.** `model_timeout_seconds`, `monitoring_interval_seconds`, `auto_monitoring` сохраняются, но нигде не читаются — работают 4 настройки из 7. +- Без изменений: `_CM_LOCK` на всё время subprocess; глобальная запись `gemini:antigravity` без восстановления; session affinity без TTL; `router_state.json` без межпроцессной блокировки; roadmap-модули не подключены; веб-стек мёртв. + +--- + +## 6. Рекомендация + +1. **Merge — да.** Регрессий не внесено, тесты и гейт зелёные, качество ответа на замечания высокое. +2. **Кандидатом v0.1.1 объявлять после закрытия блокера из раздела 2** — иначе установленное каноническим установщиком приложение не стартует. +3. M1 — быстро и по делу (убрать `allow_dev_local=True` из продакшен-вызовов). M2, M3, S7-формулировка — в обычную очередь. diff --git a/agents/inbox/2026-08-20-review-round4-verdict.md b/agents/inbox/2026-08-20-review-round4-verdict.md new file mode 100644 index 0000000..f20c29f --- /dev/null +++ b/agents/inbox/2026-08-20-review-round4-verdict.md @@ -0,0 +1,120 @@ +# Ревью, раунд 4: вердикт по кандидату v0.1.1 + +**Отчёт исполнителя описывает `8314d46`. Фактический `origin/main` — `3aae1a8`, на 3 коммита впереди.** +Проверка проводилась по фактическому HEAD. + +**Вердикт: ⛔ тег `v0.1.1` не выпускать.** Заявленное в отчёте сделано и подтверждено, но на текущем HEAD релизный гейт **падает**, а публичный фид обновлений указывает на несуществующий пакет. + +--- + +## 1. Заявленное в отчёте — подтверждаю + +| Пункт | Проверка | Итог | +|---|---|---| +| Канонический `.cs`: `EnsurePythonDependencies` | `installer/HermesHubSetup.cs:89` — проверка импорта → `ensurepip` → `pip` → фолбэк на `uv` → повторная верификация | ✅ | +| Канонический `.cs`: копирование assets | строки 263–269, три целевых каталога (install dir, плагин, hermes home) | ✅ | +| Post-install smoke test | строка 303, `HERMES_HUB_IMPORT_OK`, проверяется ExitCode и наличие маркера | ✅ | +| M1 — dev-bypass | `allow_dev_local` по умолчанию `False`, оба продакшен-вызова передают `False` явно (`:130`, `:187`) | ✅ | +| M2 — счётчик гейта | `16/16` в заголовке и в сообщении согласованы | ✅ | +| M3 — кэш настроек | mtime-кэш + `invalidate_settings_cache()` при сохранении | ✅ | +| Публичный фид | `HTTP 200`, манифест валидный, v0.1.1 | ✅ | + +Блокер раунда 3 (исправление P0-1 не в каноническом установщике) **закрыт**. + +--- + +## 2. ⛔ Блокер 1: релизный гейт падает на текущем HEAD + +Отчёт цитирует `[RELEASE GATE: PASSED]` — это результат для `8314d46`. На `3aae1a8` фактически: + +``` +[RELEASE GATE: FAILED] One or more checks failed. Release blocked. +``` + +Причина — новый `tests/test_codex_opencode_wizard.py` из коммита `3aae1a8` импортирует `customtkinter` на уровне модуля **без `pytest.importorskip`**, и сбор всей сессии прерывается: + +``` +ERROR tests/test_codex_opencode_wizard.py +!!! Interrupted: 1 error during collection !!! +1 skipped, 3 deselected, 1 error +``` + +Падают пункты гейта 2, 3 и 4. Это возврат ровно того дефекта, который закрывался как **S8** в раунде 2: отсутствие опциональной UI-зависимости должно давать *skip*, а не обрывать прогон. Остальные три тестовых модуля (`test_ui_refinement`, `test_p0_release_gate`, `test_oauth_lifecycle`) `importorskip` содержат — забыли только в новом. + +Без этого файла суite зелёный: **57 passed, 4 skipped, 3 deselected**. Исходный код, который тестируется (`CodexOAuthSession`, `enable_clipboard_shortcuts`, `HubEntry`, `extract_jwt_identity`), на месте. + +**Оговорка:** на машине, где `customtkinter` установлен, гейт пройдёт. Но инвариант «отсутствие UI-зависимости не ломает прогон» нарушен — а это ровно тот сценарий, ради которого существует P0-1. + +**Правка:** одна строка `pytest.importorskip("customtkinter")` в новом файле. + +--- + +## 3. ⛔ Блокер 2: фид обновлений указывает на несуществующий пакет + +Манифест живой и отдаёт `HTTP 200`, но пакет из него — нет: + +``` +package_url: https://github.com/ochenstarik-ui/hermes-hub-releases/releases/download/v0.1.1/hermes-hub-0.1.1.zip + -> HTTP 404 +gh release list --repo ochenstarik-ui/hermes-hub-releases -> пусто +``` + +Пункт гейта 7 проверяет только наличие полей `version` и `package_url` в манифесте, но не достижимость самого пакета — поэтому печатает `[LIVE STATUS] Public update manifest live`. Формально верно, по существу — обновление скачать нельзя. + +Сейчас это не проявится (манифест объявляет 0.1.1, приложение 0.1.1 → «обновлений нет»), но как только выйдет 0.1.2, пользователь получит ошибку загрузки. + +**Правка:** опубликовать релизный ассет с заявленным `sha256`, а в `check_production_update_feed` добавить HEAD-запрос по `package_url`. + +--- + +## 4. Артефакт релиза непроверяем из репозитория + +Отчёт приводит SHA256 собранного установщика `1D41B9D6…`. Проверить его нельзя: + +``` +файлов dist/ в git: 0 (dist/ в .gitignore) +локальный dist/HermesHubSetup.exe: 7e2531… (сборка от 20.08 00:16, устаревшая) +dist/checksums.txt: содержит старую сумму 7E2531… +``` + +Собранный установщик существует только на машине исполнителя. Любой, кто клонирует репозиторий, установщика не получает, а `checksums.txt` вводит в заблуждение. Для релиза артефакт должен попасть в GitHub Release (там же, где ассет обновления), а `checksums.txt` — обновляться сборочным скриптом. + +Заявленный clean-install тест в изолированном venv я подтвердить не могу — воспроизвести его без артефакта нечем. Код установщика при этом проверен и выглядит корректно (см. раздел 1). + +--- + +## 5. Отчёт отстал от кода на 2097 строк + +После `8314d46` в `main` влиты три коммита, которых отчёт не касается: + +``` +0d9005f fix(oauth): eliminate ERR_CONNECTION_REFUSED, immediate listener binding, single session reuse +249a888 feat(oauth): Cockpit Tools model, manual callback fallback, race protection +3aae1a8 feat(wizard): Codex OAuth/API Key & OpenCode Go with clipboard UX +``` + +`+2097 / −232` строк: новый `codex_oauth.py` (296), переработанный `profile_oauth.py` (+296), `add_account_wizard.py` (+880), `components.py` (+83), два новых тестовых модуля. + +Это функциональная разработка, а не закрытие замечаний, и она не отревьюирована. Беглая проверка на утечки: в `codex_oauth.py` и `profile_oauth.py` токены и коды в логи не пишутся — логируются только `source` и HTTP-код. Полноценного ревью этих коммитов не проводилось. + +**Вердикт по релизу не может покрывать неотрецензированный код.** Либо тег ставится на `8314d46` (после закрытия блокера 2), либо новые коммиты идут на отдельное ревью. + +--- + +## 6. Условия выпуска v0.1.1 + +1. Добавить `pytest.importorskip("customtkinter")` в `tests/test_codex_opencode_wizard.py`; убедиться, что гейт зелёный в окружении **без** UI-зависимостей. +2. Опубликовать релизный ассет `hermes-hub-0.1.1.zip` с суммой из манифеста; расширить проверку 7 HEAD-запросом по `package_url`. +3. Положить `HermesHubSetup.exe` в GitHub Release и обновить `dist/checksums.txt` сборочным скриптом. +4. Определиться с областью тега: `8314d46` либо отдельное ревью коммитов `0d9005f…3aae1a8`. + +Пункты 1 и 2 — блокирующие. 3 и 4 — обязательны для честного релиза, но могут быть закрыты процедурно. + +--- + +## 7. Остаточное из прошлых раундов (не блокирует) + +- Комментарии в `router_profiles.yaml` стираются при сохранении. +- `model_timeout_seconds`, `monitoring_interval_seconds`, `auto_monitoring` сохраняются, но не читаются (работают 4 настройки из 7). +- `tests/test_installer.py` по-прежнему пишет в Start Menu и `HKCU` при запуске с `-m installer`. +- `_CM_LOCK` на всё время subprocess; глобальная запись `gemini:antigravity` без восстановления; session affinity без TTL; `router_state.json` без межпроцессной блокировки; roadmap-модули не подключены; веб-стек мёртв. diff --git a/agents/inbox/2026-08-20-review-round5-unreviewed-features.md b/agents/inbox/2026-08-20-review-round5-unreviewed-features.md new file mode 100644 index 0000000..0cbea30 --- /dev/null +++ b/agents/inbox/2026-08-20-review-round5-unreviewed-features.md @@ -0,0 +1,123 @@ +# Ревью, раунд 5: `0d9005f…0c511cd` (OAuth, мастер, тарифы, квоты, Claude/Grok, Plan A) + +**База:** `8314d46` → **проверено:** `0c511cd` (5 коммитов, +6970 / −665) +**Состояние:** тесты 91 passed / 7 skipped / 3 deselected, релизный гейт зелёный. + +**Вердикт: ⛔ два блокирующих дефекта — фабрикация данных о квотах и поддельный код авторизации. Оба относятся к классу «пользователь видит правдоподобное, но выдуманное».** + +--- + +## 1. ⛔ Блокер A: сборщик квот выдаёт захардкоженные числа под видом данных провайдера + +`src/antigravity_provider/router/quota_collector.py` (561 строка) **не делает ни одного сетевого вызова**: + +``` +urlopen вызовов: 0 +``` + +При этом каждый сборщик возвращает фиксированные константы и помечает их источником провайдерского API: + +```python +# _collect_antigravity_quota, строки 320–346 +QuotaBucket(id="antigravity.gemini.5h", used_percent=9.0, remaining_percent=91.0, status="healthy") +QuotaBucket(id="antigravity.gemini.weekly", used_percent=1.0, remaining_percent=99.0, status="healthy") +... +return QuotaSnapshot(..., source="antigravity_api") +``` + +Всего **17 литеральных `used_percent`** по пяти провайдерам, с источниками: +`antigravity_api`, `codex_usage_api`, `opencode_api`, `claude_oauth_usage_api`, `xai_task_usage_api`. + +Эти значения доходят до пользователя: `ui/views/accounts_view.py:168` + +```python +snap = quota_snap or p.quota_snapshot or AccountQuotaService.get().get_snapshot(p.provider, p.profile_id) +``` + +и рисуются в `quota_box` карточки аккаунта. То есть на экране «Аккаунты» показывается «Gemini 5h — использовано 9%, осталось 91%» независимо от реального состояния квоты. Ровно та задача, ради которой существует продукт — понимать, где кончилась квота, — решается вымышленными числами. + +**Тесты фикцию не закрепляют** (`test_accounts_tariffs_quotas.py` проверяет арифметику `used/remaining` на собственных значениях), поэтому дефект не виден по зелёному прогону. + +**Требуется одно из двух:** +- реализовать реальный сбор (запрос к провайдеру) — тогда источники честные; +- либо пометить снапшоты как `source="baseline"`/`"estimated"` и явно показать в UI, что это оценка, а не данные провайдера. Значение `"baseline"` в коде уже есть (строка 525) — значит, различие осознавалось. + +--- + +## 2. ⛔ Блокер B: тихий фолбэк на поддельный код авторизации + +`codex_oauth.py:103–112`: + +```python +try: + resp = _post_json(CODEX_OAUTH_USER_CODE_URL, {"client_id": CODEX_OAUTH_CLIENT_ID}) + self.user_code = resp.get("user_code") + self.device_auth_id = resp.get("device_auth_id") +except Exception as e: + logger.warning("Could not reach OpenAI deviceauth endpoint directly: %s. Using local session.", e) + self.user_code = f"CDX-{secrets.token_hex(3).upper()}" + self.device_auth_id = secrets.token_urlsafe(16) +``` + +То же в `grok_oauth.py:91–94` (`GRK-XXXXXX` + локальный `device_code`). + +По RFC 8628 `user_code` и `device_code` выдаёт **сервер авторизации**. Сгенерированные локально, они серверу неизвестны. Последствие: при любой недоступности эндпоинта (сетевой сбой, смена API, гео-блокировка) мастер показывает пользователю правдоподобный код вида `CDX-A1B2C3`, предлагает ввести его на странице провайдера, затем поллит `device_auth_id`, которого не существует, — и через 15 минут выдаёт таймаут. Отличить подделку от настоящего кода пользователь не может; в логах остаётся только `warning`. + +Комментарий в коде говорит «for offline or simulated/mocked environment» — то есть режим предназначен для тестов, но срабатывает в продакшене по любому исключению. + +**Требуется:** при неудаче запроса кода показывать ошибку немедленно, а фолбэк включать только под явным флагом (`HERMES_HUB_DEV_MODE=1`), по образцу уже сделанного в `update_manager.is_allowed_update_host`. + +--- + +## 3. Существенные замечания + +**S1. Три новых заимствованных OAuth-клиента без документирования.** +``` +codex_oauth.py:31 CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann" (OpenAI Codex CLI) +grok_oauth.py:30 XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828" +claude_oauth.py клиент Claude +``` +Для Google было принято решение и оформлено `docs/OAUTH_CLIENT.md` с моделью угроз по RFC 8252. Три новых клиента появились без такого же разбора. Политику нужно распространить на них — иначе документ описывает одну треть реальной картины. + +**S2. `gui_server.py` вырос на 112 строк и по-прежнему ничем не запускается.** Проверено: ни один `.py`, `.bat`, `.ps1`, `.cs` не вызывает `run_gui_server`. В мёртвый параллельный стек вкладываются усилия; `fastapi`/`uvicorn` остаются обязательными зависимостями. + +**S3. `health_tracker`: атомарность есть, межпроцессной блокировки нет.** Запись через `tempfile.mkstemp` + `os.replace` (строки 136–145) — риск порванного файла закрыт. Но GUI-процесс и процесс Hermes по-прежнему перетирают состояние друг друга целиком (last-writer-wins). + +--- + +## 4. Закрыто в этих коммитах (подтверждаю) + +| Замечание прошлых раундов | Статус | +|---|---| +| Session affinity без TTL и вытеснения | ✅ `ttl_seconds=1800`, `max_entries=1000`, `prune_expired()` | +| `_restore_status` блокирует UI сетевым сканом | ✅ читает готовый снапшот из `HubStateStore`, сети в UI-потоке нет | +| Новые модули оказываются мёртвым кодом | ✅ не повторилось: `quota_collector` (9 импортёров), `state_store` (8), `account_identity` (5), `scheduler` (5), `event_bus` (4), `model_registry` (2) — всё подключено | +| Новые адаптеры не регистрируются | ✅ `claude` и `grok` есть в `_ADAPTERS` | +| Наложение периодических задач | ✅ `HermesRefreshScheduler` с overlap-skip и счётчиком `tasks_skipped_overlap` | +| Утечка секретов в логи | ✅ не найдено: в `add_account_wizard.py`, `codex_oauth.py`, `grok_oauth.py`, `claude_oauth.py` токены и ключи не логируются; в мастере поле с `show="*"` и маскирование идентичности | + +Отдельно отмечу качество: капабилити-роутинг в `router_engine` со скорингом моделей и фолбэком в пределах аккаунта сделан аккуратно, `state_store` с дельта-обновлениями — правильное архитектурное решение против прежних полных пересканов. + +--- + +## 5. Остаётся открытым с прошлых раундов + +- `capability_matrix`, `lifecycle_supervisor`, `skill_registry`, `deepseek_adapter` — по-прежнему не подключены ни к одному кодовому пути. +- Комментарии в `router_profiles.yaml` стираются при сохранении. +- `model_timeout_seconds`, `monitoring_interval_seconds`, `auto_monitoring` сохраняются, но не читаются. +- `tests/test_installer.py` при запуске с `-m installer` пишет в Start Menu и `HKCU`. +- `_CM_LOCK` удерживается на всё время subprocess `agy`; глобальная запись `gemini:antigravity` перезаписывается без восстановления. + +--- + +## 6. Условия приёмки + +**Блокирующие:** +1. Блокер A — либо реальный сбор квот, либо честная маркировка `estimated`/`baseline` в источнике и в UI. +2. Блокер B — фолбэк на локальный код только под `HERMES_HUB_DEV_MODE=1`, иначе немедленная ошибка. + +**До релиза:** +3. Распространить решение по OAuth-клиентам на OpenAI, xAI и Claude в `docs/OAUTH_CLIENT.md`. +4. Определиться с `gui_server.py`: удалить, вынести в `legacy/` или подключить. + +После закрытия 1 и 2 препятствий к тегу со стороны кода не вижу — остаются организационные пункты из раунда 4 (опубликовать ассет релиза и `HermesHubSetup.exe`, обновить `checksums.txt`). diff --git a/agents/inbox/2026-08-21-truthful-data-and-release-readiness.md b/agents/inbox/2026-08-21-truthful-data-and-release-readiness.md new file mode 100644 index 0000000..ad583b0 --- /dev/null +++ b/agents/inbox/2026-08-21-truthful-data-and-release-readiness.md @@ -0,0 +1,87 @@ +# Задание: Правдивость данных и готовность к релизу v0.1.1 + +## Дата поступления +2026-08-21 + +## Контекст +По итогам ревью раунда 5 (`0d9005f…0c511cd`, +6970 строк) выявлены два блокирующих дефекта одного класса: **пользователю показываются правдоподобные, но выдуманные данные**. Тесты и релизный гейт при этом зелёные — дефекты не детектируются существующими проверками. + +Ветка `main` = `0c511cd`. Тег `v0.1.1` снят с `origin` и локально (указывал на `65482e8` без исправления P0-1). + +**Просьба: на время выполнения этого задания заморозить `main` для новой функциональности.** За два предыдущих ревью в ветку влилось ~7300 строк, из-за чего вердикт устаревал к моменту выдачи. + +--- + +## Область задачи (Scope) + +### P0-1. Сборщик квот не должен выдавать выдуманные числа за данные провайдера + +`src/antigravity_provider/router/quota_collector.py` не делает ни одного сетевого вызова (`urlopen`: 0), но возвращает 17 захардкоженных `used_percent` и помечает снапшоты источниками `antigravity_api`, `codex_usage_api`, `opencode_api`, `claude_oauth_usage_api`, `xai_task_usage_api`. Эти значения выводятся в карточке аккаунта (`ui/views/accounts_view.py:168`). + +Выбрать один из путей: + +**Путь А (предпочтительный) — реальный сбор.** Запрашивать потребление у провайдера там, где есть API; при успехе оставлять источник `*_api`, при неудаче — переключаться на путь Б. + +**Путь Б — честная маркировка.** Если данные вычисляются локально: +- источник снапшота — `estimated` или `baseline`, но **не** `*_api`; +- в UI рядом с корзиной квоты — явный признак оценки (значок/подпись «оценка», не проверено у провайдера); +- значения не должны выглядеть как измерение: либо диапазон, либо «нет данных» вместо `9%`. + +Смешанный вариант (А с фолбэком на Б) допустим и желателен. + +### P0-2. Убрать тихий фолбэк на поддельный код авторизации + +`router/codex_oauth.py:103–112` и `router/grok_oauth.py:91–94` при недоступности эндпоинта провайдера генерируют локальные `user_code` (`CDX-…`, `GRK-…`) и `device_code`/`device_auth_id`. По RFC 8628 их выдаёт сервер авторизации; локально сгенерированные ему неизвестны, поллинг гарантированно завершается таймаутом через 15 минут, а пользователь всё это время видит правдоподобный код. + +Требуется: +- в обычном режиме при неудаче запроса кода — **немедленная ошибка** с текстом причины в мастере, без запуска поллинга; +- фолбэк на локальную сессию — только под явным `HERMES_HUB_DEV_MODE=1` (по образцу `update_manager.is_allowed_update_host`), с видимой пометкой в UI, что сессия тестовая; +- проверить остальные OAuth-модули (`claude_oauth.py`, `profile_oauth.py`) на такой же паттерн. + +### P1-3. Распространить решение по OAuth-клиентам на новых провайдеров + +`docs/OAUTH_CLIENT.md` описывает только Google. В коде появились ещё три заимствованных клиента: +``` +codex_oauth.py:31 app_EMoamEEZ73f0CkXaXp7hrann (OpenAI) +grok_oauth.py:30 b1a00492-073a-47ea-816f-4c329264a828 (xAI) +claude_oauth.py клиент Claude +``` +Добавить их в документ с той же структурой: тип клиента, модель угроз, обоснование по RFC 8252/7636, явное указание происхождения. + +### P1-4. Определить судьбу `gui_server.py` + +Файл вырос ещё на 112 строк, но `run_gui_server` не вызывается ниоткуда (проверено по `.py`, `.bat`, `.ps1`, `.cs`). Либо подключить, либо вынести в `legacy/`, либо удалить вместе с `gui_cockpit.html`. При удалении — убрать `fastapi`/`uvicorn` из обязательных зависимостей `pyproject.toml`. + +### P1-5. Организационные пункты релиза (из раунда 4) + +1. Опубликовать GitHub Release в `ochenstarik-ui/hermes-hub-releases`: ассет `hermes-hub-0.1.1.zip` с суммой, совпадающей с `sha256` в `update_manifest.json` (сейчас `package_url` → HTTP 404). +2. Положить туда же `HermesHubSetup.exe`; `dist/checksums.txt` обновлять сборочным скриптом, а не вручную. +3. Тег `v0.1.1` ставить только на коммит, прошедший ревью. Если локальный тег остался на машине исполнителя — удалить: `cd ; git tag -d v0.1.1`. + +### P2-6. Долг, накопленный за пять раундов (в обычную очередь) + +- `capability_matrix`, `lifecycle_supervisor`, `skill_registry`, `deepseek_adapter` — не подключены ни к одному кодовому пути; подключить либо пометить экспериментальными. +- Комментарии в `router_profiles.yaml` стираются при сохранении. +- `model_timeout_seconds`, `monitoring_interval_seconds`, `auto_monitoring` сохраняются, но не читаются (работают 4 настройки из 7). +- `tests/test_installer.py` при `-m installer` пишет в Start Menu и `HKCU`. +- `_CM_LOCK` удерживается на всё время subprocess `agy`; глобальная запись `gemini:antigravity` перезаписывается без восстановления. +- `health_tracker`: запись атомарна, но межпроцессной блокировки нет (last-writer-wins между GUI и Hermes). + +--- + +## Критерии приёмки + +Ревью будет проверять ровно это, исполнением: + +1. `grep -c urlopen quota_collector.py` > 0 **либо** ни один снапшот с локально вычисленными значениями не имеет источник `*_api`. Проверяется вызовом сборщика и чтением поля `source`. +2. В UI карточки аккаунта для оценочных данных присутствует визуальный признак оценки. +3. При недоступности эндпоинта устройства (мокируется исключением в `_post_json`) и **без** `HERMES_HUB_DEV_MODE`: `start()` возвращает ошибку, `status != "pending"`, поток поллинга не стартует. +4. С `HERMES_HUB_DEV_MODE=1` фолбэк работает и помечен в UI. +5. `docs/OAUTH_CLIENT.md` содержит разделы по OpenAI, xAI и Claude. +6. `run_gui_server` либо вызывается, либо файла нет в `src/`. +7. `curl -I ` → HTTP 200, sha256 совпадает. +8. Регрессия не внесена: полный `pytest tests/` зелёный **в окружении без UI-зависимостей** (проверка инварианта `importorskip`), релизный гейт PASSED. +9. Новые тесты на P0-1 и P0-2, падающие на текущем `0c511cd`. + +## Что НЕ входит в задание +Новая функциональность. Задание закрывает дефекты и готовит релиз; любые новые возможности — после тега.