diff --git a/src/apm_cli/integration/skill_integrator.py b/src/apm_cli/integration/skill_integrator.py index da372390f..f158961af 100644 --- a/src/apm_cli/integration/skill_integrator.py +++ b/src/apm_cli/integration/skill_integrator.py @@ -9,6 +9,7 @@ from pathlib import Path from apm_cli.integration.base_integrator import BaseIntegrator +from apm_cli.integration.targets import TargetProfile from apm_cli.utils.atomic_io import write_text_lf @@ -440,6 +441,20 @@ def __init__(self) -> None: # map so that same-manifest collisions are detected before the lockfile is written. self._native_skill_session_owners: dict[str, str] = {} + @staticmethod + def _target_skills_root(target: TargetProfile, project_root: Path) -> Path: + """Return the target skills root for static and dynamic-root targets.""" + if target.resolved_deploy_root is not None: + return target.deploy_path(project_root) + skills_mapping = target.primitives["skills"] + effective_root = skills_mapping.deploy_root or target.root_dir + return project_root / effective_root / "skills" + + @staticmethod + def _target_skill_dir(target: TargetProfile, project_root: Path, skill_name: str) -> Path: + """Return the concrete directory for a deployed skill.""" + return SkillIntegrator._target_skills_root(target, project_root) / skill_name + def find_instruction_files(self, package_path: Path) -> list[Path]: """Find all instruction files in a package. @@ -832,13 +847,7 @@ def _promote_sub_skills_standalone( continue is_primary = idx == 0 # first active target owns diagnostics - skills_mapping = target.primitives["skills"] - # Dynamic-root targets (cowork): use resolved_deploy_root. - if target.resolved_deploy_root is not None: - target_skills_root = target.resolved_deploy_root - else: - effective_root = skills_mapping.deploy_root or target.root_dir - target_skills_root = project_root / effective_root / "skills" + target_skills_root = self._target_skills_root(target, project_root) # Dedup: skip if same resolved skills root already processed. resolved_root = target_skills_root.resolve() @@ -974,12 +983,9 @@ def _integrate_native_skill( is_primary = idx == 0 # first active target owns diagnostics skills_mapping = target.primitives["skills"] - # Dynamic-root targets (cowork): use resolved_deploy_root. - if target.resolved_deploy_root is not None: - target_skill_dir = target.resolved_deploy_root / skill_name - else: - effective_root = skills_mapping.deploy_root or target.root_dir - target_skill_dir = project_root / effective_root / "skills" / skill_name + # Static targets still need the effective root for the containment guard below. + effective_root = skills_mapping.deploy_root or target.root_dir + target_skill_dir = self._target_skill_dir(target, project_root, skill_name) # Security: validate name + containment + symlink rejection. from apm_cli.utils.path_security import ( @@ -1072,11 +1078,8 @@ def _ignore_non_content_and_apm(directory, contents): if is_primary: files_copied = sum(1 for _ in target_skill_dir.rglob("*") if _.is_file()) - # Promote sub-skills for this target - if target.resolved_deploy_root is not None: - target_skills_root = target.resolved_deploy_root - else: - target_skills_root = project_root / effective_root / "skills" + # Promote sub-skills for this target. + target_skills_root = self._target_skills_root(target, project_root) _, sub_deployed = self._promote_sub_skills( sub_skills_dir, target_skills_root, diff --git a/tests/unit/integration/test_skill_integrator_hermetic.py b/tests/unit/integration/test_skill_integrator_hermetic.py index d83f868e4..86554d18e 100644 --- a/tests/unit/integration/test_skill_integrator_hermetic.py +++ b/tests/unit/integration/test_skill_integrator_hermetic.py @@ -63,6 +63,11 @@ def _make_target( target.root_dir = root_dir target.auto_create = auto_create target.resolved_deploy_root = resolved_deploy_root + target.deploy_path = MagicMock( + side_effect=lambda project_root, *parts: ( + resolved_deploy_root if resolved_deploy_root is not None else project_root / root_dir + ).joinpath(*parts) + ) prim = MagicMock() mapping = MagicMock() mapping.deploy_root = deploy_root diff --git a/tests/unit/integration/test_skill_integrator_phase3w4.py b/tests/unit/integration/test_skill_integrator_phase3w4.py index f7030d94f..639270fed 100644 --- a/tests/unit/integration/test_skill_integrator_phase3w4.py +++ b/tests/unit/integration/test_skill_integrator_phase3w4.py @@ -63,6 +63,11 @@ def _make_target( target.root_dir = root_dir target.auto_create = auto_create target.resolved_deploy_root = resolved_deploy_root + target.deploy_path = MagicMock( + side_effect=lambda project_root, *parts: ( + resolved_deploy_root if resolved_deploy_root is not None else project_root / root_dir + ).joinpath(*parts) + ) prim = MagicMock() mapping = MagicMock() mapping.deploy_root = deploy_root