Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Skills from third-party and transitive dependencies now deploy to `.claude/skills/` and `.opencode/skills/` when the install target includes those platforms (`target: all`, `target: claude`, `target: opencode`)
Comment on lines +11 to +13
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog entry format: per Keep a Changelog/SemVer conventions in this repo, each bullet should be a single concise line ending with the PR number like (#123). This new entry is missing the (#PR_NUMBER) suffix (and should keep to one line per PR).

Copilot generated this review using guidance from repository custom instructions.

## [0.8.5] - 2026-03-24

### Added
Expand Down
2 changes: 2 additions & 0 deletions src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,8 @@ def _log_integration(msg):
skill_result = skill_integrator.integrate_package_skill(
package_info, project_root,
diagnostics=diagnostics, managed_files=managed_files, force=force,
integrate_claude=integrate_claude,
integrate_opencode=integrate_opencode,
)
if skill_result.skill_created:
result["skills"] += 1
Expand Down
67 changes: 54 additions & 13 deletions src/apm_cli/integration/skill_integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ def _build_skill_ownership_map(project_root: Path) -> dict[str, str]:
def _promote_sub_skills_standalone(
self, package_info, project_root: Path, diagnostics=None,
managed_files=None, force: bool = False, logger=None,
integrate_claude: bool = False, integrate_opencode: bool = False,
) -> tuple[int, list[Path]]:
"""Promote sub-skills from a package that is NOT itself a skill.

Expand All @@ -592,6 +593,10 @@ def _promote_sub_skills_standalone(
Args:
package_info: PackageInfo object with package metadata.
project_root: Root directory of the project.
integrate_claude: When True, deploy to .claude/skills/ even if
.claude/ does not yet exist.
integrate_opencode: When True, deploy to .opencode/skills/ even if
.opencode/ does not yet exist.
Comment on lines 593 to +599
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method-level docstring above still describes promotion to .github/skills/ and only .claude/skills/ "when present", but the code also promotes to .cursor/skills/ and .opencode/skills/ and can create .claude/.opencode when integrate_* flags are true. Please update the docstring text so it matches the actual target behavior.

This issue also appears on line 653 of the same file.

See below for a potential fix:

        ``.apm/skills/``. This method promotes them to ``.github/skills/``
        and also:

        - ``.claude/skills/`` when ``integrate_claude`` is True or a
          ``.claude/`` directory already exists (creating it if needed)
        - ``.cursor/skills/`` when a ``.cursor/`` directory exists
        - ``.opencode/skills/`` when ``integrate_opencode`` is True or an
          ``.opencode/`` directory already exists (creating it if needed)

        This promotion does not create a top-level skill entry for the
        parent package.

        Args:
            package_info: PackageInfo object with package metadata.
            project_root: Root directory of the project.
            integrate_claude: When True, deploy to .claude/skills/ even if
                .claude/ does not yet exist (the directory may be created).
            integrate_opencode: When True, deploy to .opencode/skills/ even if
                .opencode/ does not yet exist (the directory may be created).

Copilot uses AI. Check for mistakes.

Returns:
tuple[int, list[Path]]: (count of promoted sub-skills, list of deployed dirs)
Expand All @@ -610,9 +615,9 @@ def _promote_sub_skills_standalone(
)
all_deployed = list(deployed)

# Also promote into .claude/skills/ when .claude/ exists
# Also promote into .claude/skills/ when .claude/ exists or target requests it
claude_dir = project_root / ".claude"
if claude_dir.exists() and claude_dir.is_dir():
if integrate_claude or (claude_dir.exists() and claude_dir.is_dir()):
claude_skills_root = claude_dir / "skills"
_, claude_deployed = self._promote_sub_skills(
sub_skills_dir, claude_skills_root, parent_name, warn=False, project_root=project_root
Expand All @@ -628,9 +633,9 @@ def _promote_sub_skills_standalone(
)
all_deployed.extend(cursor_deployed)

# Also promote into .opencode/skills/ when .opencode/ exists
# Also promote into .opencode/skills/ when .opencode/ exists or target requests it
opencode_dir = project_root / ".opencode"
if opencode_dir.exists() and opencode_dir.is_dir():
if integrate_opencode or (opencode_dir.exists() and opencode_dir.is_dir()):
opencode_skills_root = opencode_dir / "skills"
_, opencode_deployed = self._promote_sub_skills(
sub_skills_dir, opencode_skills_root, parent_name, warn=False, project_root=project_root
Expand All @@ -643,6 +648,7 @@ def _integrate_native_skill(
self, package_info, project_root: Path, source_skill_md: Path,
diagnostics=None, managed_files=None, force: bool = False,
logger=None,
integrate_claude: bool = False, integrate_opencode: bool = False,
) -> SkillIntegrationResult:
"""Copy a native Skill (with existing SKILL.md) to .github/skills/ and optionally .claude/skills/ and .cursor/skills/.

Expand All @@ -657,9 +663,11 @@ def _integrate_native_skill(
detection uses apm.lock via directory name matching instead.

T7 Enhancement: Also copies to .claude/skills/ when .claude/ folder exists
and to .cursor/skills/ when .cursor/ folder exists.
This ensures Claude Code and Cursor users get skills while not polluting
projects that don't use those tools.
or integrate_claude is True, and to .cursor/skills/ when .cursor/ folder
exists, and to .opencode/skills/ when .opencode/ folder exists or
integrate_opencode is True.
This ensures Claude Code, Cursor, and OpenCode users get skills while not
polluting projects that don't use those tools.

Copies:
- SKILL.md (required)
Expand All @@ -672,6 +680,10 @@ def _integrate_native_skill(
package_info: PackageInfo object with package metadata
project_root: Root directory of the project
source_skill_md: Path to the source SKILL.md file
integrate_claude: When True, deploy to .claude/skills/ even if
.claude/ does not yet exist.
integrate_opencode: When True, deploy to .opencode/skills/ even if
.opencode/ does not yet exist.

Returns:
SkillIntegrationResult: Results of the integration operation
Expand Down Expand Up @@ -743,7 +755,7 @@ def _integrate_native_skill(

# === T7: Copy to .claude/skills/ (secondary - compatibility) ===
claude_dir = project_root / ".claude"
if claude_dir.exists() and claude_dir.is_dir():
if integrate_claude or (claude_dir.exists() and claude_dir.is_dir()):
claude_skill_dir = claude_dir / "skills" / skill_name

if claude_skill_dir.exists():
Expand Down Expand Up @@ -777,6 +789,24 @@ def _integrate_native_skill(
_, cursor_sub_deployed = self._promote_sub_skills(sub_skills_dir, cursor_skills_root, skill_name, warn=False, project_root=project_root)
all_target_paths.extend(cursor_sub_deployed)

# === Copy to .opencode/skills/ (compatibility) ===
opencode_dir = project_root / ".opencode"
if integrate_opencode or (opencode_dir.exists() and opencode_dir.is_dir()):
opencode_skill_dir = opencode_dir / "skills" / skill_name

if opencode_skill_dir.exists():
shutil.rmtree(opencode_skill_dir)

opencode_skill_dir.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(package_path, opencode_skill_dir,
ignore=shutil.ignore_patterns('.apm'))
all_target_paths.append(opencode_skill_dir)

# Promote sub-skills for OpenCode too
opencode_skills_root = opencode_dir / "skills"
_, opencode_sub_deployed = self._promote_sub_skills(sub_skills_dir, opencode_skills_root, skill_name, warn=False, project_root=project_root)
all_target_paths.extend(opencode_sub_deployed)

return SkillIntegrationResult(
skill_created=skill_created,
skill_updated=skill_updated,
Expand All @@ -788,18 +818,23 @@ def _integrate_native_skill(
target_paths=all_target_paths
)

def integrate_package_skill(self, package_info, project_root: Path, diagnostics=None, managed_files=None, force: bool = False, logger=None) -> SkillIntegrationResult:
def integrate_package_skill(self, package_info, project_root: Path, diagnostics=None, managed_files=None, force: bool = False, logger=None, integrate_claude: bool = False, integrate_opencode: bool = False) -> SkillIntegrationResult:
"""Integrate a package's skill into .github/skills/ directory.

Copies native skills (packages with SKILL.md at root) to .github/skills/
and optionally .claude/skills/ and .cursor/skills/. Also promotes any sub-skills from .apm/skills/.
and optionally .claude/skills/, .cursor/skills/, and .opencode/skills/.
Also promotes any sub-skills from .apm/skills/.

Packages without SKILL.md at root are not installed as skills -- only their
sub-skills (if any) are promoted.

Args:
package_info: PackageInfo object with package metadata
project_root: Root directory of the project
integrate_claude: When True, deploy to .claude/skills/ even if
.claude/ does not yet exist.
integrate_opencode: When True, deploy to .opencode/skills/ even if
.opencode/ does not yet exist.
Comment on lines +834 to +837
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior change: with integrate_claude/integrate_opencode True, this integrator will now deploy (and create) .claude/skills/ / .opencode/skills/ even when those root dirs do not already exist. Several docs pages currently state OpenCode/Claude skill deployment happens only when .opencode//.claude/ already exists (e.g., docs/src/content/docs/guides/skills.md and integrations/ide-tool-integration.md). Please update the docs to reflect the new flag-based behavior (and how users enable it via target: claude|opencode|all).

Copilot uses AI. Check for mistakes.

Returns:
SkillIntegrationResult: Results of the integration operation
Expand All @@ -811,7 +846,8 @@ def integrate_package_skill(self, package_info, project_root: Path, diagnostics=
# Even non-skill packages may ship sub-skills under .apm/skills/.
# Promote them so Copilot can discover them independently.
sub_skills_count, sub_deployed = self._promote_sub_skills_standalone(
package_info, project_root, diagnostics=diagnostics, managed_files=managed_files, force=force, logger=logger
package_info, project_root, diagnostics=diagnostics, managed_files=managed_files, force=force, logger=logger,
integrate_claude=integrate_claude, integrate_opencode=integrate_opencode,
)
return SkillIntegrationResult(
skill_created=False,
Expand Down Expand Up @@ -844,12 +880,17 @@ def integrate_package_skill(self, package_info, project_root: Path, diagnostics=
# Check if this is a native Skill (already has SKILL.md at root)
source_skill_md = package_path / "SKILL.md"
if source_skill_md.exists():
return self._integrate_native_skill(package_info, project_root, source_skill_md, diagnostics=diagnostics, managed_files=managed_files, force=force, logger=logger)
return self._integrate_native_skill(
package_info, project_root, source_skill_md,
diagnostics=diagnostics, managed_files=managed_files, force=force, logger=logger,
integrate_claude=integrate_claude, integrate_opencode=integrate_opencode,
)

# No SKILL.md at root -- not a skill package.
# Still promote any sub-skills shipped under .apm/skills/.
sub_skills_count, sub_deployed = self._promote_sub_skills_standalone(
package_info, project_root, diagnostics=diagnostics, managed_files=managed_files, force=force, logger=logger
package_info, project_root, diagnostics=diagnostics, managed_files=managed_files, force=force, logger=logger,
integrate_claude=integrate_claude, integrate_opencode=integrate_opencode,
)
return SkillIntegrationResult(
skill_created=False,
Expand Down
Loading
Loading