Manages extension hook execution.
| 2860 | |
| 2861 | |
| 2862 | class HookExecutor: |
| 2863 | """Manages extension hook execution.""" |
| 2864 | |
| 2865 | def __init__(self, project_root: Path): |
| 2866 | """Initialize hook executor. |
| 2867 | |
| 2868 | Args: |
| 2869 | project_root: Root directory of the spec-kit project |
| 2870 | """ |
| 2871 | self.project_root = project_root |
| 2872 | self.extensions_dir = project_root / ".specify" / "extensions" |
| 2873 | self.config_file = project_root / ".specify" / "extensions.yml" |
| 2874 | self._init_options_cache: Optional[Dict[str, Any]] = None |
| 2875 | |
| 2876 | def _load_init_options(self) -> Dict[str, Any]: |
| 2877 | """Load persisted init options used to determine invocation style. |
| 2878 | |
| 2879 | Uses the shared helper from specify_cli and caches values per executor |
| 2880 | instance to avoid repeated filesystem reads during hook rendering. |
| 2881 | """ |
| 2882 | if self._init_options_cache is None: |
| 2883 | from .. import load_init_options |
| 2884 | |
| 2885 | payload = load_init_options(self.project_root) |
| 2886 | self._init_options_cache = payload if isinstance(payload, dict) else {} |
| 2887 | return self._init_options_cache |
| 2888 | |
| 2889 | @staticmethod |
| 2890 | def _skill_name_from_command(command: Any) -> str: |
| 2891 | """Map a command id like speckit.plan to speckit-plan skill name.""" |
| 2892 | if not isinstance(command, str): |
| 2893 | return "" |
| 2894 | command_id = command.strip() |
| 2895 | if not command_id.startswith("speckit."): |
| 2896 | return "" |
| 2897 | return f"speckit-{command_id[len('speckit.') :].replace('.', '-')}" |
| 2898 | |
| 2899 | def _render_hook_invocation(self, command: Any) -> str: |
| 2900 | """Render an agent-specific invocation string for a hook command.""" |
| 2901 | if not isinstance(command, str): |
| 2902 | return "" |
| 2903 | |
| 2904 | command_id = command.strip() |
| 2905 | if not command_id: |
| 2906 | return "" |
| 2907 | |
| 2908 | init_options = self._load_init_options() |
| 2909 | selected_ai = init_options.get("ai") |
| 2910 | ai_skills_enabled = is_ai_skills_enabled(init_options) |
| 2911 | |
| 2912 | dollar_skill_mode = is_dollar_skills_agent(selected_ai, ai_skills_enabled) |
| 2913 | kimi_skill_mode = selected_ai == "kimi" |
| 2914 | cline_mode = selected_ai == "cline" |
| 2915 | |
| 2916 | skill_name = self._skill_name_from_command(command_id) |
| 2917 | if dollar_skill_mode and skill_name: |
| 2918 | return f"${skill_name}" |
| 2919 | if kimi_skill_mode and skill_name: |
no outgoing calls