Profile config sync and migration during updates
NousResearch/hermes-agent · cluster 16176 · hermes cli
Data as of
Nothing open on GitHub. Fix merged in #19869. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 3 issues already closed.
- Fix event
- A fix was merged to main in PR #19869. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
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.
Open issues0
Open PRs0
Fixes on record
-
#19869 fix(update): sync bundled skills to all profiles, including active
files:
hermes_cli/main.py,tests/hermes_cli/test_cmd_update.pyShow the diff
-
hermes_cli/main.py@@ -7069,20 +7069,22 @@ def _cmd_update_impl(args, gateway_mode: bool): except Exception as e: logger.debug("Skills sync during update failed: %s", e) - # Sync bundled skills to all other profiles + # Sync bundled skills to all profiles (including the active one). + # seed_profile_skills() uses subprocess with an explicit HERMES_HOME so + # it is not affected by sync_skills()'s module-level HERMES_HOME cache, + # which means the active profile is reliably synced regardless of whether + # the caller's HERMES_HOME env var points at the default or a named profile. try: from hermes_cli.profiles import ( list_profiles, - get_active_profile_name, seed_profile_skills, ) - active = get_active_profile_name() - other_profiles = [p for p in list_profiles() if p.name != active] - if other_profiles: + all_profiles = list_profiles() + if all_profiles: print() - print("→ Syncing bundled skills to other profiles...") - for p in other_profiles: + print("→ Syncing bundled skills to all profiles...") + for p in all_profiles: try: r = seed_profile_skills(p.path, quiet=True) if r: -
tests/hermes_cli/test_cmd_update.py@@ -163,3 +163,78 @@ def test_update_non_interactive_skips_migration_prompt(self, mock_args, capsys): mock_input.assert_not_called() captured = capsys.readouterr() assert "Non-interactive session" in captured.out + + +class TestCmdUpdateProfileSkillSync: + """cmd_update syncs bundled skills to all profiles, including the active one. + + Regression guard for #16176: previously the active profile was excluded + from the seed_profile_skills loop, leaving it on stale skill content. + """ + + @patch("shutil.which", return_value=None) + @patch("subprocess.run") + def test_active_profile_included_in_skill_sync( + self, mock_run, _mock_which, mock_args, capsys + ): + from pathlib import Path + + mock_run.side_effect = _make_run_side_effect( + branch="main", verify_ok=True, commit_count="1" + ) + + default_p = SimpleNamespace(name="default", path=Path("/fake/.hermes")) + active_p = SimpleNamespace(name="bit", path=Path("/fake/.hermes/profiles/bit")) + other_p = SimpleNamespace(name="work", path=Path("/fake/.hermes/profiles/work")) + all_profiles = [default_p, active_p, other_p] + + synced_paths = [] + + def fake_seed(path, quiet=False): + synced_paths.append(path) + return {"copied": [], "updated": [], "user_modified": []} + + empty_sync = {"copied": [], "updated": [], "user_modified": [], "cleaned": []} + + with ( + patch("hermes_cli.profiles.list_profiles", return_value=all_profiles), + patch("hermes_cli.profiles.seed_profile_skills", side_effect=fake_seed), + patch("tools.skills_sync.sync_skills", return_value=empty_sync), + ): + cmd_update(mock_args) + + assert active_p.path in synced_paths, ( + f"Active profile 'bit' must be included in skill sync; got: {synced_paths}" + ) + assert set(synced_paths) == {p.path for p in all_profiles}, ( + f"All profiles must be synced; got: {synced_paths}" + ) + + @patch("shutil.which", return_value=None) + @patch("subprocess.run") + def test_single_profile_default_is_synced( + self, mock_run, _mock_which, mock_args, capsys + ): + from pathlib import Path + + mock_run.side_effect = _make_run_side_effect( + branch="main", verify_ok=True, commit_count="1" + ) + + default_p = SimpleNamespace(name="default", path=Path("/fake/.hermes")) + synced_paths = [] + + def fake_seed(path, quiet=False): + synced_paths.append(path) + return {"copied": [], "updated": [], "user_modified": []} + + empty_sync = {"copied": [], "updated": [], "user_modified": [], "cleaned": []} + + with ( + patch("hermes_cli.profiles.list_profiles", return_value=[default_p]), + patch("hermes_cli.profiles.seed_profile_skills", side_effect=fake_seed), + patch("tools.skills_sync.sync_skills", return_value=empty_sync), + ): + cmd_update(mock_args) + + assert default_p.path in synced_paths
-
2 PRs closed without merging
From the close comments: merged via another change 1 · superseded by another PR 1
Closed issues · 3
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.31: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means 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 LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #19869
- Closed issues
- 3
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 4 May 2026