From 90b802e516a696b4a45868fdd7d2331c2b176b2e Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 22 Jul 2026 16:02:57 +0500 Subject: [PATCH] fix(extensions): render hyphenated hook invocations for Forge projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forge is a hyphenated slash-command agent: it registers its commands as `/speckit-` (see `format_forge_command_name` and `ForgeIntegration.build_command_invocation`), exactly like Cline. `HookExecutor._render_hook_invocation` special-cases dollar-skills agents, kimi, cline, and slash-skills agents, but had no Forge branch. Forge matches none of those, so it fell through to `return f"/{command_id}"` and rendered the DOTTED form — `/speckit.plan`, `/speckit.git.commit` — which Forge does not recognize as a registered command. Add a Forge branch mirroring the adjacent Cline branch, using `format_forge_command_name` (idempotent, same contract as the Cline formatter). Non-Forge agents are unaffected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/specify_cli/extensions/__init__.py | 5 ++++ tests/test_extensions.py | 36 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/specify_cli/extensions/__init__.py b/src/specify_cli/extensions/__init__.py index 39235bf198..78fdf735da 100644 --- a/src/specify_cli/extensions/__init__.py +++ b/src/specify_cli/extensions/__init__.py @@ -3608,6 +3608,7 @@ def _render_hook_invocation(self, command: Any) -> str: dollar_skill_mode = is_dollar_skills_agent(selected_ai, ai_skills_enabled) kimi_skill_mode = selected_ai == "kimi" cline_mode = selected_ai == "cline" + forge_mode = selected_ai == "forge" skill_name = self._skill_name_from_command(command_id) if dollar_skill_mode and skill_name: @@ -3618,6 +3619,10 @@ def _render_hook_invocation(self, command: Any) -> str: from ..integrations.cline import format_cline_command_name return f"/{format_cline_command_name(command_id)}" + if forge_mode: + from ..integrations.forge import format_forge_command_name + + return f"/{format_forge_command_name(command_id)}" use_slash = is_slash_skills_agent(selected_ai, ai_skills_enabled) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 08831c3ae5..018ed8a3fc 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -8238,6 +8238,42 @@ def test_cline_hooks_render_extension_command(self, project_dir): assert execution["command"] == "my-extension.do-something" assert execution["invocation"] == "/speckit-my-extension-do-something" + def test_forge_hooks_render_hyphenated_invocation(self, project_dir): + """Forge projects should render /speckit-* invocations (like Cline).""" + init_options = project_dir / ".specify" / "init-options.json" + init_options.parent.mkdir(parents=True, exist_ok=True) + init_options.write_text(json.dumps({"ai": "forge"})) + + hook_executor = HookExecutor(project_dir) + execution = hook_executor.execute_hook( + { + "extension": "test-ext", + "command": "speckit.tasks", + "optional": False, + } + ) + + assert execution["command"] == "speckit.tasks" + assert execution["invocation"] == "/speckit-tasks" + + def test_forge_hooks_render_extension_command(self, project_dir): + """Forge projects should render /speckit-my-ext-cmd for extension hooks.""" + init_options = project_dir / ".specify" / "init-options.json" + init_options.parent.mkdir(parents=True, exist_ok=True) + init_options.write_text(json.dumps({"ai": "forge"})) + + hook_executor = HookExecutor(project_dir) + execution = hook_executor.execute_hook( + { + "extension": "test-ext", + "command": "my-extension.do-something", + "optional": False, + } + ) + + assert execution["command"] == "my-extension.do-something" + assert execution["invocation"] == "/speckit-my-extension-do-something" + def test_non_skill_command_keeps_slash_invocation(self, project_dir): """Custom hook commands should keep slash invocation style.""" init_options = project_dir / ".specify" / "init-options.json"