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

0open issues
0open PRs
6items grouped
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

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.

  • #19869 fix(update): sync bundled skills to all profiles, including active

    merged to main

    files: hermes_cli/main.py, tests/hermes_cli/test_cmd_update.py

    Show 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

  • #16217 fix(update): sync bundled skills to all profiles, including active (#16176) · merged via another change → #19869
  • #62492 fix(config): auto-migrate stale profile config on serve/dashboard startup · superseded by another PR → #91277
Closed issues · 3
  • Closed#16176`hermes update` skips bundled skill sync for the active profile
  • Closed#20438Config migration is profile-local; non-active profiles silently drift across Hermes upgrades
  • Closed#54926hermes update should migrate all profiles and clean up deprecated toolsets from config
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.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
hermes cli
Merged PRs
#19869
Closed issues
3
Attempts
2 PRs closed without merging
Fix link checked
4 May 2026

For your agent

The same facts, typed.