Preserve YAML comments and formatting in config writes

NousResearch/hermes-agent · cluster 10890 · gateway

Data as of

Nothing open on GitHub. Fix merged in #22845. Check it covers your case.

The short answer

0open issues
0open PRs
16items grouped
Known?
Yes. 16 related items grouped here, 5 issues already closed.
Fix event
A fix was merged to main in PR #22845. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #22845, #52386, #118918. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and PRs address a recurring problem where config write operations (particularly hermes config set/unset) strip comments, formatting, and template blocks from config.yaml. Several entries also concern YAML serialization inconsistencies and list indentation across multiple serializers.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #22845 fix(cli): preserve config comments on setting writes (salvage #22717)

    merged to main

    files: cli.py, pyproject.toml, scripts/release.py, tests/cli/test_cli_save_config_value.py, utils.py

    Show the diff
    • cli.py
      @@ -2206,26 +2206,10 @@ def save_config_value(key_path: str, value: any) -> bool:
               # Ensure parent directory exists (for ~/.hermes/config.yaml on first use)
               config_path.parent.mkdir(parents=True, exist_ok=True)
               
      -        # Load existing config
      -        if config_path.exists():
      -            with open(config_path, 'r', encoding="utf-8") as f:
      -                config = yaml.safe_load(f) or {}
      -        else:
      -            config = {}
      -        
      -        # Navigate to the key and set value
      -        keys = key_path.split('.')
      -        current = config
      -        for key in keys[:-1]:
      -            if key not in current or not isinstance(current[key], dict):
      -                current[key] = {}
      -            current = current[key]
      -        current[keys[-1]] = value
      -        
      -        # Save back atomically — write to temp file + fsync + os.replace
      -        # so an interrupt never leaves config.yaml truncated or empty.
      -        from utils import atomic_yaml_write
      -        atomic_yaml_write(config_path, config)
      +        # Save back atomically while preserving comments, ordering, quotes, and
      +        # readable Unicode in user-edited config.yaml.
      +        from utils import atomic_roundtrip_yaml_update
      +        atomic_roundtrip_yaml_update(config_path, key_path, value)
               
               # Enforce owner-only permissions on config files (contain API keys)
               try:
    • pyproject.toml
      @@ -20,6 +20,7 @@ dependencies = [
         "rich>=14.3.3,<15",
         "tenacity>=9.1.4,<10",
         "pyyaml>=6.0.2,<7",
      +  "ruamel.yaml>=0.18.16,<0.19",
         "requests>=2.33.0,<3",  # CVE-2026-25645
         "jinja2>=3.1.5,<4",
         "pydantic>=2.12.5,<3",
    • scripts/release.py
      @@ -67,6 +67,7 @@
           "[redacted:email]": "kidonng",
           "[redacted:email]": "DanielLSM",
           "[redacted:email]": "v1b3coder",
      +    "[redacted:email]": "ming1523",
           "[redacted:email]": "Qwinty",
           "[redacted:email]": "CalmProton",
           "[redacted:email]": "mbac",
    • tests/cli/test_cli_save_config_value.py
      @@ -1,15 +1,13 @@
       """Tests for save_config_value() in cli.py — atomic write behavior."""
       
      -import os
       import yaml
      -from pathlib import Path
      -from unittest.mock import patch, MagicMock
      +from unittest.mock import MagicMock
       
       import pytest
       
       
       class TestSaveConfigValueAtomic:
      -    """save_config_value() must use atomic_yaml_write to avoid data loss."""
      +    """save_config_value() must use atomic round-trip YAML updates."""
       
           @pytest.fixture
           def config_env(self, tmp_path, monkeypatch):
      @@ -24,18 +22,15 @@ def config_env(self, tmp_path, monkeypatch):
               monkeypatch.setattr("cli._hermes_home", hermes_home)
               return config_path
       
      -    def test_calls_atomic_yaml_write(self, config_env, monkeypatch):
      -        """save_config_value must route through atomic_yaml_write, not bare open()."""
      -        mock_atomic = MagicMock()
      -        monkeypatch.setattr("utils.atomic_yaml_write", mock_atomic)
      +    def test_calls_roundtrip_yaml_update(self, config_env, monkeypatch):
      +        """save_config_value must preserve user-edited YAML structure."""
      +        mock_update = MagicMock()
      +        monkeypatch.setattr("utils.atomic_roundtrip_yaml_update", mock_update)
       
               from cli import save_config_value
               save_config_value("display.skin", "mono")
       
      -        mock_atomic.assert_called_once()
      -        written_path, written_data = mock_atomic.call_args[0]
      -        assert Path(written_path) == config_env
      -        assert written_data["display"]["skin"] == "mono"
      +        mock_update.assert_called_once_with(config_env, "display.skin", "mono")
       
           def test_preserves_existing_keys(self, config_env):
               """Writing a new key must not clobber existing config entries."""
      @@ -82,6 +77,47 @@ def test_preserves_env_ref_templates_in_unrelated_fields(self, config_env):
               assert result["model"]["default"] == "doubao-pro"
               assert result["custom_providers"][0]["api_key"] == "${TU_ZI_API_KEY}"
       
      +    def test_preserves_comments_after_config_mutation(self, config_env):
      +        """CLI config writes should not strip existing user comments."""
      +        config_env.write_text(
      +            "# user selected model\n"
      +            "model:\n"
      +            "  # keep this provider note\n"
      +            "  provider: openrouter\n"
      +            "display:\n"
      +            "  skin: default  # inline skin note\n",
      +            encoding="utf-8",
      +        )
      +
      +        from cli import save_config_value
      +        save_config_value("display.skin", "mono")
      +
      +        text = config_env.read_text(encoding="utf-8")
      +        result = yaml.safe_load(text)
      +        assert result["display"]["skin"] == "mono"
      +        assert "# user selected model" in text
      +        assert "# keep this provider note" in text
      +        assert "# inline skin note" in text
      +
      +    def test_preserves_readable_unicode_after_config_mutation(self, config_env):
      +        """Non-ASCII prompts should remain readable instead of \\u-escaped."""
      +        config_env.write_text(
      +            "agent:\n"
      +            "  system_prompt: 你好,保持中文输出\n"
      +            "display:\n"
      +            "  skin: default\n",
      +            encoding="utf-8",
      +        )
      +
      +        from cli import save_config_value
      +        save_config_value("display.skin", "mono")
      +
      +        text = config_env.read_text(encoding="utf-8")
      +        result = yaml.safe_load(text)
      +        assert result["agent"]["system_prompt"] == "你好,保持中文输出"
      +        assert "你好,保持中文输出" in text
      +        assert "\\u4f60" not in text
      +
           def test_file_not_truncated_on_error(self, config_env, monkeypatch):
               """If atomic_yaml_write raises, the original file is untouched."""
               original_content = config_env.read_text()
      @@ -89,7 +125,7 @@ def test_file_not_truncated_on_error(self, config_env, monkeypatch):
               def exploding_write(*args, **kwargs):
                   raise OSError("disk full")
       
      -        monkeypatch.setattr("utils.atomic_yaml_write", exploding_write)
      +        monkeypatch.setattr("utils.atomic_roundtrip_yaml_update", exploding_write)
       
               from cli import save_config_value
               result = save_config_value("display.skin", "broken")
    • utils.py
      @@ -188,6 +188,70 @@ def atomic_yaml_write(
               raise
       
       
      +def atomic_roundtrip_yaml_update(
      +    path: Union[str, Path],
      +    key_path: str,
      +    value: Any,
      +) -> None:
      +    """Update one dotted YAML key while preserving comments and readable text.
      +
      +    This is intentionally narrower than :func:`atomic_yaml_write`: it is for
      +    user-edited config files where comments, ordering, quoting, and Unicode
      +    should survive a single setting mutation.  Writes still use the same temp
      +    file + fsync + atomic replace pattern.
      +    """
      +    from ruamel.yaml import YAML
      +    from ruamel.yaml.comments import CommentedMap
      +
      +    path = Path(path)
      +    path.parent.mkdir(parents=True, exist_ok=True)
      +
      +    yaml_rt = YAML(typ="rt")
      +    yaml_rt.preserve_quotes = True
      +    yaml_rt.allow_unicode = True
      +    yaml_rt.default_flow_style = False
      +    yaml_rt.indent(mapping=2, sequence=4, offset=2)
      +
      +    if path.exists():
      +        with path.open("r", encoding="utf-8") as f:
      +            config = yaml_rt.load(f) or CommentedMap()
      +    else:
      +        config = CommentedMap()
      +
      +    if not isinstance(config, CommentedMap):
      +        config = CommentedMap(config)
      +
      +    current = config
      +    keys = key_path.split(".")
      +    for key in keys[:-1]:
      +        next_value = current.get(key)
      +        if not isinstance(next_value, CommentedMap):
      +            next_value = CommentedMap()
      +            current[key] = next_value
      +        current = next_value
      +    current[keys[-1]] = value
      +
      +    original_mode = _preserve_file_mode(path)
      +    fd, tmp_path = tempfile.mkstemp(
      +        dir=str(path.parent),
      +        prefix=f".{path.stem}_",
      +        suffix=".tmp",
      +    )
      +    try:
      +        with os.fdopen(fd, "w", encoding="utf-8") as f:
      +            yaml_rt.dump(config, f)
      +            f.flush()
      +            os.fsync(f.fileno())
      +        real_path = atomic_replace(tmp_path, path)
      +        _restore_file_mode(real_path, original_mode)
      +    except BaseException:
      +        try:
      +            os.unlink(tmp_path)
      +        except OSError:
      +            pass
      +        raise
      +
      +
       # ─── JSON Helpers ─────────────────────────────────────────────────────────────
       
       
    • uv.lock
      @@ -1976,6 +1976,7 @@ dependencies = [
           { name = "pyyaml" },
           { name = "requests" },
           { name = "rich" },
      +    { name = "ruamel-yaml" },
           { name = "tenacity" },
           { name = "tzdata", marker = "sys_platform == 'win32'" },
       ]
      @@ -2265,6 +2266,7 @@ requires-dist = [
           { name = "qrcode", marker = "extra == 'messaging'", specifier = ">=7.0,<8" },
           { name = "requests", specifier = ">=2.33.0,<3" },
           { name = "rich", specifier = ">=14.3.3,<15" },
      +    { name = "ruamel-yaml", specifier = ">=0.18.16,<0.19" },
           { name = "ruff", marker = "extra == 'dev'" },
           { name = "simple-term-menu", marker = "extra == 'cli'", specifier = ">=1.0,<2" },
           { name = "slack-bolt", marker = "extra == 'messaging'", specifier = ">=1.18.0,<2" },
      @@ -4912,6 +4914,66 @@ wheels = [
           { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
       ]
       
      +[[package]]
      +name = "ruamel-yaml"
      +version = "0.18.17"
      +source = { registry = "https://pypi.org/simple" }
      +dependencies = [
      +    { name = "ruamel-yaml-clib", marker = "python_full_version < '3.15' and platform_python_implementation == 'CPython'" },
      +]
      +sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" }
      +wheels = [
      +    { url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" },
      +]
      +
      +[[package]]
      +name = "ruamel-yaml-clib"
      +version = "0.2.15"
      +source = { registry = "https://pypi.org/simple" }
      +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" }
      +wheels = [
      +    { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" },
      +    { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" },
      +    { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" },
      +    { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" },
      +    { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" },
      +    { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" },
      +    { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" },
      +    { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" },
      +    { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" },
      +    { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" },
      +    { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" },
      +    { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" },
      +    { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" },
      +    { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" },
      +    { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" },
      +    { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" },
      +    { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" },
      +    { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" },
      +    { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" },
      +    { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" },
      +    { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" },
      +    { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" },
      +    { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" },
      +    { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" },
      +    { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" },
      +    { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" },
      +    { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" },
      +    { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" },
      +    { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" },
      +    { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" },
      +    { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" },
      +    { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" },
      +    { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" },
      +    { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" },
      +    { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" },
      +    { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" },
      +    { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" },
      +    { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" },
      +    { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" },
      +    { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" },
      +]
      +
       [[package]]
       name = "ruff"
       version = "0.15.10"
  • #52386 fix(utils): unify YAML list indent across all config writers (#31999)

    merged to main

    files: plugins/platforms/telegram/adapter.py, tests/test_yaml_indent_consistency_31999.py, tui_gateway/server.py, utils.py

  • #118918 config.yaml comments survive every writer: one comment-preserving writer + CI guard (#92554, #63039, #50698, #107511)

    merged to main

    files: .github/workflows/lint.yml, hermes_cli/AGENTS.md, hermes_cli/auth.py, hermes_cli/backup.py, hermes_cli/config.py, hermes_cli/credential_lifecycle.py, hermes_cli/profile_channels.py, hermes_cli/profiles.py, plugins/platforms/telegram/adapter.py, scripts/check_config_yaml_writers.py, tests/gateway/test_dm_topics.py, tests/hermes_cli/test_config.py, tests/hermes_cli/test_config_yaml_comment_preservation.py, tests/hermes_cli/test_model_provider_persistence.py, tui_gateway/server.py, utils.py

8 PRs closed without merging

From the close comments: done elsewhere 4 · merged via another change 2 · PR volume cleanup 1 · duplicate 1

  • #22717 fix(cli): preserve config comments on setting writes · merged via another change → #22845
  • #32034 fix(utils): unify YAML list indent across all config writers (#31999) · merged via another change → 0aea0c3, #52386, #32034
  • #50706 fix: preserve comments when using hermes config set · done elsewhere → 43504da, #118918, #92554, #63039, #50698
  • #63140 fix(cli): preserve YAML comments and formatting in set_config_value · duplicate → #50706, #63039
  • #63665 fix(config): preserve config set YAML comments · PR volume cleanup
Show 3 more
  • #72581 fix(config): preserve config.yaml comments/formatting in `hermes config set` + `unset` (#63039, salvage of #50706) · done elsewhere → 43504da, #118918, #92554, #63039, #50698
  • #107537 fix(config): preserve unrelated YAML list indent on config set · done elsewhere → 43504da, #118918, #92554, #63039, #50698
  • #111106 fix(config): preserve comments and key order in config set/unset · done elsewhere → 43504da, #118918, #92554, #63039, #50698
Closed issues · 5
  • Closed#10890fix: Preserve comments and Unicode in config.yaml when saving settings
  • Closed#10892refactor: atomic_yaml_write naming and implementation mismatch
  • Closed#31999YAML serialization inconsistency across 3 serializers causes config corruption and Gateway crash
  • Closed#50698hermes config set strips all comments and commented-out template blocks from config.yaml
  • Closed#63039config: set_config_value drops comments/formatting — diverges from save_config_value
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.13: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#22845, #52386, #118918
Closed issues
5
Attempts
8 PRs closed without merging
Fix link checked
22 Sep 2026

For your agent

The same facts, typed.