mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(extensions): resolve __SPECKIT_COMMAND tokens in auto-registered skills (#3544)
* fix(extensions): resolve command ref tokens in extension skills * fix(extensions): render skill command refs by invocation style Resolve extension skill command-reference tokens with the active skill invocation style so Codex and ZCode use $speckit-* while slash-style agents keep their native forms. Preserve literal command-looking text. * fix(extensions): resolve slash skill command refs from init options --------- Co-authored-by: root <kinsonnee@gmail.com>
This commit is contained in:
@@ -1004,6 +1004,7 @@ class ExtensionManager:
|
||||
from .. import load_init_options
|
||||
from ..agents import CommandRegistrar
|
||||
from ..integrations import get_integration
|
||||
from ..integrations.base import IntegrationBase
|
||||
|
||||
written: List[str] = []
|
||||
opts = load_init_options(self.project_root)
|
||||
@@ -1015,6 +1016,30 @@ class ExtensionManager:
|
||||
registrar = CommandRegistrar()
|
||||
agent_config = registrar.AGENT_CONFIGS.get(selected_ai, {})
|
||||
integration = get_integration(selected_ai)
|
||||
ai_skills_enabled = is_ai_skills_enabled(opts)
|
||||
|
||||
def _resolve_command_ref_tokens(body: str) -> str:
|
||||
"""Resolve explicit command-ref tokens with the active skill style."""
|
||||
|
||||
def _replacement(match: re.Match[str]) -> str:
|
||||
command_name = "speckit." + match.group(1).lower().replace("_", ".")
|
||||
if is_dollar_skills_agent(selected_ai, ai_skills_enabled):
|
||||
return "$" + command_name.replace("speckit.", "speckit-").replace(
|
||||
".", "-"
|
||||
)
|
||||
if is_slash_skills_agent(selected_ai, ai_skills_enabled):
|
||||
return "/" + command_name.replace("speckit.", "speckit-").replace(
|
||||
".", "-"
|
||||
)
|
||||
if integration is not None:
|
||||
return integration.build_command_invocation(command_name)
|
||||
return IntegrationBase.resolve_command_refs(
|
||||
match.group(0), agent_config.get("invoke_separator", ".")
|
||||
)
|
||||
|
||||
return re.sub(
|
||||
r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__", _replacement, body
|
||||
)
|
||||
|
||||
for cmd_info in manifest.commands:
|
||||
cmd_name = cmd_info["name"]
|
||||
@@ -1086,6 +1111,7 @@ class ExtensionManager:
|
||||
body = registrar.resolve_skill_placeholders(
|
||||
selected_ai, frontmatter, body, self.project_root, extension_id=manifest.id
|
||||
)
|
||||
body = _resolve_command_ref_tokens(body)
|
||||
|
||||
original_desc = frontmatter.get("description", "")
|
||||
description = original_desc or f"Extension command: {cmd_name}"
|
||||
|
||||
@@ -970,6 +970,113 @@ class TestExtensionSkillRegistration:
|
||||
assert "Read agents/control" not in content
|
||||
assert "and knowledge-base/" not in content
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ai", "expected_invocation"),
|
||||
[
|
||||
("claude", "/speckit-plan"),
|
||||
("copilot", "/speckit-plan"),
|
||||
("codex", "$speckit-plan"),
|
||||
("kimi", "/skill:speckit-plan"),
|
||||
("zcode", "$speckit-plan"),
|
||||
],
|
||||
)
|
||||
def test_skill_registration_resolves_command_ref_tokens(
|
||||
self, project_dir, temp_dir, ai, expected_invocation
|
||||
):
|
||||
"""Auto-registered skills should resolve explicit command ref tokens."""
|
||||
_create_init_options(project_dir, ai=ai, ai_skills=True)
|
||||
skills_dir = _create_skills_dir(project_dir, ai=ai)
|
||||
|
||||
ext_dir = temp_dir / "command-ref-ext"
|
||||
ext_dir.mkdir()
|
||||
manifest_data = {
|
||||
"schema_version": "1.0",
|
||||
"extension": {
|
||||
"id": "command-ref-ext",
|
||||
"name": "Command Ref Extension",
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
},
|
||||
"requires": {"speckit_version": ">=0.1.0"},
|
||||
"provides": {
|
||||
"commands": [
|
||||
{
|
||||
"name": "speckit.command-ref-ext.run",
|
||||
"file": "commands/run.md",
|
||||
"description": "Run command",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
with open(ext_dir / "extension.yml", "w") as f:
|
||||
yaml.safe_dump(manifest_data, f)
|
||||
|
||||
(ext_dir / "commands").mkdir()
|
||||
(ext_dir / "commands" / "run.md").write_text(
|
||||
"---\n"
|
||||
"description: Run command\n"
|
||||
"---\n\n"
|
||||
"Use __SPECKIT_COMMAND_PLAN__ before proceeding.\n"
|
||||
)
|
||||
|
||||
manager = ExtensionManager(project_dir)
|
||||
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
|
||||
|
||||
content = (skills_dir / "speckit-command-ref-ext-run" / "SKILL.md").read_text()
|
||||
assert "__SPECKIT_COMMAND_PLAN__" not in content
|
||||
assert expected_invocation in content
|
||||
|
||||
def test_skill_registration_does_not_rewrite_literal_speckit_text(
|
||||
self, project_dir, temp_dir
|
||||
):
|
||||
"""Auto-registered skills should leave literal speckit text untouched."""
|
||||
_create_init_options(project_dir, ai="codex", ai_skills=True)
|
||||
skills_dir = _create_skills_dir(project_dir, ai="codex")
|
||||
|
||||
ext_dir = temp_dir / "literal-ref-ext"
|
||||
ext_dir.mkdir()
|
||||
manifest_data = {
|
||||
"schema_version": "1.0",
|
||||
"extension": {
|
||||
"id": "literal-ref-ext",
|
||||
"name": "Literal Ref Extension",
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
},
|
||||
"requires": {"speckit_version": ">=0.1.0"},
|
||||
"provides": {
|
||||
"commands": [
|
||||
{
|
||||
"name": "speckit.literal-ref-ext.run",
|
||||
"file": "commands/run.md",
|
||||
"description": "Run command",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
with open(ext_dir / "extension.yml", "w") as f:
|
||||
yaml.safe_dump(manifest_data, f)
|
||||
|
||||
(ext_dir / "commands").mkdir()
|
||||
(ext_dir / "commands" / "run.md").write_text(
|
||||
"---\n"
|
||||
"description: Run command\n"
|
||||
"---\n\n"
|
||||
"Literal slash form: /speckit.foo.bar\n"
|
||||
"Literal skill form: /speckit-plan\n"
|
||||
"Literal bare form: speckit.foo.bar\n"
|
||||
)
|
||||
|
||||
manager = ExtensionManager(project_dir)
|
||||
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
|
||||
|
||||
content = (skills_dir / "speckit-literal-ref-ext-run" / "SKILL.md").read_text()
|
||||
assert "/speckit.foo.bar" in content
|
||||
assert "/speckit-plan" in content
|
||||
assert "speckit.foo.bar" in content
|
||||
assert "/speckit-foo-bar" not in content
|
||||
assert "$speckit-plan" not in content
|
||||
|
||||
def test_missing_command_file_skipped(self, skills_project, temp_dir):
|
||||
"""Commands with missing source files should be skipped gracefully."""
|
||||
project_dir, skills_dir = skills_project
|
||||
|
||||
Reference in New Issue
Block a user