- 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.