fix(extensions): render hyphenated hook invocations for Forge projects (#3641)

Forge is a hyphenated slash-command agent: it registers its commands as
`/speckit-<name>` (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) <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-07-22 17:45:51 +05:00
committed by GitHub
parent 0add7131c9
commit 735fe0c5da
2 changed files with 41 additions and 0 deletions

View File

@@ -3608,6 +3608,7 @@ class HookExecutor:
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 @@ class HookExecutor:
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)

View File

@@ -8238,6 +8238,42 @@ class TestHookInvocationRendering:
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"