@@ -95,6 +95,14 @@ class VibeIntegration(SkillsIntegration):
9595 # still parse cleanly.
9696 events_context_envelope = {"*" : "hook_specific_output" }
9797
98+ # Set by ``setup()`` from ``--no-model-invocation``; applies to core
99+ # command skills only, since mandatory extension hooks rely on the model
100+ # invoking the hook skill. Integrations are process-wide singletons, so
101+ # preset overrides re-registered later in the same run see the same value.
102+ # Commands that rewrite skills without calling ``setup()`` (e.g.
103+ # ``preset add``, ``integration use``) get the default.
104+ _disable_model_invocation : bool = False
105+
98106 @classmethod
99107 def options (cls ) -> list [IntegrationOption ]:
100108 opts = super ().options ()
@@ -106,6 +114,17 @@ def options(cls) -> list[IntegrationOption]:
106114 help = "Install as agent skills" ,
107115 ),
108116 )
117+ opts .append (
118+ IntegrationOption (
119+ "--no-model-invocation" ,
120+ is_flag = True ,
121+ default = False ,
122+ help = (
123+ "Install core command skills as user-invocable only "
124+ "(disable-model-invocation: true)"
125+ ),
126+ )
127+ )
109128 return opts
110129
111130 def build_exec_args (
@@ -204,6 +223,14 @@ def _skill_stem_from_content(content: str) -> str | None:
204223 Reads the ``name:`` field of the first frontmatter block and strips
205224 the ``speckit-`` prefix. Returns ``None`` when no name is present.
206225 """
226+ name = VibeIntegration ._skill_name_from_content (content )
227+ if name and name .startswith ("speckit-" ):
228+ return name [len ("speckit-" ):]
229+ return name
230+
231+ @staticmethod
232+ def _skill_name_from_content (content : str ) -> str | None :
233+ """Return the ``name:`` field of a skill's first frontmatter block."""
207234 dash_count = 0
208235 for line in content .splitlines ():
209236 stripped = line .rstrip ("\r \n " )
@@ -214,21 +241,35 @@ def _skill_stem_from_content(content: str) -> str | None:
214241 continue
215242 if dash_count == 1 and stripped .startswith ("name:" ):
216243 name = stripped [len ("name:" ):].strip ().strip ('"' ).strip ("'" )
217- if name .startswith ("speckit-" ):
218- return name [len ("speckit-" ):]
219244 return name or None
220245 return None
221246
247+ @classmethod
248+ def _is_core_command_skill (cls , content : str ) -> bool :
249+ from specify_cli .extensions import CORE_COMMAND_NAMES # lazy: avoids circular import
250+
251+ name = cls ._skill_name_from_content (content )
252+ return bool (name ) and name .startswith ("speckit-" ) and (
253+ name .removeprefix ("speckit-" ) in CORE_COMMAND_NAMES
254+ )
255+
222256 def post_process_skill_content (self , content : str ) -> str :
223257 """Inject Vibe-specific frontmatter flags.
224258
225259 Applied by every skill-generation path (setup, presets, extensions),
226260 so Vibe-specific frontmatter stays consistent however the SKILL.md
227261 was produced.
262+
263+ ``disable-model-invocation`` is ``true`` for core command skills when
264+ the last ``setup()`` in this process was given
265+ ``--no-model-invocation``, else ``false``.
228266 """
229267 updated = super ().post_process_skill_content (content )
230268 updated = self ._inject_frontmatter_flag (updated , "user-invocable" )
231- updated = self ._inject_frontmatter_flag (updated , "disable-model-invocation" , "false" )
269+ disable = self ._disable_model_invocation and self ._is_core_command_skill (updated )
270+ updated = self ._inject_frontmatter_flag (
271+ updated , "disable-model-invocation" , "true" if disable else "false"
272+ )
232273
233274 stem = self ._skill_stem_from_content (updated )
234275 if stem :
@@ -255,7 +296,16 @@ def setup(
255296 err = True ,
256297 )
257298
258- return super ().setup (project_root , manifest , parsed_options = parsed_options , ** opts )
299+ self ._disable_model_invocation = (
300+ (parsed_options or {}).get ("no_model_invocation" ) is True
301+ )
302+ try :
303+ return super ().setup (
304+ project_root , manifest , parsed_options = parsed_options , ** opts
305+ )
306+ except BaseException :
307+ self ._disable_model_invocation = False
308+ raise
259309
260310 @staticmethod
261311 def _hook_target_os () -> str :
0 commit comments