- load_gateway_config (536 LOC) is now a thin orchestrator: legacy gateway.json
-> config_loader.load_yaml_layer -> GatewayConfig.from_dict -> env overrides
-> validation. The yaml phase lives in gateway/config_loader.py as small
functions driven by tables: _TOPLEVEL_BRIDGE (23 top-level/nested
gateway.<key> bridges with 5 fallback modes), _SHARED_KEYS (28 per-platform
keys copied into extra, with per-platform restrictions and transforms) and
_PORT_BRIDGE_KEYS. Logger name kept as "gateway.config".
- GatewayConfig.from_dict: shared pick()/key_label() helpers replace the
repeated "top-level key present else nested gateway.<key>" blocks; warning
order preserved.
- _normalize_unauthorized_dm_behavior / _normalize_notice_delivery unified into
_normalize_choice; _ensure_platform_extra_dict -> _dict_slot (also used by
persist_home_channel); _getenv_int removed (dead since the env pass moved to
config_env, which has its own _int_or).
- Platform._missing_: one _add_pseudo_member helper for both branches.
- Single warning sites in _coerce_optional_positive_int and
coerce_systemd_watchdog_seconds; _validate_gateway_config placeholder pass
flattened; small to_dict/getter collapses. Ruff F401/SIM102 clean.
- tests/hermes_cli/test_config_read_guard.py: allowlist gateway/config_loader.py
(same owner as gateway/config.py — the extracted load_gateway_config phase).
gateway/config.py 2684 -> 1319 LOC (-50.9%); largest function now
GatewayConfig.from_dict at 119 LOC. Resolved-config parity: 160 cells
(16 yaml fixtures x 10 env sets) byte-identical to the integration base,
including captured log records and stderr.
Byte-identical bodies moved out of hermes_cli/kanban_db.py into four sibling
modules, re-exported from the origin so kanban_db.<name> keeps resolving and
stays the single monkeypatch target; origin-resident helpers are reached via a
late-bound _kb namespace. AST-identity verified for all 329 moved symbols; SQL
statement multiset parity vs base. Three source-inspection tests repointed at
kanban_db_dispatch; the _add_column_if_missing alias test now imports the real
owner (hermes_cli.sqlite_util).
Completes the half-done move snapshotted by the parent (wip commit 283f63b):
the tts.providers.<name> config layer (_get_named_provider_config,
_resolve_command_provider_config, _generate_command_tts, ...) now lives in
tools/tts_command_provider.py; tts_tool re-imports the historical names.
cli_model_switch_mixin: the identical snapshot->stage->agent.switch_model->rollback
block in _apply_model_switch_result and _confirm_and_apply_cli_model_switch (2x47
lines) -> _stage_and_swap_model(result, old_model) -> bool. cli_loops_mixin: the
identical reversed-history assistant-text extraction in the loop-tick and goal
post-turn hooks -> _last_assistant_response_text(). Four _StubCLI test doubles bind
the new helper to the real implementation.
AST-driven, body-identical move of 359 GatewayRunner methods into cohesive
mixin modules (gateway/run_{voice,adapters,topics,turn,shutdown,busy,
config_loaders,startup,watchers,notifications,inbound,goals,agent_cache}.py)
plus TurnRunner -> gateway/run_turn_runner.py. run.py-internal symbols are
imported lazily inside method bodies so patch('gateway.run.X') keeps
intercepting; neutral deps are top-level; logger name stays 'gateway.run'.
_UNSET moved to leaf gateway/run_common.py (def-time default-arg sentinel).
Whole-module inspect.getsource(gateway_run) AST-walker tests repointed to
the module that now holds the walked code.
326 methods lifted by AST (bodies identical; ast.dump-verified) into
hermes_cli/cli_{tui,status_bar,voice,model_switch,session,stream,modal,
terminal,info,loops}_mixin.py. cli.py-internal symbols resolve via lazy
'from cli import ...' inside each method (no import cycle; patch('cli.X')
keeps working). The three 'global' writers (_skill_commands, _cli_wake_owner)
now write the cli module attribute explicitly so the origin's readers still
see them. Dropped imports left unused in cli.py; kept display_hermes_home /
build_welcome_banner as re-exports (mixins + tests resolve them via cli).
Repointed two AST change-detector tests to cli_tui_mixin.py; one test
fixture now keeps 'cli' in sys.modules across its patch.dict scope.
The adapters branch extracted connect()'s webhook/polling branches into methods;
the guard is intact in _start_webhook_mode. The source-inspection test now checks
the two method bodies by AST instead of the old inline if/else regex.