@@ -910,8 +910,15 @@ def _ignore(directory: str, entries: List[str]) -> Set[str]:
910910
911911 return _ignore
912912
913- def _get_skills_dir (self ) -> Optional [Path ]:
914- """Return the active skills directory for extension skill registration.
913+ def _get_skills_dir (self , agent_name : Optional [str ] = None ) -> Optional [Path ]:
914+ """Return the skills directory for extension skill registration.
915+
916+ When *agent_name* is given, resolves the skills directory and
917+ skills-enabled state for that specific agent (using per-agent
918+ settings in ``.specify/integration.json``), so skill rendering
919+ works correctly for non-active agents (#2948). When omitted,
920+ falls back to the previous behaviour of using the active agent
921+ from init-options.
915922
916923 Delegates to :func:`resolve_active_skills_dir` which reads
917924 init-options, applies the Kimi native-skills fallback, and
@@ -926,6 +933,8 @@ def _get_skills_dir(self) -> Optional[Path]:
926933 load_init_options ,
927934 resolve_active_skills_dir ,
928935 )
936+ from .._init_options import is_agent_skills_enabled
937+ from .. import _get_skills_dir as resolve_agent_skills_dir
929938
930939 def _ensure_usable (skills_dir : Path ) -> Optional [Path ]:
931940 try :
@@ -943,26 +952,42 @@ def _ensure_usable(skills_dir: Path) -> Optional[Path]:
943952 return None
944953 return skills_dir
945954
946- try :
947- skills_dir = resolve_active_skills_dir (self .project_root )
948- except (ValueError , OSError ) as exc :
949- _print_cli_warning (
950- "resolve" ,
951- "skills directory" ,
952- None ,
953- exc ,
954- continuing = "Continuing without skill registration." ,
955- )
956- return None
957- if skills_dir is None :
958- return None
959-
960955 opts = load_init_options (self .project_root )
961956 if not isinstance (opts , dict ):
962- return _ensure_usable (skills_dir )
963- selected_ai = opts .get ("ai" )
964- if not isinstance (selected_ai , str ) or not selected_ai :
965- return _ensure_usable (skills_dir )
957+ opts = {}
958+
959+ if agent_name is None :
960+ try :
961+ skills_dir = resolve_active_skills_dir (self .project_root )
962+ except (ValueError , OSError ) as exc :
963+ _print_cli_warning (
964+ "resolve" ,
965+ "skills directory" ,
966+ None ,
967+ exc ,
968+ continuing = "Continuing without skill registration." ,
969+ )
970+ return None
971+ if skills_dir is None :
972+ return None
973+ selected_ai = opts .get ("ai" )
974+ if not isinstance (selected_ai , str ) or not selected_ai :
975+ return _ensure_usable (skills_dir )
976+ else :
977+ if not is_agent_skills_enabled (self .project_root , agent_name , opts ):
978+ return None
979+ try :
980+ skills_dir = resolve_agent_skills_dir (self .project_root , agent_name )
981+ except (ValueError , OSError ) as exc :
982+ _print_cli_warning (
983+ "resolve" ,
984+ "skills directory" ,
985+ None ,
986+ exc ,
987+ continuing = "Continuing without skill registration." ,
988+ )
989+ return None
990+ selected_ai = agent_name
966991
967992 from ..agents import CommandRegistrar
968993
@@ -980,6 +1005,7 @@ def _register_extension_skills(
9801005 manifest : ExtensionManifest ,
9811006 extension_dir : Path ,
9821007 link_outputs : bool = False ,
1008+ agent_name : Optional [str ] = None ,
9831009 ) -> List [str ]:
9841010 """Generate SKILL.md files for extension commands as agent skills.
9851011
@@ -997,11 +1023,12 @@ def _register_extension_skills(
9971023 Returns:
9981024 List of skill names that were created (for registry storage).
9991025 """
1000- skills_dir = self ._get_skills_dir ()
1026+ skills_dir = self ._get_skills_dir (agent_name )
10011027 if not skills_dir :
10021028 return []
10031029
10041030 from .. import load_init_options
1031+ from .._init_options import is_agent_skills_enabled
10051032 from ..agents import CommandRegistrar
10061033 from ..integrations import get_integration
10071034 from ..integrations .base import IntegrationBase
@@ -1010,13 +1037,13 @@ def _register_extension_skills(
10101037 opts = load_init_options (self .project_root )
10111038 if not isinstance (opts , dict ):
10121039 opts = {}
1013- selected_ai = opts .get ("ai" )
1040+ selected_ai = agent_name if agent_name else opts .get ("ai" )
10141041 if not isinstance (selected_ai , str ) or not selected_ai :
10151042 return []
10161043 registrar = CommandRegistrar ()
10171044 agent_config = registrar .AGENT_CONFIGS .get (selected_ai , {})
10181045 integration = get_integration (selected_ai )
1019- ai_skills_enabled = is_ai_skills_enabled ( opts )
1046+ ai_skills_enabled = is_agent_skills_enabled ( self . project_root , selected_ai , opts )
10201047
10211048 def _resolve_command_ref_tokens (body : str ) -> str :
10221049 """Resolve explicit command-ref tokens with the active skill style."""
@@ -1731,17 +1758,16 @@ def unregister_agent_artifacts(self, agent_name: str) -> None:
17311758 def register_enabled_extensions_for_agent (self , agent_name : str ) -> None :
17321759 """Register installed, enabled extensions for ``agent_name``.
17331760
1734- Command-file registration is scoped to the explicit ``agent_name``
1735- argument, so this method can be used after install, upgrade, or switch.
1736- Extension skill rendering is still scoped to the active ``ai`` /
1737- ``ai_skills`` settings in init-options, so non-active skills-mode
1738- targets receive command files here. Per-agent skills parity is tracked
1739- separately in #2948.
1761+ Both command-file and skill registration are scoped to the explicit
1762+ ``agent_name`` argument, so this method can be used after install,
1763+ upgrade, or switch and renders skills correctly for any enabled
1764+ skills-mode agent, not just the active one (#2948).
17401765 """
17411766 if not agent_name :
17421767 return
17431768
17441769 from .. import load_init_options
1770+ from .._init_options import is_agent_skills_enabled
17451771
17461772 registrar = CommandRegistrar ()
17471773 agent_config = registrar .AGENT_CONFIGS .get (agent_name )
@@ -1750,10 +1776,11 @@ def register_enabled_extensions_for_agent(self, agent_name: str) -> None:
17501776 init_options = {}
17511777
17521778 active_agent = init_options .get ("ai" )
1753- ai_skills_enabled = is_ai_skills_enabled (init_options )
1779+ ai_skills_enabled = is_agent_skills_enabled (
1780+ self .project_root , agent_name , init_options
1781+ )
17541782 skills_mode_active = (
1755- active_agent == agent_name
1756- and ai_skills_enabled
1783+ ai_skills_enabled
17571784 and bool (agent_config )
17581785 and agent_config .get ("extension" ) != "/SKILL.md"
17591786 )
@@ -1793,46 +1820,42 @@ def register_enabled_extensions_for_agent(self, agent_name: str) -> None:
17931820 if new_registered != registered_commands :
17941821 updates ["registered_commands" ] = new_registered
17951822
1796- # Extension *skills* are only ever rendered for the active agent:
1797- # `_register_extension_skills` resolves the skills dir and
1798- # frontmatter from init-options["ai"], ignoring ``agent_name``.
1799- # When this method runs for a non-active agent — as install/upgrade
1800- # now do for a secondary integration (#2886) — the skills pass would
1801- # re-render the *active* agent's extension skills as a side effect,
1802- # resurrecting skill files the user deliberately deleted. Skip it
1803- # unless the target is the active agent; `switch` is unaffected
1804- # because it activates the target before registering. (Rendering
1805- # skills for a non-active target is tracked separately in #2948.)
1806- if agent_name == active_agent :
1807- try :
1808- registered_skills = self ._register_extension_skills (
1809- manifest , ext_dir
1823+ # Extension skills are rendered whenever *agent_name* itself
1824+ # has skills mode enabled (checked via per-agent settings in
1825+ # `.specify/integration.json`, falling back to the legacy
1826+ # global init-options for the active agent). This makes
1827+ # skill rendering agent-aware instead of only ever
1828+ # targeting the active agent (#2948), while still avoiding
1829+ # unrelated side effects on other installed agents.
1830+ try :
1831+ registered_skills = self ._register_extension_skills (
1832+ manifest , ext_dir , agent_name = agent_name
1833+ )
1834+ except Exception as skills_err :
1835+ # Skills are a companion artifact. If command registration
1836+ # already succeeded, still persist it so later cleanup can
1837+ # find those command files.
1838+ from .. import _print_cli_warning
1839+
1840+ _print_cli_warning (
1841+ "register extension skills for" ,
1842+ "extension" ,
1843+ ext_id ,
1844+ skills_err ,
1845+ continuing = (
1846+ "Continuing with available registration results for this "
1847+ "extension and the remaining extensions."
1848+ ),
1849+ )
1850+ else :
1851+ if registered_skills :
1852+ existing_skills = self ._valid_name_list (
1853+ metadata .get ("registered_skills" , [])
18101854 )
1811- except Exception as skills_err :
1812- # Skills are a companion artifact. If command registration
1813- # already succeeded, still persist it so later cleanup can
1814- # find those command files.
1815- from .. import _print_cli_warning
1816-
1817- _print_cli_warning (
1818- "register extension skills for" ,
1819- "extension" ,
1820- ext_id ,
1821- skills_err ,
1822- continuing = (
1823- "Continuing with available registration results for this "
1824- "extension and the remaining extensions."
1825- ),
1855+ merged_skills = list (
1856+ dict .fromkeys (existing_skills + registered_skills )
18261857 )
1827- else :
1828- if registered_skills :
1829- existing_skills = self ._valid_name_list (
1830- metadata .get ("registered_skills" , [])
1831- )
1832- merged_skills = list (
1833- dict .fromkeys (existing_skills + registered_skills )
1834- )
1835- updates ["registered_skills" ] = merged_skills
1858+ updates ["registered_skills" ] = merged_skills
18361859
18371860 if updates :
18381861 self .registry .update (ext_id , updates )
0 commit comments