Skip to content

Commit 75f64d9

Browse files
committed
fix: Optional type, rollback safety, and override skill restoration
- Fix context_note type to Optional[str] - Wrap shutil.rmtree in try/except during install rollback - Separate override-backed skills from core/extension in _reconcile_skills
1 parent a1a0094 commit 75f64d9

2 files changed

Lines changed: 60 additions & 48 deletions

File tree

src/specify_cli/agents.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import os
1010
from pathlib import Path
11-
from typing import Dict, List, Any
11+
from typing import Dict, List, Any, Optional
1212

1313
import platform
1414
import re
@@ -657,7 +657,7 @@ def register_commands_for_non_skill_agents(
657657
source_id: str,
658658
source_dir: Path,
659659
project_root: Path,
660-
context_note: str = None,
660+
context_note: Optional[str] = None,
661661
) -> Dict[str, List[str]]:
662662
"""Register commands for all non-skill agents in the project.
663663

src/specify_cli/presets.py

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)