@@ -983,51 +983,60 @@ def _reconcile_skills(self, command_names: List[str]) -> None:
983983 non_preset_skills .append ((skill_name , cmd_name , layers [0 ]))
984984
985985 # Restore skills for commands whose winner is non-preset.
986- # Use _unregister_skills which restores from core/extension, but
987- # also handles project overrides by reading the winning layer directly.
988986 if non_preset_skills and skills_dir :
989- skill_names_only = [s [0 ] for s in non_preset_skills ]
990- self ._unregister_skills (skill_names_only , self .presets_dir )
991- # For project overrides, _unregister_skills restores from core.
992- # Re-write from the actual winning layer if it's an override.
993- for skill_name , cmd_name , top_layer in non_preset_skills :
994- if top_layer ["source" ] == "project override" :
995- skill_subdir = skills_dir / skill_name
996- if skill_subdir .is_dir ():
997- skill_file = skill_subdir / "SKILL.md"
998- try :
999- from .agents import CommandRegistrar
1000- from . import SKILL_DESCRIPTIONS , load_init_options
1001- registrar = CommandRegistrar ()
1002- content = top_layer ["path" ].read_text (encoding = "utf-8" )
1003- fm , body = registrar .parse_frontmatter (content )
1004- short_name = cmd_name
1005- if short_name .startswith ("speckit." ):
1006- short_name = short_name [len ("speckit." ):]
1007- desc = SKILL_DESCRIPTIONS .get (
1008- short_name .replace ("." , "-" ),
1009- fm .get ("description" , f"Command: { short_name } " ),
1010- )
1011- init_opts = load_init_options (self .project_root )
1012- selected_ai = init_opts .get ("ai" ) if isinstance (init_opts , dict ) else ""
1013- if isinstance (selected_ai , str ):
1014- body = registrar .resolve_skill_placeholders (
1015- selected_ai , fm , body , self .project_root
1016- )
1017- fm_data = registrar .build_skill_frontmatter (
1018- selected_ai if isinstance (selected_ai , str ) else "" ,
1019- skill_name , desc ,
1020- f"override:{ cmd_name } " ,
1021- )
1022- fm_text = yaml .safe_dump (fm_data , sort_keys = False ).strip ()
1023- skill_title = self ._skill_title_from_command (cmd_name )
1024- skill_content = (
1025- f"---\n { fm_text } \n ---\n \n "
1026- f"# Speckit { skill_title } Skill\n \n { body } \n "
1027- )
1028- skill_file .write_text (skill_content , encoding = "utf-8" )
1029- except Exception :
1030- pass # best-effort override skill restoration
987+ # Separate override-backed skills from core/extension-backed ones.
988+ # _unregister_skills can rmtree the skill dir, so overrides must
989+ # be handled directly (create dir + write) without that call.
990+ core_ext_skills = []
991+ override_skills = []
992+ for item in non_preset_skills :
993+ if item [2 ]["source" ] == "project override" :
994+ override_skills .append (item )
995+ else :
996+ core_ext_skills .append (item )
997+
998+ if core_ext_skills :
999+ self ._unregister_skills (
1000+ [s [0 ] for s in core_ext_skills ], self .presets_dir
1001+ )
1002+
1003+ for skill_name , cmd_name , top_layer in override_skills :
1004+ skill_subdir = skills_dir / skill_name
1005+ skill_subdir .mkdir (parents = True , exist_ok = True )
1006+ skill_file = skill_subdir / "SKILL.md"
1007+ try :
1008+ from .agents import CommandRegistrar
1009+ from . import SKILL_DESCRIPTIONS , load_init_options
1010+ registrar = CommandRegistrar ()
1011+ content = top_layer ["path" ].read_text (encoding = "utf-8" )
1012+ fm , body = registrar .parse_frontmatter (content )
1013+ short_name = cmd_name
1014+ if short_name .startswith ("speckit." ):
1015+ short_name = short_name [len ("speckit." ):]
1016+ desc = SKILL_DESCRIPTIONS .get (
1017+ short_name .replace ("." , "-" ),
1018+ fm .get ("description" , f"Command: { short_name } " ),
1019+ )
1020+ init_opts = load_init_options (self .project_root )
1021+ selected_ai = init_opts .get ("ai" ) if isinstance (init_opts , dict ) else ""
1022+ if isinstance (selected_ai , str ):
1023+ body = registrar .resolve_skill_placeholders (
1024+ selected_ai , fm , body , self .project_root
1025+ )
1026+ fm_data = registrar .build_skill_frontmatter (
1027+ selected_ai if isinstance (selected_ai , str ) else "" ,
1028+ skill_name , desc ,
1029+ f"override:{ cmd_name } " ,
1030+ )
1031+ fm_text = yaml .safe_dump (fm_data , sort_keys = False ).strip ()
1032+ skill_title = self ._skill_title_from_command (cmd_name )
1033+ skill_content = (
1034+ f"---\n { fm_text } \n ---\n \n "
1035+ f"# Speckit { skill_title } Skill\n \n { body } \n "
1036+ )
1037+ skill_file .write_text (skill_content , encoding = "utf-8" )
1038+ except Exception :
1039+ pass # best-effort override skill restoration
10311040
10321041 # Register skills only for the specific commands being reconciled,
10331042 # not all commands in each winning preset's manifest.
@@ -1511,8 +1520,11 @@ def install_from_directory(
15111520 self ._unregister_commands (registered_commands )
15121521 if registered_skills :
15131522 self ._unregister_skills (registered_skills , dest_dir )
1514- if dest_dir .exists ():
1515- shutil .rmtree (dest_dir )
1523+ try :
1524+ if dest_dir .exists ():
1525+ shutil .rmtree (dest_dir )
1526+ except OSError :
1527+ pass # best-effort cleanup; don't mask the original error
15161528 self .registry .remove (manifest .id )
15171529 raise
15181530
0 commit comments