@@ -731,17 +731,29 @@ def _reconcile_composed_commands(self, command_names: List[str]) -> None:
731731 if not registered :
732732 # Top layer is a non-preset source (extension, core, or
733733 # project override). Register directly from the layer path.
734- # Extract stable source_id from display label
735734 source = layers [0 ]["source" ]
736735 if source .startswith ("extension:" ):
737- # "extension:foo v1.0" → "foo"
738- source_id = source .split (":" , 1 )[1 ].split (" " , 1 )[0 ]
739- else :
740- source_id = source
741- self ._register_command_from_path (
742- registrar , cmd_name , top_path ,
743- source_id = source_id ,
744- )
736+ # Use extension's own registration to preserve context formatting
737+ ext_id = source .split (":" , 1 )[1 ].split (" " , 1 )[0 ]
738+ ext_dir = self .project_root / ".specify" / "extensions" / ext_id
739+ ext_manifest_path = ext_dir / "extension.yml"
740+ if ext_manifest_path .exists ():
741+ try :
742+ from .extensions import ExtensionManifest
743+ ext_manifest = ExtensionManifest (ext_manifest_path )
744+ registrar .register_commands_for_non_skill_agents (
745+ ext_manifest .commands , ext_id , ext_dir ,
746+ self .project_root ,
747+ )
748+ registered = True
749+ except Exception :
750+ pass
751+ if not registered :
752+ source_id = source .split (":" , 1 )[1 ].split (" " , 1 )[0 ] if source .startswith ("extension:" ) else source
753+ self ._register_command_from_path (
754+ registrar , cmd_name , top_path ,
755+ source_id = source_id ,
756+ )
745757 else :
746758 # Composed command — resolve from full stack
747759 composed = resolver .resolve_content (cmd_name , "command" )
@@ -831,7 +843,7 @@ def _register_command_from_path(
831843 if source_id and not source_id .startswith ("preset:" ):
832844 try :
833845 from .extensions import ExtensionManifest
834- for ext_dir in self .extensions_dir .iterdir ():
846+ for ext_dir in ( self .project_root / ".specify" / "extensions" ) .iterdir ():
835847 if not ext_dir .is_dir ():
836848 continue
837849 if cmd_path .is_relative_to (ext_dir ):
@@ -956,14 +968,56 @@ def _reconcile_skills(self, command_names: List[str]) -> None:
956968 break
957969 if not found_preset :
958970 # Winner is a non-preset source (core/extension/override).
959- # Restore skill from that source using _unregister_skills logic .
971+ # Track the winning layer path for skill restoration .
960972 skill_name , _ = self ._skill_names_for_command (cmd_name )
961- non_preset_skills .append (skill_name )
973+ non_preset_skills .append (( skill_name , cmd_name , layers [ 0 ]) )
962974
963- # Restore skills for commands whose winner is non-preset
975+ # Restore skills for commands whose winner is non-preset.
976+ # Use _unregister_skills which restores from core/extension, but
977+ # also handles project overrides by reading the winning layer directly.
964978 if non_preset_skills and skills_dir :
965- # Use a dummy preset_dir (won't be read for core/extension restore)
966- self ._unregister_skills (non_preset_skills , self .presets_dir )
979+ skill_names_only = [s [0 ] for s in non_preset_skills ]
980+ self ._unregister_skills (skill_names_only , self .presets_dir )
981+ # For project overrides, _unregister_skills restores from core.
982+ # Re-write from the actual winning layer if it's an override.
983+ for skill_name , cmd_name , top_layer in non_preset_skills :
984+ if top_layer ["source" ] == "project override" :
985+ skill_subdir = skills_dir / skill_name
986+ if skill_subdir .is_dir ():
987+ skill_file = skill_subdir / "SKILL.md"
988+ try :
989+ from .agents import CommandRegistrar
990+ from . import SKILL_DESCRIPTIONS , load_init_options
991+ registrar = CommandRegistrar ()
992+ content = top_layer ["path" ].read_text (encoding = "utf-8" )
993+ fm , body = registrar .parse_frontmatter (content )
994+ short_name = cmd_name
995+ if short_name .startswith ("speckit." ):
996+ short_name = short_name [len ("speckit." ):]
997+ desc = SKILL_DESCRIPTIONS .get (
998+ short_name .replace ("." , "-" ),
999+ fm .get ("description" , f"Command: { short_name } " ),
1000+ )
1001+ init_opts = load_init_options (self .project_root )
1002+ selected_ai = init_opts .get ("ai" ) if isinstance (init_opts , dict ) else ""
1003+ if isinstance (selected_ai , str ):
1004+ body = registrar .resolve_skill_placeholders (
1005+ selected_ai , fm , body , self .project_root
1006+ )
1007+ fm_data = registrar .build_skill_frontmatter (
1008+ selected_ai if isinstance (selected_ai , str ) else "" ,
1009+ skill_name , desc ,
1010+ f"override:{ cmd_name } " ,
1011+ )
1012+ fm_text = yaml .safe_dump (fm_data , sort_keys = False ).strip ()
1013+ skill_title = self ._skill_title_from_command (cmd_name )
1014+ skill_content = (
1015+ f"---\n { fm_text } \n ---\n \n "
1016+ f"# Speckit { skill_title } Skill\n \n { body } \n "
1017+ )
1018+ skill_file .write_text (skill_content , encoding = "utf-8" )
1019+ except Exception :
1020+ pass # best-effort override skill restoration
9671021
9681022 # Register skills only for the specific commands being reconciled,
9691023 # not all commands in each winning preset's manifest.
0 commit comments