Skip to content

Commit df93a74

Browse files
echarrodclaude
andcommitted
feat(vibe): add --no-model-invocation integration option
Refs #2654. Writes disable-model-invocation: true on core speckit-* command skills (Mistral Vibe README: explicit-only, model does not see or invoke it). Extension skills stay model-invocable. Persists through integration.json so `integration upgrade vibe` reapplies it. Assisted-by: Claude Code (claude-sonnet-5-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mv5BUgsqLc1YT7q6y5EjPJ
1 parent 67e0f72 commit df93a74

3 files changed

Lines changed: 335 additions & 7 deletions

File tree

‎docs/integrations/vibe.md‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,40 @@ Skills are installed as `.vibe/skills/speckit-<command>/SKILL.md`.
2424
Invoke them in the agent's chat, for example `/speckit-specify` or
2525
`/speckit-bug-assess` for the installed bug extension.
2626

27-
Generated skills are both user- and model-invocable. The optional
27+
Generated skills are both user- and model-invocable by default. The optional
2828
`--integration-options="--skills"` flag restates the default.
2929

30+
## User-Invocable-Only Skills
31+
32+
To stop Mistral Vibe invoking the core Spec Kit command skills automatically:
33+
34+
```bash
35+
specify init my-project --integration vibe --integration-options="--no-model-invocation"
36+
```
37+
38+
This writes `disable-model-invocation: true` into the core command skills.
39+
Per Mistral Vibe's [README](https://github.com/mistralai/mistral-vibe#skills-system),
40+
such a skill is explicit-only: you can still invoke `/speckit-<command>`, but
41+
the model does not see or invoke it automatically.
42+
43+
- Extension skills stay model-invocable, because mandatory extension hooks rely
44+
on the model invoking them. If an extension's mandatory hook runs a core
45+
command (its `command:` is a core `speckit.<command>`), leave this option off:
46+
the model cannot run that hook while the core skill is user-invocable only.
47+
- The option is stored with the integration's other options and reapplied by
48+
`specify integration upgrade vibe`. Passing `--integration-options` to
49+
`upgrade` replaces all stored options; `--integration-options=""` clears
50+
them. Re-running `specify init` likewise stores only the options passed to it.
51+
- Core command skills that a preset overrides, or stops overriding, are reverted
52+
to the default by commands that re-register preset skills without reinstalling
53+
Mistral Vibe: commands that install, remove or update presets (including
54+
`specify bundle` commands that carry presets), `specify integration use vibe`,
55+
and `specify integration switch vibe` when Mistral Vibe is installed but not
56+
the default. To reapply the option, run `specify integration upgrade vibe
57+
--force` while Mistral Vibe is the default integration (`--force` is needed
58+
because those commands modify installed skill files, and it overwrites local
59+
edits to Spec Kit-managed files).
60+
3061
## Runtime Events
3162

3263
Native events use `.vibe/hooks.toml`. Vibe supports only `pre_tool_use`,

‎src/specify_cli/integrations/vibe/__init__.py‎

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)