fix: improve sub-skill overwrite UX with content skip and collision protection#289
Merged
danielmeppiel merged 5 commits intomainfrom Mar 13, 2026
Merged
fix: improve sub-skill overwrite UX with content skip and collision protection#289danielmeppiel merged 5 commits intomainfrom
danielmeppiel merged 5 commits intomainfrom
Conversation
…rotection - Add content-identical skip: sub-skills with unchanged content are no longer removed/re-copied (uses filecmp byte-level comparison) - Add user-authored collision protection: skills not in managed_files are skipped with actionable 'use --force' message instead of being silently destroyed - Wire diagnostics to all integrate_package_skill() call sites so cross-package overwrites appear in the end-of-install summary instead of cluttering inline output - Improve diagnostic wording from 'sub-skills overwrote existing skills' to 'skills replaced by a different package (last installed wins)' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves the apm install experience for promoted sub-skills by reducing noisy overwrite warnings, adding content-based no-op behavior, and introducing collision protection plumbing consistent with the rest of the install pipeline.
Changes:
- Added directory-content equality checks to skip re-promoting sub-skills when source/target contents are identical.
- Threaded
diagnostics,managed_files, andforcethrough skill integration call paths to support deferred reporting and collision handling. - Updated diagnostic wording + docs, and added/updated unit tests for the new behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/integration/skill_integrator.py |
Adds directory comparison + collision/diagnostics logic to sub-skill promotion and threads new params through the integrator call chain. |
src/apm_cli/commands/install.py |
Passes diagnostics, managed_files, and force into skill integration calls. |
src/apm_cli/utils/diagnostics.py |
Updates overwrite summary wording and changes verbose rendering behavior. |
docs/src/content/docs/reference/cli-commands.md |
Updates docs to reflect the new diagnostic category wording. |
tests/unit/integration/test_skill_integrator.py |
Adds tests covering content-identical skip, collision behavior, and diagnostics routing. |
tests/unit/test_diagnostics.py |
Updates assertions for the new overwrite wording. |
Comments suppressed due to low confidence (3)
src/apm_cli/integration/skill_integrator.py:521
_promote_sub_skills()reimplements user-authored collision detection instead of usingBaseIntegrator.check_collision()even thoughSkillIntegratorextendsBaseIntegrator. Duplicating this logic increases drift risk and makes it harder to keep collision behavior consistent across integrators (e.g., path normalization, diagnostics formatting). Suggest delegating toself.check_collision(...)(orBaseIntegrator.check_collision) and centralizing any skill-specific behavior around it.
# Check if this is a user-authored skill (not managed by APM)
rel_dir = f".github/skills/{sub_name}"
is_managed = (
managed_files is not None
and rel_dir.replace("\\", "/") in managed_files
)
prev_owner = (owned_by or {}).get(sub_name)
is_self_overwrite = prev_owner is not None and prev_owner == parent_name
if managed_files is not None and not is_managed and not is_self_overwrite:
# User-authored skill — respect force flag
if not force:
if diagnostics is not None:
diagnostics.skip(
f"skills/{sub_name}/", package=parent_name
)
else:
try:
from apm_cli.utils.console import _rich_warning
_rich_warning(
f"Skipping skill '{sub_name}' — local skill exists (not managed by APM). "
f"Use 'apm install --force' to overwrite."
)
except ImportError:
pass
continue # SKIP — protect user content
src/apm_cli/integration/skill_integrator.py:493
_dirs_equal()assumes both inputs are directories. In_promote_sub_skills()this is called whentarget.exists(), buttargetcould be a file or symlink (user-created), which would raise fromfilecmp.dircmpbefore the collision-protection logic runs. Suggest guarding withtarget.is_dir()(treat non-dir as not equal) and handling non-directory targets explicitly (skip unless--force, or remove withunlinkwhen forcing).
if target.exists():
# Content-identical → skip entirely (no copy, no warning)
if SkillIntegrator._dirs_equal(sub_skill_path, target):
promoted += 1
deployed.append(target)
src/apm_cli/integration/skill_integrator.py:703
- In
_integrate_native_skill(),managed_files/forceare now threaded through, but within this method they’re only used for_promote_sub_skills(...)(sub-skill promotion). The main skill directory copy logic above still overwrites any existing.github/skills/<skill_name>/.claude/skills/<skill_name>unconditionally, so user-authored root-skill collisions remain unprotected. Consider applying the same collision semantics to the root skill directory too (ideally viaBaseIntegrator.check_collision).
sub_skills_dir = package_path / ".apm" / "skills"
github_skills_root = project_root / ".github" / "skills"
owned_by = self._build_skill_ownership_map(project_root)
sub_skills_count, sub_deployed = self._promote_sub_skills(sub_skills_dir, github_skills_root, skill_name, warn=True, owned_by=owned_by, diagnostics=diagnostics, managed_files=managed_files, force=force)
Codifies DX principles for CLI logging: Traffic Light Rule, So What Test, Newspaper Test, DiagnosticCollector usage, console helper conventions, and anti-patterns. Activates when editing user-facing terminal output code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Render diagnostic detail in verbose mode for overwrite group (previously detail was accepted but never displayed) - Use project-relative paths in diagnostic messages (.github/skills/name instead of skills/name) - Replace flaky mtime-based test with mock-based assertion - Add test for verbose detail rendering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add explicit 'Emojis are banned' rule to console helper section - Clarify STATUS_SYMBOLS renders ASCII text symbols, not emojis - Add anti-pattern #4 for emoji ban enforcement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel
added a commit
that referenced
this pull request
Mar 13, 2026
- Fix Codex runtime download 404: Windows assets use .exe.tar.gz naming - Configure UTF-8 encoding on Windows consoles to prevent UnicodeEncodeError - Resolve .cmd/.ps1 shell wrappers via shutil.which() on Windows - Replace GIT_CONFIG_GLOBAL=NUL with empty temp file for git compatibility - Update CHANGELOG v0.7.9 with Windows fixes, PR #289, PR #290 Closes #285 Co-authored-by: Sergio Sisternes <207026618+sergio-sisternes-epam@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When running
apm install, sub-skill overwrite warnings were:managed_files/forcecheck, silently destroying user-authored skills at the same path.Solution
Three fixes applied to
_promote_sub_skillsand its call chain:1. Content-identical skip
Before any collision logic, compare source and target directories using
filecmp.cmpfiles(shallow=False). If identical, skip entirely — no copy, no warning, no noise. Handles the common re-install case silently.2. User-authored collision protection
Thread
managed_filesandforcethrough the skill integrator call chain (matching the pattern of all other integrators). If target exists and is NOT inmanaged_files, skip with actionable warning:use --force to overwrite.3. Diagnostics wiring + wording
Pass
diagnosticsto all 3integrate_package_skill()call sites so cross-package overwrites appear in the end-of-install grouped summary instead of inline. Updated wording from "sub-skills overwrote existing skills" to "skills replaced by a different package (last installed wins)".Before / After
Before (re-install, nothing changed):
After (re-install, nothing changed): Content-identical skip — zero noise.
Changes
skill_integrator.py_dirs_equalhelper, content skip,managed_files/forcecollision protection, threaded params through call chaininstall.pydiagnostics,managed_files,forceto all 3integrate_package_skill()call sitesdiagnostics.pycli-commands.mdtest_skill_integrator.pytest_diagnostics.py