Skip to content

Commit 64a8e8b

Browse files
committed
Fix legacy hook invocations for skills integrations
1 parent 10d7d39 commit 64a8e8b

5 files changed

Lines changed: 65 additions & 32 deletions

File tree

src/specify_cli/extensions.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2552,22 +2552,32 @@ def _render_hook_invocation(self, command: Any) -> str:
25522552
pass
25532553

25542554
init_options = self._load_init_options()
2555+
skill_name = self._skill_name_from_command(command_id)
25552556
selected_ai = init_options.get("ai")
2556-
codex_skill_mode = selected_ai == "codex" and bool(init_options.get("ai_skills"))
2557-
claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills"))
2558-
kimi_skill_mode = selected_ai == "kimi"
2559-
cursor_skill_mode = selected_ai == "cursor-agent" and bool(init_options.get("ai_skills"))
2560-
cline_mode = selected_ai == "cline"
2557+
ai_skills = bool(init_options.get("ai_skills"))
2558+
if skill_name and isinstance(selected_ai, str):
2559+
try:
2560+
from .integrations import get_integration
2561+
from .integrations.base import SkillsIntegration
25612562

2562-
skill_name = self._skill_name_from_command(command_id)
2563-
if codex_skill_mode and skill_name:
2564-
return f"${skill_name}"
2565-
if claude_skill_mode and skill_name:
2566-
return f"/{skill_name}"
2567-
if kimi_skill_mode and skill_name:
2568-
return f"/skill:{skill_name}"
2569-
if cursor_skill_mode and skill_name:
2570-
return f"/{skill_name}"
2563+
integration = get_integration(selected_ai)
2564+
except Exception:
2565+
integration = None
2566+
2567+
if integration is not None:
2568+
parsed_options = {"skills": True} if ai_skills else None
2569+
if isinstance(integration, SkillsIntegration) and (ai_skills or selected_ai == "kimi"):
2570+
return integration.build_user_command_invocation(
2571+
command_id,
2572+
parsed_options=parsed_options,
2573+
)
2574+
if selected_ai == "copilot" and ai_skills:
2575+
return integration.build_user_command_invocation(
2576+
command_id,
2577+
parsed_options=parsed_options,
2578+
)
2579+
2580+
cline_mode = selected_ai == "cline"
25712581
if cline_mode:
25722582
from .integrations.cline import format_cline_command_name
25732583

src/specify_cli/integrations/agy/__init__.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,4 @@ def setup(
6464
fg="yellow",
6565
err=True,
6666
)
67-
created = super().setup(project_root, manifest, parsed_options=parsed_options, **opts)
68-
69-
skills_dir = self.skills_dest(project_root).resolve()
70-
for path in created:
71-
try:
72-
path.resolve().relative_to(skills_dir)
73-
except ValueError:
74-
continue
75-
if path.name != "SKILL.md":
76-
continue
77-
78-
content = path.read_bytes().decode("utf-8")
79-
updated = self.post_process_skill_content(content)
80-
if updated != content:
81-
path.write_bytes(updated.encode("utf-8"))
82-
self.record_file_in_manifest(path, project_root, manifest)
83-
84-
return created
67+
return super().setup(project_root, manifest, parsed_options=parsed_options, **opts)

tests/integrations/test_integration_agy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def test_hook_note_injected_in_skills_with_hooks(self, tmp_path):
100100
assert "replace dots" in content, (
101101
"speckit-specify should have dot-to-hyphen hook note"
102102
)
103+
assert "$speckit-git-commit" in content
103104

104105
def test_hook_note_not_in_skills_without_hooks(self):
105106
"""Skills without hook sections should not get the note."""
@@ -120,6 +121,7 @@ def test_hook_note_idempotent(self):
120121
once = AgyIntegration._inject_hook_command_note(content)
121122
twice = AgyIntegration._inject_hook_command_note(once)
122123
assert once == twice, "Hook note injection should be idempotent"
124+
assert "$speckit-git-commit" in once
123125

124126
def test_hook_note_preserves_indentation(self):
125127
"""The injected note must match the indentation of the target line."""

tests/integrations/test_integration_subcommand.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,9 @@ def test_install_multi_safe_integration(self, tmp_path):
229229
assert data["integration_state_schema"] == 1
230230
assert data["installed_integrations"] == ["claude", "codex"]
231231
assert data["integration_settings"]["claude"]["invoke_separator"] == "-"
232+
assert data["integration_settings"]["claude"]["command_prefix"] == "/"
232233
assert data["integration_settings"]["codex"]["invoke_separator"] == "-"
234+
assert data["integration_settings"]["codex"]["command_prefix"] == "$"
233235

234236
assert (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
235237
assert (project / ".agents" / "skills" / "speckit-plan" / "SKILL.md").exists()

tests/test_extensions.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4884,6 +4884,42 @@ def test_codex_hooks_render_dollar_skill_invocation(self, project_dir):
48844884
assert execution["command"] == "speckit.tasks"
48854885
assert execution["invocation"] == "$speckit-tasks"
48864886

4887+
@pytest.mark.parametrize(
4888+
("ai", "expected"),
4889+
[
4890+
("agy", "$speckit-git-commit"),
4891+
("trae", "$speckit-git-commit"),
4892+
("devin", "/speckit-git-commit"),
4893+
],
4894+
)
4895+
def test_skills_hooks_render_from_legacy_init_options(self, project_dir, ai, expected):
4896+
"""Skills integrations should render native invocations without integration.json."""
4897+
init_options = project_dir / ".specify" / "init-options.json"
4898+
init_options.parent.mkdir(parents=True, exist_ok=True)
4899+
init_options.write_text(json.dumps({"ai": ai, "ai_skills": True}))
4900+
4901+
hook_executor = HookExecutor(project_dir)
4902+
execution = hook_executor.execute_hook(
4903+
{
4904+
"extension": "test-ext",
4905+
"command": "speckit.git.commit",
4906+
"optional": False,
4907+
}
4908+
)
4909+
4910+
assert execution["command"] == "speckit.git.commit"
4911+
assert execution["invocation"] == expected
4912+
4913+
def test_legacy_codex_without_skills_keeps_dotted_invocation(self, project_dir):
4914+
"""Legacy Codex command-mode metadata should not be promoted during fallback."""
4915+
init_options = project_dir / ".specify" / "init-options.json"
4916+
init_options.parent.mkdir(parents=True, exist_ok=True)
4917+
init_options.write_text(json.dumps({"ai": "codex", "ai_skills": False}))
4918+
4919+
hook_executor = HookExecutor(project_dir)
4920+
4921+
assert hook_executor._render_hook_invocation("speckit.tasks") == "/speckit.tasks"
4922+
48874923
def test_cline_hooks_render_hyphenated_invocation(self, project_dir):
48884924
"""Cline projects should render /speckit-* invocations."""
48894925
init_options = project_dir / ".specify" / "init-options.json"

0 commit comments

Comments
 (0)