Repository navigation
fix(profile): preserve user-added skills and cron jobs on install --force / update - #44386
mohsinzahid wants to merge 1 commit into
Conversation
|
Code Review — Positive Verification Reviewed the full diff across What was checked:
Verdict: Clean. The merge-instead-of-mirror approach prevents data loss for users who install additional skills via |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Summary
Fixes hermes update/install --force to preserve user-added skills and cron jobs that were installed after the distribution was first deployed. Previously, distribution updates would wipe user additions inside distribution-owned directories.
Changes
- Adds
_replace_dir_merging_user_entries()that recursively merges directories, preserving user entries while replacing distribution-owned skill roots wholesale. - Distribution directories containing
SKILL.mdare replaced wholesale; other directories are merged recursively.
Review
- Good fix with clear documentation of the preservation semantics.
- No security concerns.
- Clean approach that distinguishes skill roots from general category folders.
Reviewed by Hermes Agent (batch cron)
The §4a extras-preservation block in install.sh works around hermes-agent deleting user-added skills on 'profile install --force'. That is fixed upstream by NousResearch/hermes-agent#44386 (issue #25120); note in both the block and its probe that §4a can be removed once the deployed hermes-agent includes the fix, verified by the probe passing with §4a reverted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating a real destructive update path. Current main still removes skills/ and cron/ wholesale in hermes_cli/profile_distribution.py:573-585, so the preservation direction is valuable.
Problems
hermes_cli/profile_distribution.py:581— the new file-copy branch callsshutil.copy2(child, node)without first removing a destination symlink. Unlike the directory branch at:570-573, this follows a file symlink. In a recursively merged user-preserved directory, an update can therefore overwrite the symlink target outside the profile tree.
Suggested changes
- Unlink destination symlinks before copying files and add a regression test covering a nested preserved-directory symlink.
- Update
website/docs/user-guide/profile-distributions.md:247and:356-358; they still describeskills/andcron/as replaced rather than merged.
Automated hermes-sweeper review.
| else: | ||
| if node.is_dir(): | ||
| shutil.rmtree(node) | ||
| shutil.copy2(child, node) |
There was a problem hiding this comment.
node.is_dir() is false for a symlink to a file, so copy2() follows that symlink and overwrites its target. Unlink node when node.is_symlink() before copying, and add a regression test using a nested symlink in a recursively merged directory.
There was a problem hiding this comment.
@teknium1 Thanks — addressed on exact head dc032d0f89ac8ebacad238bae23d078deb5ee357.
- The copy path now unlinks a destination symlink/file before
copy2(), so an external symlink target is not overwritten. - Added the requested nested symlink regression in a recursively merged cron directory; it proves the external target remains unchanged.
- Updated the profile-distribution docs for recursive skills/cron merge-preservation semantics.
- Local verification passed: nested regression (1 passed),
TestUserAdditionsSurviveReinstall(6 passed), fulltests/hermes_cli/test_profile_distribution.py(75 passed), Ruff on the modified Python files,git diff --check, the static symlink guard,profile update --help, and the website build (with only existing broken-link/anchor warnings).
Could you please re-review this exact head?
28219e2 to
dc032d0
Compare
Fixes #25120
What
hermes profile install --forceandhermes profile updateboth advertise "user data preserved" in their help text, but_copy_dist_payload()rmtree'd + recopied every directory the distribution ships. Anything the user added insideskills/after install was silently deleted — including skills installed withhermes skills install, which writes into the same tree. User-addedcron/jobs were deleted the same way.This is the destructive replacement reported in #25120, implemented along the lines of its "Option B — additive merge", with one refinement for distribution-owned skills (below).
How
A new
_replace_dir_merging_user_entries()replaces the wholesalermtree+copytreein_copy_dist_payload():SKILL.md) still replaces its counterpart wholesale, so files dropped or renamed by the new version don't linger inside distribution-owned skills;skills/devops/shared by the distribution andhermes skills install --category devops(the exact layout in Bug: hermes profile install/update destructively replaces local skills despite distribution_owned flag #25120's repro);One semantics change to be aware of: a skill the distribution removes in a new version now lingers in the profile instead of being deleted as a side effect of the mirror. If cleanup of dist-removed skills matters, the follow-up would be payload bookkeeping in the installed manifest (record shipped entries at install; delete previously-shipped-now-absent ones on update). Happy to iterate in that direction if you prefer.
Also removed: the
ignore=lambda previously passed tocopytree, which was dead code — it only filtered names when the visited directory equaled the staged root, but thatcopytreestarted one level below the root, so the predicate never fired. Top-level filtering already happens via thename in USER_OWNED_EXCLUDEguard at the top of the loop;TestNestedUserOwnedExcludeNotFilteredpasses unchanged.The module docstring's "Update semantics" section and the
profile updateparser description are updated to state the merge behavior, which makes the existing "(user data preserved)" help text on--forceandupdateaccurate.How to test
New tests in
tests/hermes_cli/test_profile_distribution.py::TestUserAdditionsSurviveReinstall— written first, red onmain, green with this change:test_force_install_preserves_user_added_skilltest_update_preserves_user_added_skill— dist's own skill still receives its new contenttest_update_preserves_user_skill_in_shared_category_dir— Bug: hermes profile install/update destructively replaces local skills despite distribution_owned flag #25120'sskills/<category>/<custom-skill>casetest_update_preserves_user_added_cron_jobtest_update_still_mirrors_dist_shipped_skill_dir— guard: stale files inside a dist-shipped skill must NOT linger (passes before and after)End-to-end repro (fails on stock
mainat e71d746, fixed with this branch):repro script
Before this change:
BUG: user-added skill was DELETED. After:OK: user-added skill preserved.Platforms
macOS (Darwin 25.2.0), Python 3.11.15. Full
scripts/run_tests.sh tests/hermes_cli/run: the only failures (11 tests across 4 gateway/service test files, plus 2 provider files with collection errors) are identical on a pristinemainworktree in this environment — pre-existing and unrelated to this change. The code path touched here is platform-independent (pathlib/shutilonly).🤖 Generated with Claude Code