{"repo":"NousResearch/hermes-agent","public_id":"16176","display_label":"Profile config sync and migration during updates","summary":"These issues and PRs concern Hermes update and startup commands not consistently syncing bundled skills or migrating config across all profiles, with fixes targeting both the update flow and serve/dashboard startup.","subsystem":"hermes_cli","counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":19869},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-04T19:34:54+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.31466666666666665},"fix_edges":[{"issue_number":16176,"pr_number":19869,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":16176,"title":"`hermes update` skips bundled skill sync for the active profile","url":null,"state":"closed"},{"kind":"issue","number":20438,"title":"Config migration is profile-local; non-active profiles silently drift across Hermes upgrades","url":null,"state":"closed"},{"kind":"issue","number":54926,"title":"hermes update should migrate all profiles and clean up deprecated toolsets from config","url":null,"state":"closed"},{"kind":"pr","number":16217,"title":"fix(update): sync bundled skills to all profiles, including active (#16176)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19869","number":19869}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":19869,"title":"fix(update): sync bundled skills to all profiles, including active","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py","tests/hermes_cli/test_cmd_update.py"],"fix_hunks":[{"file":"hermes_cli/main.py","hunks":["@@ -7069,20 +7069,22 @@ def _cmd_update_impl(args, gateway_mode: bool):\n         except Exception as e:\n             logger.debug(\"Skills sync during update failed: %s\", e)\n \n-        # Sync bundled skills to all other profiles\n+        # Sync bundled skills to all profiles (including the active one).\n+        # seed_profile_skills() uses subprocess with an explicit HERMES_HOME so\n+        # it is not affected by sync_skills()'s module-level HERMES_HOME cache,\n+        # which means the active profile is reliably synced regardless of whether\n+        # the caller's HERMES_HOME env var points at the default or a named profile.\n         try:\n             from hermes_cli.profiles import (\n                 list_profiles,\n-                get_active_profile_name,\n                 seed_profile_skills,\n             )\n \n-            active = get_active_profile_name()\n-            other_profiles = [p for p in list_profiles() if p.name != active]\n-            if other_profiles:\n+            all_profiles = list_profiles()\n+            if all_profiles:\n                 print()\n-                print(\"→ Syncing bundled skills to other profiles...\")\n-                for p in other_profiles:\n+                print(\"→ Syncing bundled skills to all profiles...\")\n+                for p in all_profiles:\n                     try:\n                         r = seed_profile_skills(p.path, quiet=True)\n                         if r:"]},{"file":"tests/hermes_cli/test_cmd_update.py","hunks":["@@ -163,3 +163,78 @@ def test_update_non_interactive_skips_migration_prompt(self, mock_args, capsys):\n             mock_input.assert_not_called()\n             captured = capsys.readouterr()\n             assert \"Non-interactive session\" in captured.out\n+\n+\n+class TestCmdUpdateProfileSkillSync:\n+    \"\"\"cmd_update syncs bundled skills to all profiles, including the active one.\n+\n+    Regression guard for #16176: previously the active profile was excluded\n+    from the seed_profile_skills loop, leaving it on stale skill content.\n+    \"\"\"\n+\n+    @patch(\"shutil.which\", return_value=None)\n+    @patch(\"subprocess.run\")\n+    def test_active_profile_included_in_skill_sync(\n+        self, mock_run, _mock_which, mock_args, capsys\n+    ):\n+        from pathlib import Path\n+\n+        mock_run.side_effect = _make_run_side_effect(\n+            branch=\"main\", verify_ok=True, commit_count=\"1\"\n+        )\n+\n+        default_p = SimpleNamespace(name=\"default\", path=Path(\"/fake/.hermes\"))\n+        active_p = SimpleNamespace(name=\"bit\", path=Path(\"/fake/.hermes/profiles/bit\"))\n+        other_p = SimpleNamespace(name=\"work\", path=Path(\"/fake/.hermes/profiles/work\"))\n+        all_profiles = [default_p, active_p, other_p]\n+\n+        synced_paths = []\n+\n+        def fake_seed(path, quiet=False):\n+            synced_paths.append(path)\n+            return {\"copied\": [], \"updated\": [], \"user_modified\": []}\n+\n+        empty_sync = {\"copied\": [], \"updated\": [], \"user_modified\": [], \"cleaned\": []}\n+\n+        with (\n+            patch(\"hermes_cli.profiles.list_profiles\", return_value=all_profiles),\n+            patch(\"hermes_cli.profiles.seed_profile_skills\", side_effect=fake_seed),\n+            patch(\"tools.skills_sync.sync_skills\", return_value=empty_sync),\n+        ):\n+            cmd_update(mock_args)\n+\n+        assert active_p.path in synced_paths, (\n+            f\"Active profile 'bit' must be included in skill sync; got: {synced_paths}\"\n+        )\n+        assert set(synced_paths) == {p.path for p in all_profiles}, (\n+            f\"All profiles must be synced; got: {synced_paths}\"\n+        )\n+\n+    @patch(\"shutil.which\", return_value=None)\n+    @patch(\"subprocess.run\")\n+    def test_single_profile_default_is_synced(\n+        self, mock_run, _mock_which, mock_args, capsys\n+    ):\n+        from pathlib import Path\n+\n+        mock_run.side_effect = _make_run_side_effect(\n+            branch=\"main\", verify_ok=True, commit_count=\"1\"\n+        )\n+\n+        default_p = SimpleNamespace(name=\"default\", path=Path(\"/fake/.hermes\"))\n+        synced_paths = []\n+\n+        def fake_seed(path, quiet=False):\n+            synced_paths.append(path)\n+            return {\"copied\": [], \"updated\": [], \"user_modified\": []}\n+\n+        empty_sync = {\"copied\": [], \"updated\": [], \"user_modified\": [], \"cleaned\": []}\n+\n+        with (\n+            patch(\"hermes_cli.profiles.list_profiles\", return_value=[default_p]),\n+            patch(\"hermes_cli.profiles.seed_profile_skills\", side_effect=fake_seed),\n+            patch(\"tools.skills_sync.sync_skills\", return_value=empty_sync),\n+        ):\n+            cmd_update(mock_args)\n+\n+        assert default_p.path in synced_paths\n"]}]},{"kind":"pr","number":62492,"title":"fix(config): auto-migrate stale profile config on serve/dashboard startup","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#91277","number":91277}],"category":"superseded_by_pr","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}