Skip to content

fix(cli): use additive merge in profile install/update to preserve local skills#25150

Open
zccyman wants to merge 1 commit into
NousResearch:mainfrom
atyou2happy:fix/profile-install-additive-merge
Open

fix(cli): use additive merge in profile install/update to preserve local skills#25150
zccyman wants to merge 1 commit into
NousResearch:mainfrom
atyou2happy:fix/profile-install-additive-merge

Conversation

@zccyman

@zccyman zccyman commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

_copy_dist_payload used shutil.rmtree + copytree for all directories, destroying locally-installed skills on every hermes profile install or hermes profile update. The distribution_owned manifest field was parsed but never enforced during the copy phase.

Before

# _copy_dist_payload — line 554-561
dest = target / name
if entry.is_dir():
    if dest.exists():
        shutil.rmtree(dest)    # ← deletes ALL local skills!
    shutil.copytree(entry, dest, ...)

Walking a profile with skills/devops/my-custom-skill/ → gone after update.

After

New _additive_copytree helper walks the source tree and copies/overwrites only files present in the distribution, preserving everything else:

def _additive_copytree(src: Path, dst: Path, ignore=None) -> None:
    if not dst.exists():
        shutil.copytree(src, dst, ignore=ignore)
        return
    for entry in src.iterdir():
        dest_entry = dst / entry.name
        if entry.is_dir():
            _additive_copytree(entry, dest_entry, ignore=ignore)  # recurse
        else:
            shutil.copy2(entry, dest_entry)  # overwrite only this file

Behavior

Scenario Before After
Install to empty profile Works Works (same)
Install to profile with local skills Local skills deleted Local skills preserved
Update distribution skill Works Works (overwrites only dist files)
Update with new dist skill Works Works (new skill added)
Nested directory merge Broken (rmtree kills everything) Correct (recursive additive)

Testing

  • 8 new tests:
    • 5 unit tests for _additive_copytree (new files, preserve local, overwrite dist, nested merge, ignore)
    • 3 integration tests for _copy_dist_payload (install preserves locals, update overwrites dist only, fresh install)
  • 71 existing profile distribution tests passing (0 regressions)

Files changed

File Change
hermes_cli/profile_distribution.py Add _additive_copytree helper; replace rmtree+copytree with additive merge
tests/hermes_cli/test_profile_distribution_additive.py New: 8 tests

Closes #25120

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard tool/skills Skills system (list, view, manage) labels May 13, 2026
…cal skills

_copy_dist_payload previously used shutil.rmtree + copytree for
directories, destroying all locally-installed skills on every profile
install or update.  The distribution_owned manifest field existed but
was never enforced during the copy phase.

New _additive_copytree helper walks the source tree and overwrites only
files present in the distribution, leaving local-only files intact.
This prevents data loss while still correctly updating distribution
shipped content.

Closes NousResearch#25120
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: hermes profile install/update destructively replaces local skills despite distribution_owned flag

2 participants