Skip to content

Commit d21c355

Browse files
echarrodclaude
andcommitted
fix(presets): create skill overrides when skills_source is marketplace
When a project had `"skills_source": "marketplace"` in integration.json alongside `"ai_skills": false` in init-options.json, `_register_skills()` silently skipped writing the local SKILL.md override. Two guards both needed relaxing: 1. `_get_skills_dir()` returned None early for non-kimi agents when ai_skills was false, so _register_skills never reached the create_missing_skills check at all. 2. `create_missing_skills` was gated on `ai_skills_enabled`, so even if the skills dir existed the new skill file would not be created. For marketplace-sourced projects a local SKILL.md is required to shadow the marketplace plugin version. Both checks now treat ai_skills as effectively enabled when skills_source == "marketplace". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c275919 commit d21c355

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

src/specify_cli/presets.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,7 @@ def _get_skills_dir(self) -> Optional[Path]:
11091109
The skills directory ``Path``, or ``None`` if skills were not
11101110
enabled and no native-skills fallback applies.
11111111
"""
1112-
from . import load_init_options, _get_skills_dir
1112+
from . import load_init_options, _get_skills_dir, _read_integration_json
11131113

11141114
opts = load_init_options(self.project_root)
11151115
if not isinstance(opts, dict):
@@ -1119,7 +1119,9 @@ def _get_skills_dir(self) -> Optional[Path]:
11191119
return None
11201120

11211121
ai_skills_enabled = bool(opts.get("ai_skills"))
1122-
if not ai_skills_enabled and agent != "kimi":
1122+
integration_data = _read_integration_json(self.project_root)
1123+
skills_source = integration_data.get("skills_source")
1124+
if not ai_skills_enabled and agent != "kimi" and skills_source != "marketplace":
11231125
return None
11241126

11251127
skills_dir = _get_skills_dir(self.project_root, agent)
@@ -1244,7 +1246,7 @@ def _register_skills(
12441246
if not skills_dir:
12451247
return []
12461248

1247-
from . import SKILL_DESCRIPTIONS, load_init_options
1249+
from . import SKILL_DESCRIPTIONS, load_init_options, _read_integration_json
12481250
from .agents import CommandRegistrar
12491251
from .integrations import get_integration
12501252

@@ -1255,14 +1257,20 @@ def _register_skills(
12551257
if not isinstance(selected_ai, str):
12561258
return []
12571259
ai_skills_enabled = bool(init_opts.get("ai_skills"))
1260+
integration_data = _read_integration_json(self.project_root)
1261+
skills_source = integration_data.get("skills_source")
1262+
# When skills_source is "marketplace" the user needs a local SKILL.md to
1263+
# shadow the marketplace plugin version, so treat ai_skills as effectively
1264+
# enabled regardless of the init-options flag.
1265+
ai_skills_effective = ai_skills_enabled or skills_source == "marketplace"
12581266
registrar = CommandRegistrar()
12591267
integration = get_integration(selected_ai)
12601268
agent_config = registrar.AGENT_CONFIGS.get(selected_ai, {})
12611269
# Native skill agents (e.g. codex/kimi/agy/trae) materialize brand-new
12621270
# preset skills in _register_commands() because their detected agent
12631271
# directory is already the skills directory. This flag is only for
12641272
# command-backed agents that also mirror commands into skills.
1265-
create_missing_skills = ai_skills_enabled and agent_config.get("extension") != "/SKILL.md"
1273+
create_missing_skills = ai_skills_effective and agent_config.get("extension") != "/SKILL.md"
12661274

12671275
written: List[str] = []
12681276

tests/test_presets.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2289,6 +2289,39 @@ def test_skill_not_updated_when_ai_skills_disabled(self, project_dir, temp_dir):
22892289
content = skill_file.read_text()
22902290
assert "untouched" in content, "Skill should not be modified when ai_skills=False"
22912291

2292+
def test_skill_created_when_ai_skills_false_but_marketplace_source(self, project_dir, temp_dir):
2293+
"""When ai_skills is False but skills_source is 'marketplace', skill override IS created.
2294+
2295+
A marketplace-integrated project sets skills_source=marketplace in integration.json
2296+
and ai_skills=false in init-options.json. The preset install must still write a
2297+
local SKILL.md so the local file can shadow the marketplace plugin skill.
2298+
"""
2299+
import json as _json
2300+
2301+
# ai_skills disabled, but integration.json declares marketplace as the source
2302+
self._write_init_options(project_dir, ai="claude", ai_skills=False)
2303+
specify_dir = project_dir / ".specify"
2304+
specify_dir.mkdir(parents=True, exist_ok=True)
2305+
integration_json = specify_dir / "integration.json"
2306+
integration_json.write_text(
2307+
_json.dumps({"skills_source": "marketplace", "integration_state_schema": 1})
2308+
)
2309+
2310+
# The parent skills dir must exist for _get_skills_dir() to return it,
2311+
# but the specific skill subdir must NOT exist yet (create_missing_skills path).
2312+
skills_dir = project_dir / ".claude" / "skills"
2313+
skills_dir.mkdir(parents=True, exist_ok=True)
2314+
2315+
manager = PresetManager(project_dir)
2316+
install_self_test_preset(manager)
2317+
2318+
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
2319+
assert skill_file.exists(), (
2320+
"SKILL.md should be created for marketplace projects even when ai_skills=False"
2321+
)
2322+
content = skill_file.read_text()
2323+
assert "preset:self-test" in content, "Skill should reference preset source"
2324+
22922325
def test_get_skills_dir_returns_none_for_non_string_ai(self, project_dir):
22932326
"""Corrupted init-options ai values should not crash preset skill resolution."""
22942327
init_options = project_dir / ".specify" / "init-options.json"

0 commit comments

Comments
 (0)