e7d01dd021
tui_gateway/server.py:_save_cfg called yaml.safe_dump on a deep-loaded config dict, which reordered top-level keys alphabetically, stripped every user-edited comment, and re-escaped non-ASCII (kaomoji/Chinese) personality prompts to \uXXXX. Every TUI setting change - /personality, /reasoning, /details_mode, /skin, /prompt - rewrote the file top to bottom. Changes: * Add atomic_roundtrip_yaml_save(path, new_state) in utils.py - a comment-, ordering-, and unicode-preserving full-state replacement for yaml.safe_dump(cfg, f). Uses ruamel round-trip mode like the existing atomic_roundtrip_yaml_update, but accepts the whole cfg dict so callers that mutate multiple keys before saving (the _save_cfg pattern) don't have to be rewritten. Recurses into nested dicts, deletes keys missing from new_state (preserves the cfg.pop()-then-save semantic), and overwrites lists/scalars wholesale. * Fail closed on an unreadable existing config.yaml the same way hermes_cli.config.atomic_config_write does, via a lazy import of require_readable_config_before_write (avoids a module-level circular import, since hermes_cli.config itself imports from utils). Also preserves both file mode and owner across the write, matching the existing atomic_roundtrip_yaml_update contract. * Force-quote any new string value that YAML 1.1 would misparse as a bool/null (yes/no/on/off/true/false/null/~). ruamel's round-trip dumper resolves against the YAML 1.2 core schema and emits these unquoted, but PyYAML-based readers elsewhere in the codebase parse under YAML 1.1 rules - so an unquoted `approvals.mode: off` would silently round-trip back as the boolean False. * tui_gateway/server.py:_save_cfg now delegates to atomic_roundtrip_yaml_save. Drop-in - all call sites (/personality, /reasoning, /details_mode, /prompt, etc.) inherit comment preservation and the fail-closed contract. Tests: * tests/test_utils_atomic_roundtrip_yaml_save.py - unit tests covering create-from-empty, top-level key-order preservation, comment preservation, readable Unicode, append-new-keys, delete-missing-keys, scalar/list overwrite, nested-dict recursion, refusal on an unreadable existing config, and owner preservation. * tests/test_atomic_replace_symlinks.py - owner-preservation regression test mirroring the existing atomic_roundtrip_yaml_update coverage. * tests/test_tui_gateway_server.py - 4 new tests pinning _save_cfg comment preservation, top-level key-order preservation, and unicode-readability under unrelated writes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>