diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c38126a1..e7bbdfe40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Fixed
+
+- `--target opencode` no longer creates `.github/` directory -- target-driven dispatch via `INTEGRATION_DISPATCH` registry replaces per-target boolean flags (#470)
+
+### Changed
+
+- Refactored `_integrate_package_primitives()` to use declarative `INTEGRATION_DISPATCH` registry in `targets.py` instead of monolithic if-chains (#470)
+
 ## [0.8.6] - 2026-03-27
 
 ### Added
diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py
index bbda5af15..ba79d634d 100644
--- a/src/apm_cli/commands/install.py
+++ b/src/apm_cli/commands/install.py
@@ -837,9 +837,7 @@ def _integrate_package_primitives(
     package_info,
     project_root,
     *,
-    integrate_vscode,
-    integrate_claude,
-    integrate_opencode=False,
+    targets,
     prompt_integrator,
     agent_integrator,
     skill_integrator,
@@ -854,215 +852,34 @@ def _integrate_package_primitives(
 ):
     """Run the full integration pipeline for a single package.
 
+    Delegates to ``integrate_package_for_targets()`` which iterates the
+    active ``TargetProfile`` list and dispatches to the correct integrator
+    method for each supported primitive.  This function is a thin adapter
+    that keeps the call-site signature stable.
+
     Returns a dict with integration counters and the list of deployed file paths.
     """
-    result = {
-        "prompts": 0,
-        "agents": 0,
-        "skills": 0,
-        "sub_skills": 0,
-        "instructions": 0,
-        "commands": 0,
-        "hooks": 0,
-        "links_resolved": 0,
-        "deployed_files": [],
+    from apm_cli.integration.targets import integrate_package_for_targets
+
+    integrators = {
+        "prompt_integrator": prompt_integrator,
+        "agent_integrator": agent_integrator,
+        "skill_integrator": skill_integrator,
+        "instruction_integrator": instruction_integrator,
+        "command_integrator": command_integrator,
+        "hook_integrator": hook_integrator,
     }
 
-    deployed = result["deployed_files"]
-
-    if not (integrate_vscode or integrate_claude or integrate_opencode):
-        return result
-
-    def _log_integration(msg):
-        if logger:
-            logger.tree_item(msg)
-
-    # --- prompts ---
-    prompt_result = prompt_integrator.integrate_package_prompts(
-        package_info, project_root,
-        force=force, managed_files=managed_files,
-        diagnostics=diagnostics,
-    )
-    if prompt_result.files_integrated > 0:
-        result["prompts"] += prompt_result.files_integrated
-        _log_integration(f"  └─ {prompt_result.files_integrated} prompts integrated -> .github/prompts/")
-    if prompt_result.files_updated > 0:
-        _log_integration(f"  └─ {prompt_result.files_updated} prompts updated")
-    result["links_resolved"] += prompt_result.links_resolved
-    for tp in prompt_result.target_paths:
-        deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- agents (.github) ---
-    agent_result = agent_integrator.integrate_package_agents(
-        package_info, project_root,
-        force=force, managed_files=managed_files,
+    return integrate_package_for_targets(
+        targets,
+        package_info,
+        project_root,
+        integrators,
+        force=force,
+        managed_files=managed_files,
         diagnostics=diagnostics,
+        logger=logger,
     )
-    if agent_result.files_integrated > 0:
-        result["agents"] += agent_result.files_integrated
-        _log_integration(f"  └─ {agent_result.files_integrated} agents integrated -> .github/agents/")
-    if agent_result.files_updated > 0:
-        _log_integration(f"  └─ {agent_result.files_updated} agents updated")
-    result["links_resolved"] += agent_result.links_resolved
-    for tp in agent_result.target_paths:
-        deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- skills ---
-    if integrate_vscode or integrate_claude or integrate_opencode:
-        skill_result = skill_integrator.integrate_package_skill(
-            package_info, project_root,
-            diagnostics=diagnostics, managed_files=managed_files, force=force,
-        )
-        # Build human-readable list of target dirs from deployed paths
-        _skill_target_dirs: set[str] = set()
-        for tp in skill_result.target_paths:
-            rel = tp.relative_to(project_root)
-            if rel.parts:
-                _skill_target_dirs.add(rel.parts[0])
-        _skill_targets = sorted(_skill_target_dirs)
-        _skill_target_str = ", ".join(f"{d}/skills/" for d in _skill_targets) or "skills/"
-        if skill_result.skill_created:
-            result["skills"] += 1
-            _log_integration(f"  |-- Skill integrated -> {_skill_target_str}")
-        if skill_result.sub_skills_promoted > 0:
-            result["sub_skills"] += skill_result.sub_skills_promoted
-            _log_integration(f"  |-- {skill_result.sub_skills_promoted} skill(s) integrated -> {_skill_target_str}")
-        for tp in skill_result.target_paths:
-            deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- instructions (.github) ---
-    if integrate_vscode:
-        instruction_result = instruction_integrator.integrate_package_instructions(
-            package_info, project_root,
-            force=force, managed_files=managed_files,
-            diagnostics=diagnostics,
-        )
-        if instruction_result.files_integrated > 0:
-            result["instructions"] += instruction_result.files_integrated
-            _log_integration(f"  └─ {instruction_result.files_integrated} instruction(s) integrated -> .github/instructions/")
-        result["links_resolved"] += instruction_result.links_resolved
-        for tp in instruction_result.target_paths:
-            deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- Cursor rules (.cursor/rules/) ---
-    cursor_rules_result = instruction_integrator.integrate_package_instructions_cursor(
-        package_info, project_root,
-        force=force, managed_files=managed_files,
-        diagnostics=diagnostics,
-    )
-    if cursor_rules_result.files_integrated > 0:
-        result["instructions"] += cursor_rules_result.files_integrated
-        _log_integration(f"  └─ {cursor_rules_result.files_integrated} rule(s) integrated -> .cursor/rules/")
-    result["links_resolved"] += cursor_rules_result.links_resolved
-    for tp in cursor_rules_result.target_paths:
-        deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- Claude agents (.claude) ---
-    if integrate_claude:
-        claude_agent_result = agent_integrator.integrate_package_agents_claude(
-            package_info, project_root,
-            force=force, managed_files=managed_files,
-            diagnostics=diagnostics,
-        )
-        if claude_agent_result.files_integrated > 0:
-            result["agents"] += claude_agent_result.files_integrated
-            _log_integration(f"  └─ {claude_agent_result.files_integrated} agents integrated -> .claude/agents/")
-        result["links_resolved"] += claude_agent_result.links_resolved
-        for tp in claude_agent_result.target_paths:
-            deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- Cursor agents (.cursor) ---
-    cursor_agent_result = agent_integrator.integrate_package_agents_cursor(
-        package_info, project_root,
-        force=force, managed_files=managed_files,
-        diagnostics=diagnostics,
-    )
-    if cursor_agent_result.files_integrated > 0:
-        result["agents"] += cursor_agent_result.files_integrated
-        _log_integration(f"  └─ {cursor_agent_result.files_integrated} agents integrated -> .cursor/agents/")
-    result["links_resolved"] += cursor_agent_result.links_resolved
-    for tp in cursor_agent_result.target_paths:
-        deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- OpenCode agents (.opencode) ---
-    opencode_agent_result = agent_integrator.integrate_package_agents_opencode(
-        package_info, project_root,
-        force=force, managed_files=managed_files,
-        diagnostics=diagnostics,
-    )
-    if opencode_agent_result.files_integrated > 0:
-        result["agents"] += opencode_agent_result.files_integrated
-        _log_integration(f"  └─ {opencode_agent_result.files_integrated} agents integrated -> .opencode/agents/")
-    result["links_resolved"] += opencode_agent_result.links_resolved
-    for tp in opencode_agent_result.target_paths:
-        deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- commands (.claude) ---
-    if integrate_claude:
-        command_result = command_integrator.integrate_package_commands(
-            package_info, project_root,
-            force=force, managed_files=managed_files,
-            diagnostics=diagnostics,
-        )
-        if command_result.files_integrated > 0:
-            result["commands"] += command_result.files_integrated
-            _log_integration(f"  └─ {command_result.files_integrated} commands integrated -> .claude/commands/")
-        if command_result.files_updated > 0:
-            _log_integration(f"  └─ {command_result.files_updated} commands updated")
-        result["links_resolved"] += command_result.links_resolved
-        for tp in command_result.target_paths:
-            deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- OpenCode commands (.opencode) ---
-    opencode_command_result = command_integrator.integrate_package_commands_opencode(
-        package_info, project_root,
-        force=force, managed_files=managed_files,
-        diagnostics=diagnostics,
-    )
-    if opencode_command_result.files_integrated > 0:
-        result["commands"] += opencode_command_result.files_integrated
-        _log_integration(f"  └─ {opencode_command_result.files_integrated} commands integrated -> .opencode/commands/")
-    result["links_resolved"] += opencode_command_result.links_resolved
-    for tp in opencode_command_result.target_paths:
-        deployed.append(tp.relative_to(project_root).as_posix())
-
-    # --- hooks ---
-    if integrate_vscode:
-        hook_result = hook_integrator.integrate_package_hooks(
-            package_info, project_root,
-            force=force, managed_files=managed_files,
-            diagnostics=diagnostics,
-        )
-        if hook_result.hooks_integrated > 0:
-            result["hooks"] += hook_result.hooks_integrated
-            _log_integration(f"  └─ {hook_result.hooks_integrated} hook(s) integrated -> .github/hooks/")
-        for tp in hook_result.target_paths:
-            deployed.append(tp.relative_to(project_root).as_posix())
-    if integrate_claude:
-        hook_result_claude = hook_integrator.integrate_package_hooks_claude(
-            package_info, project_root,
-            force=force, managed_files=managed_files,
-            diagnostics=diagnostics,
-        )
-        if hook_result_claude.hooks_integrated > 0:
-            result["hooks"] += hook_result_claude.hooks_integrated
-            _log_integration(f"  └─ {hook_result_claude.hooks_integrated} hook(s) integrated -> .claude/settings.json")
-        for tp in hook_result_claude.target_paths:
-            deployed.append(tp.relative_to(project_root).as_posix())
-
-    # Cursor hooks (.cursor/hooks.json) — method self-guards on .cursor/ existence
-    hook_result_cursor = hook_integrator.integrate_package_hooks_cursor(
-        package_info, project_root,
-        force=force, managed_files=managed_files,
-        diagnostics=diagnostics,
-    )
-    if hook_result_cursor.hooks_integrated > 0:
-        result["hooks"] += hook_result_cursor.hooks_integrated
-        _log_integration(f"  └─ {hook_result_cursor.hooks_integrated} hook(s) integrated -> .cursor/hooks.json")
-    for tp in hook_result_cursor.target_paths:
-        deployed.append(tp.relative_to(project_root).as_posix())
-
-    return result
 
 
 
@@ -1349,9 +1166,6 @@ def _collect_descendants(node, visited=None):
         # Auto-detect target for integration (same logic as compile)
         from apm_cli.core.target_detection import (
             detect_target,
-            should_integrate_vscode,
-            should_integrate_claude,
-            should_integrate_opencode,
             get_target_description,
         )
 
@@ -1382,11 +1196,6 @@ def _collect_descendants(node, visited=None):
             config_target=config_target,
         )
 
-        # Determine which integrations to run based on detected target
-        integrate_vscode = should_integrate_vscode(detected_target)
-        integrate_claude = should_integrate_claude(detected_target)
-        integrate_opencode = should_integrate_opencode(detected_target)
-
         # Initialize integrators
         prompt_integrator = PromptIntegrator()
         agent_integrator = AgentIntegrator()
@@ -1647,9 +1456,7 @@ def _collect_descendants(node, visited=None):
 
                         int_result = _integrate_package_primitives(
                             package_info, project_root,
-                            integrate_vscode=integrate_vscode,
-                            integrate_claude=integrate_claude,
-                            integrate_opencode=integrate_opencode,
+                            targets=_targets,
                             prompt_integrator=prompt_integrator,
                             agent_integrator=agent_integrator,
                             skill_integrator=skill_integrator,
@@ -1766,7 +1573,7 @@ def _collect_descendants(node, visited=None):
                         unpinned_count += 1
 
                     # Skip integration if not needed
-                    if not (integrate_vscode or integrate_claude or integrate_opencode):
+                    if not _targets:
                         continue
 
                     # Integrate prompts for cached packages (zero-config behavior)
@@ -1851,9 +1658,7 @@ def _collect_descendants(node, visited=None):
 
                         int_result = _integrate_package_primitives(
                             cached_package_info, project_root,
-                            integrate_vscode=integrate_vscode,
-                            integrate_claude=integrate_claude,
-                            integrate_opencode=integrate_opencode,
+                            targets=_targets,
                             prompt_integrator=prompt_integrator,
                             agent_integrator=agent_integrator,
                             skill_integrator=skill_integrator,
@@ -2013,13 +1818,11 @@ def _collect_descendants(node, visited=None):
                         package_deployed_files[dep_ref.get_unique_key()] = []
                         continue
 
-                    if integrate_vscode or integrate_claude or integrate_opencode:
+                    if _targets:
                         try:
                             int_result = _integrate_package_primitives(
                                 package_info, project_root,
-                                integrate_vscode=integrate_vscode,
-                                integrate_claude=integrate_claude,
-                                integrate_opencode=integrate_opencode,
+                                targets=_targets,
                                 prompt_integrator=prompt_integrator,
                                 agent_integrator=agent_integrator,
                                 skill_integrator=skill_integrator,
diff --git a/src/apm_cli/integration/__init__.py b/src/apm_cli/integration/__init__.py
index 356f871de..2868bc173 100644
--- a/src/apm_cli/integration/__init__.py
+++ b/src/apm_cli/integration/__init__.py
@@ -21,8 +21,10 @@
     TargetProfile,
     PrimitiveMapping,
     KNOWN_TARGETS,
+    INTEGRATION_DISPATCH,
     get_integration_prefixes,
     active_targets,
+    integrate_package_for_targets,
 )
 
 __all__ = [
@@ -38,8 +40,10 @@
     'TargetProfile',
     'PrimitiveMapping',
     'KNOWN_TARGETS',
+    'INTEGRATION_DISPATCH',
     'get_integration_prefixes',
     'active_targets',
+    'integrate_package_for_targets',
     'validate_skill_name',
     'normalize_skill_name',
     'to_hyphen_case',
diff --git a/src/apm_cli/integration/targets.py b/src/apm_cli/integration/targets.py
index 9ee18e1ba..7e3b1f07f 100644
--- a/src/apm_cli/integration/targets.py
+++ b/src/apm_cli/integration/targets.py
@@ -205,3 +205,237 @@ def active_targets(project_root, explicit_target: "Optional[str]" = None) -> lis
 
     # --- fallback: copilot is the universal default ---
     return [KNOWN_TARGETS["copilot"]]
+
+
+# ------------------------------------------------------------------
+# Integration dispatch registry
+# ------------------------------------------------------------------
+#
+# Maps (target_name, primitive_name) -> (integrator_key, method_name, log_dir).
+#
+# ``integrator_key`` selects from the integrator dict passed to
+# ``integrate_package_for_targets``.
+#
+# ``method_name`` is the method to call on that integrator.
+#
+# ``log_dir`` is the human-readable deploy directory for log output.
+#
+# Skills are excluded from per-target dispatch because
+# ``SkillIntegrator.integrate_package_skill`` already handles
+# multi-target routing internally via ``active_targets()``.
+#
+# Adding a new target or primitive only requires:
+# 1. An entry in KNOWN_TARGETS above
+# 2. An entry in INTEGRATION_DISPATCH below
+# 3. The integrator method it references
+
+INTEGRATION_DISPATCH: Dict[tuple, tuple] = {
+    # --- copilot (.github) ---
+    ("copilot", "prompts"): (
+        "prompt_integrator", "integrate_package_prompts",
+        ".github/prompts/",
+    ),
+    ("copilot", "agents"): (
+        "agent_integrator", "integrate_package_agents",
+        ".github/agents/",
+    ),
+    ("copilot", "instructions"): (
+        "instruction_integrator", "integrate_package_instructions",
+        ".github/instructions/",
+    ),
+    ("copilot", "hooks"): (
+        "hook_integrator", "integrate_package_hooks",
+        ".github/hooks/",
+    ),
+
+    # --- claude (.claude) ---
+    ("claude", "agents"): (
+        "agent_integrator", "integrate_package_agents_claude",
+        ".claude/agents/",
+    ),
+    ("claude", "commands"): (
+        "command_integrator", "integrate_package_commands",
+        ".claude/commands/",
+    ),
+    ("claude", "hooks"): (
+        "hook_integrator", "integrate_package_hooks_claude",
+        ".claude/settings.json",
+    ),
+
+    # --- cursor (.cursor) ---
+    ("cursor", "instructions"): (
+        "instruction_integrator", "integrate_package_instructions_cursor",
+        ".cursor/rules/",
+    ),
+    ("cursor", "agents"): (
+        "agent_integrator", "integrate_package_agents_cursor",
+        ".cursor/agents/",
+    ),
+    ("cursor", "hooks"): (
+        "hook_integrator", "integrate_package_hooks_cursor",
+        ".cursor/hooks.json",
+    ),
+
+    # --- opencode (.opencode) ---
+    ("opencode", "agents"): (
+        "agent_integrator", "integrate_package_agents_opencode",
+        ".opencode/agents/",
+    ),
+    ("opencode", "commands"): (
+        "command_integrator", "integrate_package_commands_opencode",
+        ".opencode/commands/",
+    ),
+}
+
+
+def integrate_package_for_targets(
+    targets,
+    package_info,
+    project_root,
+    integrators,
+    *,
+    force=False,
+    managed_files=None,
+    diagnostics=None,
+    logger=None,
+):
+    """Run the full integration pipeline for a single package.
+
+    Iterates *targets* (a list of ``TargetProfile``), and for each
+    primitive that target supports, dispatches to the correct integrator
+    method via ``INTEGRATION_DISPATCH``.
+
+    Skills are handled separately because ``SkillIntegrator`` already
+    routes to all active targets internally.
+
+    Args:
+        targets: List of ``TargetProfile`` to deploy into.
+        package_info: ``PackageInfo`` for the package being installed.
+        project_root: Workspace root ``Path``.
+        integrators: Dict mapping integrator key (e.g. ``"agent_integrator"``)
+            to integrator instance.
+        force: Overwrite user-authored files on collision.
+        managed_files: Set of workspace-relative paths from apm.lock.
+        diagnostics: ``DiagnosticCollector`` instance.
+        logger: Optional ``CommandLogger`` for tree output.
+
+    Returns:
+        Dict with integration counters and deployed file paths::
+
+            {
+                "prompts": int,
+                "agents": int,
+                "skills": int,
+                "sub_skills": int,
+                "instructions": int,
+                "commands": int,
+                "hooks": int,
+                "links_resolved": int,
+                "deployed_files": list[str],
+            }
+    """
+    result = {
+        "prompts": 0,
+        "agents": 0,
+        "skills": 0,
+        "sub_skills": 0,
+        "instructions": 0,
+        "commands": 0,
+        "hooks": 0,
+        "links_resolved": 0,
+        "deployed_files": [],
+    }
+
+    if not targets:
+        return result
+
+    deployed = result["deployed_files"]
+
+    def _log(msg):
+        if logger:
+            logger.tree_item(msg)
+
+    # Collect target names for the skill integrator check
+    target_names = {t.name for t in targets}
+
+    # --- per-target primitive dispatch ---
+    for target in targets:
+        for primitive in target.primitives:
+            # Skills are handled separately below
+            if primitive == "skills":
+                continue
+
+            key = (target.name, primitive)
+            entry = INTEGRATION_DISPATCH.get(key)
+            if entry is None:
+                continue
+
+            integrator_key, method_name, log_dir = entry
+            integrator = integrators.get(integrator_key)
+            if integrator is None:
+                continue
+
+            method = getattr(integrator, method_name, None)
+            if method is None:
+                continue
+
+            call_result = method(
+                package_info, project_root,
+                force=force, managed_files=managed_files,
+                diagnostics=diagnostics,
+            )
+
+            # Accumulate counts -- handle both IntegrationResult and
+            # HookIntegrationResult (which uses hooks_integrated).
+            files = getattr(call_result, "files_integrated", 0)
+            hooks = getattr(call_result, "hooks_integrated", 0)
+            updated = getattr(call_result, "files_updated", 0)
+            links = getattr(call_result, "links_resolved", 0)
+
+            if primitive == "hooks":
+                if hooks > 0:
+                    result["hooks"] += hooks
+                    _log(f"  |-- {hooks} hook(s) integrated -> {log_dir}")
+            elif primitive in ("prompts", "agents", "instructions", "commands"):
+                if files > 0:
+                    result[primitive] += files
+                    _log(f"  |-- {files} {primitive} integrated -> {log_dir}")
+                if updated > 0:
+                    _log(f"  |-- {updated} {primitive} updated")
+
+            result["links_resolved"] += links
+            for tp in getattr(call_result, "target_paths", []):
+                deployed.append(tp.relative_to(project_root).as_posix())
+
+    # --- skills (multi-target, handled by SkillIntegrator) ---
+    has_skill_target = any(t.supports("skills") for t in targets)
+    if has_skill_target:
+        skill_integrator = integrators.get("skill_integrator")
+        if skill_integrator is not None:
+            skill_result = skill_integrator.integrate_package_skill(
+                package_info, project_root,
+                diagnostics=diagnostics, managed_files=managed_files,
+                force=force,
+            )
+            _skill_target_dirs: set[str] = set()
+            for tp in skill_result.target_paths:
+                rel = tp.relative_to(project_root)
+                if rel.parts:
+                    _skill_target_dirs.add(rel.parts[0])
+            _skill_targets = sorted(_skill_target_dirs)
+            _skill_target_str = (
+                ", ".join(f"{d}/skills/" for d in _skill_targets) or "skills/"
+            )
+            if skill_result.skill_created:
+                result["skills"] += 1
+                _log(f"  |-- Skill integrated -> {_skill_target_str}")
+            if skill_result.sub_skills_promoted > 0:
+                result["sub_skills"] += skill_result.sub_skills_promoted
+                _log(
+                    f"  |-- {skill_result.sub_skills_promoted} skill(s) "
+                    f"integrated -> {_skill_target_str}"
+                )
+            for tp in skill_result.target_paths:
+                deployed.append(tp.relative_to(project_root).as_posix())
+
+    return result
diff --git a/tests/unit/integration/test_command_integrator.py b/tests/unit/integration/test_command_integrator.py
index 18766669e..4cfdafd9c 100644
--- a/tests/unit/integration/test_command_integrator.py
+++ b/tests/unit/integration/test_command_integrator.py
@@ -368,10 +368,10 @@ def test_sync_handles_missing_dir(self, temp_project_no_opencode):
 
 
 class TestIntegratePackagePrimitivesTargetGating:
-    """Tests that _integrate_package_primitives respects the integrate_claude flag.
+    """Tests that _integrate_package_primitives only dispatches to active targets.
 
-    Regression test for: CommandIntegrator was called unconditionally, causing
-    .claude/commands/ to be created even when target=copilot (integrate_claude=False).
+    Uses KNOWN_TARGETS profiles to verify target-driven routing:
+    each target profile only triggers integrators for its declared primitives.
     """
 
     def _make_mock_integrators(self):
@@ -418,14 +418,15 @@ def _empty_result(*args, **kwargs):
             integrators[name] = m
         return integrators
 
-    def test_integrate_claude_false_does_not_call_integrate_package_commands(self):
-        """When integrate_claude=False, integrate_package_commands must not be called.
+    def test_copilot_target_does_not_call_claude_commands(self):
+        """When targets=[copilot], integrate_package_commands must not be called.
 
-        This is the regression test for the bug where .claude/commands/ was created
-        even when target=copilot (vscode) set integrate_claude=False.
+        Regression test for the bug where .claude/commands/ was created
+        even when target=copilot.
         """
         import tempfile, shutil
         from apm_cli.commands.install import _integrate_package_primitives
+        from apm_cli.integration.targets import KNOWN_TARGETS
         from apm_cli.utils.diagnostics import DiagnosticCollector
 
         temp_dir = tempfile.mkdtemp()
@@ -440,9 +441,7 @@ def test_integrate_claude_false_does_not_call_integrate_package_commands(self):
             _integrate_package_primitives(
                 package_info,
                 project_root,
-                integrate_vscode=True,
-                integrate_claude=False,
-                integrate_opencode=False,
+                targets=[KNOWN_TARGETS["copilot"]],
                 managed_files=set(),
                 force=False,
                 diagnostics=diagnostics,
@@ -454,10 +453,11 @@ def test_integrate_claude_false_does_not_call_integrate_package_commands(self):
         finally:
             shutil.rmtree(temp_dir, ignore_errors=True)
 
-    def test_integrate_claude_true_calls_integrate_package_commands(self):
-        """When integrate_claude=True, integrate_package_commands must be called."""
+    def test_claude_target_calls_integrate_package_commands(self):
+        """When targets=[claude], integrate_package_commands must be called."""
         import tempfile, shutil
         from apm_cli.commands.install import _integrate_package_primitives
+        from apm_cli.integration.targets import KNOWN_TARGETS
         from apm_cli.utils.diagnostics import DiagnosticCollector
 
         temp_dir = tempfile.mkdtemp()
@@ -472,9 +472,7 @@ def test_integrate_claude_true_calls_integrate_package_commands(self):
             _integrate_package_primitives(
                 package_info,
                 project_root,
-                integrate_vscode=False,
-                integrate_claude=True,
-                integrate_opencode=False,
+                targets=[KNOWN_TARGETS["claude"]],
                 managed_files=set(),
                 force=False,
                 diagnostics=diagnostics,
@@ -484,3 +482,203 @@ def test_integrate_claude_true_calls_integrate_package_commands(self):
             integrators["command_integrator"].integrate_package_commands.assert_called_once()
         finally:
             shutil.rmtree(temp_dir, ignore_errors=True)
+
+    def test_opencode_only_does_not_call_github_prompts_or_agents(self):
+        """When targets=[opencode], .github/ prompts and agents must not run.
+
+        Regression test for: prompts and .github/agents were called unconditionally,
+        creating .github/ even when --target opencode was set.
+        """
+        import tempfile, shutil
+        from apm_cli.commands.install import _integrate_package_primitives
+        from apm_cli.integration.targets import KNOWN_TARGETS
+        from apm_cli.utils.diagnostics import DiagnosticCollector
+
+        temp_dir = tempfile.mkdtemp()
+        try:
+            project_root = Path(temp_dir)
+            (project_root / ".opencode").mkdir()
+
+            package_info = MagicMock()
+            integrators = self._make_mock_integrators()
+            diagnostics = DiagnosticCollector(verbose=False)
+
+            _integrate_package_primitives(
+                package_info,
+                project_root,
+                targets=[KNOWN_TARGETS["opencode"]],
+                managed_files=set(),
+                force=False,
+                diagnostics=diagnostics,
+                **integrators,
+            )
+
+            integrators["prompt_integrator"].integrate_package_prompts.assert_not_called()
+            integrators["agent_integrator"].integrate_package_agents.assert_not_called()
+            integrators["instruction_integrator"].integrate_package_instructions.assert_not_called()
+            integrators["hook_integrator"].integrate_package_hooks.assert_not_called()
+            assert not (project_root / ".github").exists()
+        finally:
+            shutil.rmtree(temp_dir, ignore_errors=True)
+
+    def test_opencode_only_calls_opencode_agents_and_commands(self):
+        """When targets=[opencode], OpenCode agents and commands must run."""
+        import tempfile, shutil
+        from apm_cli.commands.install import _integrate_package_primitives
+        from apm_cli.integration.targets import KNOWN_TARGETS
+        from apm_cli.utils.diagnostics import DiagnosticCollector
+
+        temp_dir = tempfile.mkdtemp()
+        try:
+            project_root = Path(temp_dir)
+            (project_root / ".opencode").mkdir()
+
+            package_info = MagicMock()
+            integrators = self._make_mock_integrators()
+            diagnostics = DiagnosticCollector(verbose=False)
+
+            _integrate_package_primitives(
+                package_info,
+                project_root,
+                targets=[KNOWN_TARGETS["opencode"]],
+                managed_files=set(),
+                force=False,
+                diagnostics=diagnostics,
+                **integrators,
+            )
+
+            integrators["agent_integrator"].integrate_package_agents_opencode.assert_called_once()
+            integrators["command_integrator"].integrate_package_commands_opencode.assert_called_once()
+        finally:
+            shutil.rmtree(temp_dir, ignore_errors=True)
+
+    def test_cursor_only_does_not_call_github_or_claude_or_opencode(self):
+        """When targets=[cursor], only cursor-specific integrations run."""
+        import tempfile, shutil
+        from apm_cli.commands.install import _integrate_package_primitives
+        from apm_cli.integration.targets import KNOWN_TARGETS
+        from apm_cli.utils.diagnostics import DiagnosticCollector
+
+        temp_dir = tempfile.mkdtemp()
+        try:
+            project_root = Path(temp_dir)
+            (project_root / ".cursor").mkdir()
+
+            package_info = MagicMock()
+            integrators = self._make_mock_integrators()
+            diagnostics = DiagnosticCollector(verbose=False)
+
+            _integrate_package_primitives(
+                package_info,
+                project_root,
+                targets=[KNOWN_TARGETS["cursor"]],
+                managed_files=set(),
+                force=False,
+                diagnostics=diagnostics,
+                **integrators,
+            )
+
+            # .github/ integrations must NOT run
+            integrators["prompt_integrator"].integrate_package_prompts.assert_not_called()
+            integrators["agent_integrator"].integrate_package_agents.assert_not_called()
+            integrators["instruction_integrator"].integrate_package_instructions.assert_not_called()
+            integrators["hook_integrator"].integrate_package_hooks.assert_not_called()
+            # Claude must NOT run
+            integrators["command_integrator"].integrate_package_commands.assert_not_called()
+            integrators["agent_integrator"].integrate_package_agents_claude.assert_not_called()
+            integrators["hook_integrator"].integrate_package_hooks_claude.assert_not_called()
+            # OpenCode must NOT run
+            integrators["agent_integrator"].integrate_package_agents_opencode.assert_not_called()
+            integrators["command_integrator"].integrate_package_commands_opencode.assert_not_called()
+            # Cursor MUST run
+            integrators["instruction_integrator"].integrate_package_instructions_cursor.assert_called_once()
+            integrators["agent_integrator"].integrate_package_agents_cursor.assert_called_once()
+            integrators["hook_integrator"].integrate_package_hooks_cursor.assert_called_once()
+            assert not (project_root / ".github").exists()
+        finally:
+            shutil.rmtree(temp_dir, ignore_errors=True)
+
+    def test_copilot_only_does_not_call_opencode_or_cursor(self):
+        """When targets=[copilot], OpenCode and Cursor integrations must not run."""
+        import tempfile, shutil
+        from apm_cli.commands.install import _integrate_package_primitives
+        from apm_cli.integration.targets import KNOWN_TARGETS
+        from apm_cli.utils.diagnostics import DiagnosticCollector
+
+        temp_dir = tempfile.mkdtemp()
+        try:
+            project_root = Path(temp_dir)
+            (project_root / ".github").mkdir()
+
+            package_info = MagicMock()
+            integrators = self._make_mock_integrators()
+            diagnostics = DiagnosticCollector(verbose=False)
+
+            _integrate_package_primitives(
+                package_info,
+                project_root,
+                targets=[KNOWN_TARGETS["copilot"]],
+                managed_files=set(),
+                force=False,
+                diagnostics=diagnostics,
+                **integrators,
+            )
+
+            # .github/ integrations MUST run
+            integrators["prompt_integrator"].integrate_package_prompts.assert_called_once()
+            integrators["agent_integrator"].integrate_package_agents.assert_called_once()
+            integrators["instruction_integrator"].integrate_package_instructions.assert_called_once()
+            integrators["hook_integrator"].integrate_package_hooks.assert_called_once()
+            # OpenCode must NOT run
+            integrators["agent_integrator"].integrate_package_agents_opencode.assert_not_called()
+            integrators["command_integrator"].integrate_package_commands_opencode.assert_not_called()
+            # Cursor must NOT run
+            integrators["instruction_integrator"].integrate_package_instructions_cursor.assert_not_called()
+            integrators["agent_integrator"].integrate_package_agents_cursor.assert_not_called()
+            integrators["hook_integrator"].integrate_package_hooks_cursor.assert_not_called()
+        finally:
+            shutil.rmtree(temp_dir, ignore_errors=True)
+
+    def test_empty_targets_returns_zero_counters(self):
+        """When targets=[], no integrators should be called."""
+        import tempfile, shutil
+        from apm_cli.commands.install import _integrate_package_primitives
+        from apm_cli.utils.diagnostics import DiagnosticCollector
+
+        temp_dir = tempfile.mkdtemp()
+        try:
+            project_root = Path(temp_dir)
+
+            package_info = MagicMock()
+            integrators = self._make_mock_integrators()
+            diagnostics = DiagnosticCollector(verbose=False)
+
+            result = _integrate_package_primitives(
+                package_info,
+                project_root,
+                targets=[],
+                managed_files=set(),
+                force=False,
+                diagnostics=diagnostics,
+                **integrators,
+            )
+
+            assert result["prompts"] == 0
+            assert result["agents"] == 0
+            assert result["deployed_files"] == []
+            # No integrator should have been called
+            integrators["prompt_integrator"].integrate_package_prompts.assert_not_called()
+            integrators["agent_integrator"].integrate_package_agents.assert_not_called()
+            integrators["agent_integrator"].integrate_package_agents_claude.assert_not_called()
+            integrators["agent_integrator"].integrate_package_agents_cursor.assert_not_called()
+            integrators["agent_integrator"].integrate_package_agents_opencode.assert_not_called()
+            integrators["skill_integrator"].integrate_package_skill.assert_not_called()
+            integrators["instruction_integrator"].integrate_package_instructions.assert_not_called()
+            integrators["instruction_integrator"].integrate_package_instructions_cursor.assert_not_called()
+            integrators["command_integrator"].integrate_package_commands.assert_not_called()
+            integrators["command_integrator"].integrate_package_commands_opencode.assert_not_called()
+            integrators["hook_integrator"].integrate_package_hooks.assert_not_called()
+            integrators["hook_integrator"].integrate_package_hooks_claude.assert_not_called()
+            integrators["hook_integrator"].integrate_package_hooks_cursor.assert_not_called()
+        finally:
+            shutil.rmtree(temp_dir, ignore_errors=True)
diff --git a/tests/unit/integration/test_targets.py b/tests/unit/integration/test_targets.py
index 0d2845a41..d775bee0a 100644
--- a/tests/unit/integration/test_targets.py
+++ b/tests/unit/integration/test_targets.py
@@ -97,3 +97,148 @@ def test_explicit_overrides_detection(self):
     def test_explicit_unknown_returns_empty(self):
         targets = active_targets(self.root, explicit_target="nonexistent")
         assert targets == []
+
+
+class TestIntegrationDispatchRegistry:
+    """Verify INTEGRATION_DISPATCH covers every non-skill primitive."""
+
+    def test_every_target_primitive_has_dispatch_entry(self):
+        """Each (target, primitive) pair in KNOWN_TARGETS that is not 'skills'
+        must have a matching entry in INTEGRATION_DISPATCH."""
+        from apm_cli.integration.targets import INTEGRATION_DISPATCH
+
+        missing = []
+        for name, profile in KNOWN_TARGETS.items():
+            for primitive in profile.primitives:
+                if primitive == "skills":
+                    continue
+                key = (name, primitive)
+                if key not in INTEGRATION_DISPATCH:
+                    missing.append(key)
+
+        assert missing == [], f"Missing dispatch entries: {missing}"
+
+    def test_dispatch_entries_reference_valid_methods(self):
+        """All integrator_key/method_name pairs in INTEGRATION_DISPATCH
+        must exist on the actual integrator classes."""
+        from apm_cli.integration.targets import INTEGRATION_DISPATCH
+        from apm_cli.integration.prompt_integrator import PromptIntegrator
+        from apm_cli.integration.agent_integrator import AgentIntegrator
+        from apm_cli.integration.instruction_integrator import InstructionIntegrator
+        from apm_cli.integration.command_integrator import CommandIntegrator
+        from apm_cli.integration.hook_integrator import HookIntegrator
+
+        class_map = {
+            "prompt_integrator": PromptIntegrator,
+            "agent_integrator": AgentIntegrator,
+            "instruction_integrator": InstructionIntegrator,
+            "command_integrator": CommandIntegrator,
+            "hook_integrator": HookIntegrator,
+        }
+
+        for (target, primitive), (ikey, method, _) in INTEGRATION_DISPATCH.items():
+            cls = class_map.get(ikey)
+            assert cls is not None, f"Unknown integrator key: {ikey}"
+            assert hasattr(cls, method), (
+                f"({target}, {primitive}): {cls.__name__} has no method {method}"
+            )
+
+
+class TestIntegratePackageForTargets:
+    """Tests for integrate_package_for_targets() dispatcher."""
+
+    def setup_method(self):
+        self.temp_dir = tempfile.mkdtemp()
+        self.root = Path(self.temp_dir)
+
+    def teardown_method(self):
+        shutil.rmtree(self.temp_dir, ignore_errors=True)
+
+    def _make_mock_integrators(self):
+        from unittest.mock import MagicMock
+
+        def _empty_result(*args, **kwargs):
+            r = MagicMock()
+            r.files_integrated = 0
+            r.files_updated = 0
+            r.links_resolved = 0
+            r.target_paths = []
+            r.skill_created = False
+            r.sub_skills_promoted = 0
+            r.hooks_integrated = 0
+            return r
+
+        integrators = {}
+        for name in (
+            "prompt_integrator",
+            "agent_integrator",
+            "skill_integrator",
+            "instruction_integrator",
+            "command_integrator",
+            "hook_integrator",
+        ):
+            m = MagicMock()
+            for method in (
+                "integrate_package_prompts",
+                "integrate_package_agents",
+                "integrate_package_agents_claude",
+                "integrate_package_agents_cursor",
+                "integrate_package_agents_opencode",
+                "integrate_package_skill",
+                "integrate_package_instructions",
+                "integrate_package_instructions_cursor",
+                "integrate_package_commands",
+                "integrate_package_commands_opencode",
+                "integrate_package_hooks",
+                "integrate_package_hooks_claude",
+                "integrate_package_hooks_cursor",
+            ):
+                getattr(m, method).side_effect = _empty_result
+            integrators[name] = m
+        return integrators
+
+    def test_opencode_target_skips_github(self):
+        """Only opencode primitives should fire for opencode target."""
+        from unittest.mock import MagicMock
+        from apm_cli.integration.targets import integrate_package_for_targets
+
+        pkg = MagicMock()
+        integrators = self._make_mock_integrators()
+
+        result = integrate_package_for_targets(
+            [KNOWN_TARGETS["opencode"]],
+            pkg, self.root, integrators,
+        )
+
+        integrators["prompt_integrator"].integrate_package_prompts.assert_not_called()
+        integrators["agent_integrator"].integrate_package_agents.assert_not_called()
+        integrators["agent_integrator"].integrate_package_agents_opencode.assert_called_once()
+        integrators["command_integrator"].integrate_package_commands_opencode.assert_called_once()
+        assert result["deployed_files"] == []
+
+    def test_all_targets_calls_every_dispatch_entry(self):
+        """With all 4 targets, every dispatch entry should fire once."""
+        from unittest.mock import MagicMock
+        from apm_cli.integration.targets import (
+            integrate_package_for_targets,
+            INTEGRATION_DISPATCH,
+        )
+
+        for d in (".github", ".claude", ".cursor", ".opencode"):
+            (self.root / d).mkdir()
+
+        pkg = MagicMock()
+        integrators = self._make_mock_integrators()
+
+        integrate_package_for_targets(
+            list(KNOWN_TARGETS.values()),
+            pkg, self.root, integrators,
+        )
+
+        for (target, primitive), (ikey, method, _) in INTEGRATION_DISPATCH.items():
+            m = integrators[ikey]
+            fn = getattr(m, method)
+            assert fn.call_count == 1, (
+                f"({target}, {primitive}) -> {ikey}.{method} "
+                f"expected 1 call, got {fn.call_count}"
+            )
diff --git a/uv.lock b/uv.lock
index 83746999a..16a1330ac 100644
--- a/uv.lock
+++ b/uv.lock
@@ -179,7 +179,7 @@ wheels = [
 
 [[package]]
 name = "apm-cli"
-version = "0.8.5"
+version = "0.8.6"
 source = { editable = "." }
 dependencies = [
     { name = "click" },
