mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
feat: add Mistral Vibe integration with Claude parity (#4075)
* feat: add Mistral Vibe integration with Claude parity - Add VibeIntegration class with ARGUMENT_HINTS, user-invocable, disable-model-invocation - Add comprehensive test suite matching Claude integration - Support all Spec Kit workflows (py/sh/ps script types) * fix: address Vibe integration issues and test cleanup - Fix Vibe to use .vibe/hooks.toml with toml-vibe format instead of ignored .vibe/settings.json, adding toml-vibe event handler - Remove unsupported argument-hint injection (Vibe schema doesn't support it) - Restructure test file to inherit from SkillsIntegrationTests mixin - Remove all unused imports to pass Ruff F401 checks Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai> * fix: add name field to Vibe hooks and fix toml regex patterns - Add required 'name' field for each Vibe hook in hooks.toml - Fix regex patterns in _merge_vibe_toml_fragment and _remove_vibe_toml_entries to correctly match [[hooks]] blocks instead of [} characters Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai> * fix: align Vibe hooks with HookConfig schema and drop stray devcontainer lock - use Vibe's 'match' field (re:-prefixed regex translation) instead of unsupported 'matcher'; emit only on tool hooks (rejected on post_agent) - limit CANONICAL_TO_NATIVE to Vibe's three hook types (pre_tool, post_tool, post_agent); unsupported events skip with a warning - deduplicate generated hook names (Vibe drops duplicates by name) - add behavioral tests for toml-vibe generation, merging, and teardown - remove accidentally committed .devcontainer/devcontainer-lock.json Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: wrap Vibe hook stdout in structured JSON response envelope Vibe parses any non-empty hook stdout as a JSON HookStructuredResponse; plain text is reported as a hook failure and its output dropped. Add a hook_specific_output envelope to the dispatcher (template and runtime) that emits {"decision": "allow", "hook_specific_output": {"additional_context": ...}} and declare it for all Vibe events: post_tool injects the context, pre_tool/post_agent parse cleanly and ignore it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: quote Vibe hook commands for cmd.exe on Windows hosts Vibe launches hooks via asyncio.create_subprocess_shell, which is %COMSPEC% (cmd.exe) on Windows — POSIX single-quoting is not quoting there, so an interpreter or dispatcher path containing spaces made every hook fail to start. Add a 'cmd' quoting target to _shell_quote (double-quote when needed, embedded quotes doubled per MSVCRT argv rules), resolve it host-side like 'host', and select it for Vibe when generating on a Windows host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pin POSIX quoting target in Vibe test for Windows CI runners test_posix_host_keeps_shlex_quoting asserts host (shlex) quoting, but on a Windows runner _vibe_target_os() resolves to 'cmd' and the command is double-quoted. Monkeypatch the target so the test exercises the POSIX path on every platform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Mistral Vibe <vibe@mistral.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Mistral Vibe
parent
13c3c942fc
commit
54f8b2cdf0
+171
-5
@@ -297,6 +297,10 @@ def _emit(output, envelope, native_event=""):
|
||||
hookSpecificOutput → {"hookSpecificOutput": {"hookEventName": ..., "additionalContext": ...}}
|
||||
additionalContext → {"additionalContext": ...} (top-level, Copilot)
|
||||
additional_context → {"additional_context": ...} (top-level, Cursor)
|
||||
hook_specific_output → {"decision": "allow", "hook_specific_output":
|
||||
{"additional_context": ...}} (Vibe: any non-empty
|
||||
stdout must parse as a HookStructuredResponse or
|
||||
the hook is reported failed and output dropped)
|
||||
suppress → emit nothing (strict-JSON agents on events whose
|
||||
output can't be used)
|
||||
plain (default) → passthrough (Claude/Codex inject plain stdout)
|
||||
@@ -320,6 +324,9 @@ def _emit(output, envelope, native_event=""):
|
||||
if envelope == "additional_context":
|
||||
sys.stdout.write(json.dumps({"additional_context": output}) + "\\n")
|
||||
return
|
||||
if envelope == "hook_specific_output":
|
||||
sys.stdout.write(json.dumps({"decision": "allow", "hook_specific_output": {"additional_context": output}}) + "\\n")
|
||||
return
|
||||
sys.stdout.write(output)
|
||||
|
||||
|
||||
@@ -339,9 +346,10 @@ def main():
|
||||
timeout = 120
|
||||
# Optional 5th arg: context-injection envelope for stdout (C13): plain
|
||||
# (default), hookSpecificOutput, additionalContext, additional_context,
|
||||
# or suppress. Unknown values fall back to plain passthrough.
|
||||
# hook_specific_output, or suppress. Unknown values fall back to plain
|
||||
# passthrough.
|
||||
envelope = sys.argv[4] if len(sys.argv) >= 5 else "plain"
|
||||
if envelope not in ("plain", "hookSpecificOutput", "additionalContext", "additional_context", "suppress"):
|
||||
if envelope not in ("plain", "hookSpecificOutput", "additionalContext", "additional_context", "hook_specific_output", "suppress"):
|
||||
envelope = "plain"
|
||||
# Optional 6th arg: native event name for hookSpecificOutput's
|
||||
# hookEventName field (required by Qwen's hooks spec; included by
|
||||
@@ -672,8 +680,10 @@ def resolve_and_run_event_command(
|
||||
context-injection protocol (C13): ``plain`` passthrough (Claude/Codex
|
||||
inject plain stdout), ``hookSpecificOutput``/``additionalContext``/
|
||||
``additional_context`` JSON wrappers (Gemini/Tabnine/Qwen/Devin, Copilot,
|
||||
Cursor respectively), or ``suppress`` (strict-JSON agents on events whose
|
||||
output can't be used).
|
||||
Cursor respectively), ``hook_specific_output`` (Vibe's
|
||||
HookStructuredResponse — any non-empty stdout that isn't valid JSON is
|
||||
reported as a hook failure and dropped), or ``suppress`` (strict-JSON
|
||||
agents on events whose output can't be used).
|
||||
|
||||
*native_event* is the agent's native hookEventName (e.g. ``"SessionStart"``),
|
||||
required inside ``hookSpecificOutput`` by Qwen's hooks spec (and included
|
||||
@@ -738,6 +748,13 @@ def _emit_event_stdout(output: str, envelope: str, native_event: str = "") -> No
|
||||
if envelope == "additional_context":
|
||||
sys.stdout.write(json.dumps({"additional_context": output}) + "\n")
|
||||
return
|
||||
if envelope == "hook_specific_output":
|
||||
# Vibe parses any non-empty hook stdout as a HookStructuredResponse;
|
||||
# plain text would be reported as a hook failure. Wrap it as an
|
||||
# explicit allow with additional_context (injected on post_tool,
|
||||
# harmlessly ignored on pre_tool/post_agent).
|
||||
sys.stdout.write(json.dumps({"decision": "allow", "hook_specific_output": {"additional_context": output}}) + "\n")
|
||||
return
|
||||
sys.stdout.write(output)
|
||||
|
||||
|
||||
@@ -1093,6 +1110,15 @@ def _shell_quote(value: str, target_os: str) -> str:
|
||||
"""
|
||||
if target_os == "windows":
|
||||
return "'" + value.replace("'", "''") + "'"
|
||||
if target_os == "cmd":
|
||||
# cmd.exe (Vibe launches hooks via create_subprocess_shell, which is
|
||||
# %COMSPEC% on Windows): single quotes are not quoting there, so a
|
||||
# POSIX-quoted path with spaces would break apart. Double-quote only
|
||||
# when needed; embedded double quotes are doubled (MSVCRT argv
|
||||
# parsing treats "" inside a quoted string as a literal quote).
|
||||
if re.fullmatch(r"[A-Za-z0-9_.\-\\/:]+", value):
|
||||
return value
|
||||
return '"' + value.replace('"', '""') + '"'
|
||||
# "host" and "posix" both use POSIX quoting. On Windows the single-
|
||||
# command-string formats (Claude/Gemini/Qwen/Devin/Tabnine) are run via
|
||||
# Git Bash or the agent's POSIX-ish shell, so POSIX quoting is correct and
|
||||
@@ -1100,6 +1126,17 @@ def _shell_quote(value: str, target_os: str) -> str:
|
||||
return shlex.quote(value)
|
||||
|
||||
|
||||
def _vibe_target_os() -> str:
|
||||
"""Quoting target for Vibe hook commands.
|
||||
|
||||
Vibe launches hooks with ``asyncio.create_subprocess_shell`` — the host's
|
||||
native shell: POSIX ``sh`` on Unix, ``cmd.exe`` (%COMSPEC%) on Windows,
|
||||
where POSIX single-quoting is not quoting at all and an interpreter or
|
||||
dispatcher path containing spaces would split.
|
||||
"""
|
||||
return "cmd" if os.name == "nt" else "host"
|
||||
|
||||
|
||||
def _dispatcher_command(
|
||||
integration: IntegrationBase,
|
||||
project_root: Path,
|
||||
@@ -1122,6 +1159,8 @@ def _dispatcher_command(
|
||||
both POSIX and Windows variants into one checked-in file (Copilot): ``host``
|
||||
uses the host-resolved interpreter (venv-aware), while ``posix``/``windows``
|
||||
emit portable interpreters so the config works on either OS (#S4).
|
||||
``cmd`` also uses the host-resolved interpreter but quotes for cmd.exe —
|
||||
for agents that launch hooks through the native Windows shell (Vibe).
|
||||
|
||||
Each component is shell-quoted for the target shell (R2) so an interpreter
|
||||
path with spaces or a command/event containing shell metacharacters is
|
||||
@@ -1147,7 +1186,10 @@ def _dispatcher_command(
|
||||
shape the agent's hook protocol requires. Plain-passthrough agents
|
||||
(Claude/Codex) declare no envelope and get no extra argument.
|
||||
"""
|
||||
if target_os == "host":
|
||||
if target_os in ("host", "cmd"):
|
||||
# "cmd" is host-resolved too (venv-aware): it is selected only when
|
||||
# generating on a Windows host for an agent that runs hooks through
|
||||
# cmd.exe (Vibe), and differs from "host" purely in quoting style.
|
||||
interpreter = _resolve_interpreter(project_root)
|
||||
else:
|
||||
interpreter = _resolve_interpreter_for_target(target_os)
|
||||
@@ -1357,6 +1399,55 @@ def install_integration_events(
|
||||
manifest.record_existing(rel)
|
||||
created.append(config_path)
|
||||
|
||||
elif fmt == "toml-vibe":
|
||||
# Vibe hooks.toml custom merge. Flat [[hooks]] array; Vibe's
|
||||
# HookConfig schema is name/type/command/match/timeout, with type
|
||||
# limited to "pre_tool" | "post_tool" | "post_agent". Hook names must
|
||||
# be unique (Vibe silently drops duplicates by name), so a per-file
|
||||
# counter suffix disambiguates handlers whose commands share a final
|
||||
# segment (e.g. speckit.a.validate vs speckit.b.validate).
|
||||
lines: list[str] = []
|
||||
used_names: set[str] = set()
|
||||
for ev, handlers in filtered.items():
|
||||
native = canonical_to_native[ev]
|
||||
for cfg in handlers:
|
||||
command = cfg.get("command", "")
|
||||
dispatcher_cmd = _dispatcher_command(
|
||||
integration, project_root, command, ev,
|
||||
target_os=_vibe_target_os(),
|
||||
timeout_seconds=cfg.get("timeout", 60),
|
||||
)
|
||||
command_stem = command.split('.')[-1] if command else "unknown"
|
||||
command_stem = re.sub(r'[^A-Za-z0-9_-]+', '-', command_stem) or "unknown"
|
||||
base_name = f"speckit-{native}-{command_stem}"
|
||||
hook_name = base_name
|
||||
suffix = 2
|
||||
while hook_name in used_names:
|
||||
hook_name = f"{base_name}-{suffix}"
|
||||
suffix += 1
|
||||
used_names.add(hook_name)
|
||||
lines.append("[[hooks]]")
|
||||
lines.append(f'name = {_toml_quote(hook_name)}')
|
||||
lines.append(f'type = {_toml_quote(native)}')
|
||||
# Vibe's field is `match` (fnmatch glob, or `re:`-prefixed
|
||||
# regex, case-insensitive) and it is only valid on tool
|
||||
# hooks — HookConfig rejects `match` on post_agent. Canonical
|
||||
# matchers are Claude-style regexes ("Edit|Write"), so
|
||||
# non-wildcard matchers are emitted as `re:` patterns.
|
||||
matcher = cfg.get("matcher", "*")
|
||||
if matcher and matcher != "*" and native in ("pre_tool", "post_tool"):
|
||||
lines.append(f'match = {_toml_quote("re:" + matcher)}')
|
||||
lines.append(f'command = {_toml_quote(dispatcher_cmd)}')
|
||||
lines.append(f'timeout = {_native_timeout(integration, cfg.get("timeout", 60) + EVENT_TIMEOUT_BUFFER)}')
|
||||
lines.append('speckit_marker = true')
|
||||
lines.append('')
|
||||
# S5: only track when the merge wrote (skips on unreadable file).
|
||||
if _merge_vibe_toml_fragment(config_path, "\n".join(lines)):
|
||||
rel = str(config_path.relative_to(project_root))
|
||||
if rel not in manifest.files:
|
||||
manifest.record_existing(rel)
|
||||
created.append(config_path)
|
||||
|
||||
elif fmt == "json-flat":
|
||||
# Cursor hooks.json custom merge. Flat command-string entries, one
|
||||
# per handler (#2), single resolved command string (#6/#16).
|
||||
@@ -1479,6 +1570,8 @@ def _remove_native_event_hooks(
|
||||
_remove_copilot_entries(config_path)
|
||||
elif fmt == "toml":
|
||||
_remove_toml_entries(config_path)
|
||||
elif fmt == "toml-vibe":
|
||||
_remove_vibe_toml_entries(config_path)
|
||||
elif fmt in ("json-nested", "json-flat"):
|
||||
_remove_json_entries(config_path)
|
||||
elif fmt == "json-root-nested":
|
||||
@@ -1973,6 +2066,42 @@ def _merge_toml_fragment(dst: Path, fragment: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _merge_vibe_toml_fragment(dst: Path, fragment: str) -> bool:
|
||||
"""Merge Specify-owned Vibe TOML hook entries into *dst*, regenerating the file.
|
||||
|
||||
Vibe uses a flat [[hooks]] array with type/matcher/command fields.
|
||||
This removes any existing Specify-marked hooks and appends the new fragment.
|
||||
An unreadable or undecodable pre-existing file aborts the merge instead
|
||||
of discarding the user's bytes, mirroring ``_load_user_json`` (#22).
|
||||
Returns False when skipped so callers avoid tracking the untouched file
|
||||
(S5).
|
||||
"""
|
||||
_ensure_safe_destination(dst)
|
||||
existing = ""
|
||||
if dst.exists():
|
||||
try:
|
||||
existing = dst.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
logger.warning(
|
||||
"Could not read %s (it may be unreadable or not UTF-8); "
|
||||
"skipping event-config merge to preserve user content.",
|
||||
dst,
|
||||
)
|
||||
logger.debug("Read error detail: %s", exc)
|
||||
return False
|
||||
# Remove existing Specify-marked [[hooks]] blocks
|
||||
# Match [[hooks]] ... speckit_marker = true (with any content in between)
|
||||
existing = re.sub(
|
||||
r'\[\[hooks\]\]\n(?:(?!\[\[hooks\]\]).)*?speckit_marker = true\n*',
|
||||
"",
|
||||
existing,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
dst.write_text(existing.rstrip() + "\n\n" + fragment + "\n", encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def _remove_toml_entries(dst: Path) -> bool:
|
||||
"""Remove Specify-marked TOML entries; delete the file if now empty (#14).
|
||||
|
||||
@@ -2016,6 +2145,43 @@ def _remove_toml_entries(dst: Path) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _remove_vibe_toml_entries(dst: Path) -> bool:
|
||||
"""Remove Specify-marked Vibe TOML hook entries; delete the file if now empty.
|
||||
|
||||
Returns True if the file was deleted (no user content remained).
|
||||
"""
|
||||
if not dst.exists():
|
||||
return False
|
||||
_ensure_safe_destination(dst)
|
||||
try:
|
||||
existing = dst.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
logger.warning(
|
||||
"Could not read %s (it may be unreadable or not UTF-8); "
|
||||
"skipping event-config cleanup to preserve user content.",
|
||||
dst,
|
||||
)
|
||||
logger.debug("Read error detail: %s", exc)
|
||||
return False
|
||||
# Remove Specify-marked [[hooks]] blocks
|
||||
cleaned = re.sub(
|
||||
r'\[\[hooks\]\]\n(?:(?!\[\[hooks\]\]).)*?speckit_marker = true\n*',
|
||||
"",
|
||||
existing,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
# If only whitespace/comments remain, the file had no user content
|
||||
stripped = "\n".join(
|
||||
line for line in cleaned.splitlines()
|
||||
if line.strip() and not line.strip().startswith("#")
|
||||
)
|
||||
if not stripped:
|
||||
dst.unlink(missing_ok=True)
|
||||
return True
|
||||
dst.write_text(cleaned, encoding="utf-8")
|
||||
return False
|
||||
|
||||
|
||||
def _merge_copilot_json(dst: Path, new_hooks: dict[str, list]) -> bool:
|
||||
"""Merge Specify-owned hooks into Copilot's dedicated hooks JSON (#8).
|
||||
|
||||
|
||||
@@ -11,9 +11,25 @@ from typing import Any
|
||||
|
||||
from ..base import IntegrationOption, SkillsIntegration
|
||||
from ..manifest import IntegrationManifest
|
||||
from ..._utils import dump_frontmatter
|
||||
|
||||
# Per-command frontmatter overrides for skills that should run in a forked
|
||||
# subagent context.
|
||||
#
|
||||
# This is intentionally empty. ``analyze`` was previously forked (added in
|
||||
# #2511) on the assumption that its heavy reads collapse to a short summary,
|
||||
# but in practice ``/speckit-analyze`` returns a 300-500 line report that is
|
||||
# injected back into the main conversation. In long sessions each subsequent
|
||||
# fork inherits that growing context, compounding overhead until the chat
|
||||
# freezes (#3185). Until a command genuinely returns a compact result, no
|
||||
# command opts into ``context: fork``. The injection mechanism below stays in
|
||||
# place so a future command can be added here when that holds true.
|
||||
FORK_CONTEXT_COMMANDS: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class VibeIntegration(SkillsIntegration):
|
||||
"""Integration for Mistral Vibe skills."""
|
||||
|
||||
key = "vibe"
|
||||
config = {
|
||||
"name": "Mistral Vibe",
|
||||
@@ -28,24 +44,63 @@ class VibeIntegration(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
multi_install_safe = True
|
||||
|
||||
# Vibe's hooks schema supports exactly three hook types (HookConfig
|
||||
# rejects anything else): pre_tool, post_tool, post_agent. Unsupported
|
||||
# canonical events (session_start/session_end/user_prompt_submit) are
|
||||
# intentionally absent so install_integration_events skips them with a
|
||||
# warning instead of writing entries Vibe would refuse to load.
|
||||
CANONICAL_TO_NATIVE = {
|
||||
"pre_tool_use": "pre_tool",
|
||||
"post_tool_use": "post_tool",
|
||||
"stop": "post_agent",
|
||||
}
|
||||
events_config_file = ".vibe/hooks.toml"
|
||||
events_format = "toml-vibe"
|
||||
# Vibe parses any non-empty hook stdout as a JSON HookStructuredResponse;
|
||||
# plain text is reported as a hook failure and its output dropped. The
|
||||
# dispatcher therefore wraps handler stdout as {"decision": "allow",
|
||||
# "hook_specific_output": {"additional_context": ...}} for every event:
|
||||
# post_tool injects additional_context, pre_tool/post_agent ignore it but
|
||||
# still parse cleanly.
|
||||
events_context_envelope = {"*": "hook_specific_output"}
|
||||
|
||||
@classmethod
|
||||
def options(cls) -> list[IntegrationOption]:
|
||||
return [
|
||||
opts = super().options()
|
||||
opts.append(
|
||||
IntegrationOption(
|
||||
"--skills",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
help="Install as agent skills",
|
||||
),
|
||||
]
|
||||
)
|
||||
return opts
|
||||
|
||||
def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: str) -> str:
|
||||
"""Render a processed command template as a Vibe skill."""
|
||||
skill_name = f"speckit-{template_name.replace('.', '-')}"
|
||||
description = frontmatter.get(
|
||||
"description",
|
||||
f"Spec-kit workflow command: {template_name}",
|
||||
)
|
||||
skill_frontmatter = self._build_skill_fm(
|
||||
skill_name, description, f"templates/commands/{template_name}.md"
|
||||
)
|
||||
frontmatter_text = dump_frontmatter(skill_frontmatter)
|
||||
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"
|
||||
|
||||
def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
|
||||
from specify_cli.agents import CommandRegistrar
|
||||
return CommandRegistrar.build_skill_frontmatter(
|
||||
self.key, name, description, source
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
|
||||
"""
|
||||
Insert ``key: value`` before the closing ``---`` if not already present.
|
||||
Value: true by default
|
||||
"""
|
||||
"""Insert ``key: value`` before the closing ``---`` if not already present."""
|
||||
lines = content.splitlines(keepends=True)
|
||||
|
||||
# Pre-scan: bail out if already present in frontmatter
|
||||
@@ -80,13 +135,45 @@ class VibeIntegration(SkillsIntegration):
|
||||
out.append(line)
|
||||
return "".join(out)
|
||||
|
||||
def post_process_skill_content(self, content: str) -> str:
|
||||
@staticmethod
|
||||
def _skill_stem_from_content(content: str) -> str | None:
|
||||
"""Derive the command stem (e.g. ``analyze``) from a skill's frontmatter.
|
||||
|
||||
Reads the ``name:`` field of the first frontmatter block and strips
|
||||
the ``speckit-`` prefix. Returns ``None`` when no name is present.
|
||||
"""
|
||||
Inject shared hook guidance and Vibe-specific frontmatter flags:
|
||||
- user-invocable: allows the skill to be invoked by the user (not just other agents)
|
||||
dash_count = 0
|
||||
for line in content.splitlines():
|
||||
stripped = line.rstrip("\r\n")
|
||||
if stripped == "---":
|
||||
dash_count += 1
|
||||
if dash_count == 2:
|
||||
break
|
||||
continue
|
||||
if dash_count == 1 and stripped.startswith("name:"):
|
||||
name = stripped[len("name:"):].strip().strip('"').strip("'")
|
||||
if name.startswith("speckit-"):
|
||||
return name[len("speckit-"):]
|
||||
return name or None
|
||||
return None
|
||||
|
||||
def post_process_skill_content(self, content: str) -> str:
|
||||
"""Inject Vibe-specific frontmatter flags.
|
||||
|
||||
Applied by every skill-generation path (setup, presets, extensions),
|
||||
so Vibe-specific frontmatter stays consistent however the SKILL.md
|
||||
was produced.
|
||||
"""
|
||||
updated = super().post_process_skill_content(content)
|
||||
updated = self._inject_frontmatter_flag(updated, "user-invocable")
|
||||
updated = self._inject_frontmatter_flag(updated, "disable-model-invocation", "false")
|
||||
|
||||
stem = self._skill_stem_from_content(updated)
|
||||
if stem:
|
||||
fork_config = FORK_CONTEXT_COMMANDS.get(stem)
|
||||
if fork_config:
|
||||
for key, value in fork_config.items():
|
||||
updated = self._inject_frontmatter_flag(updated, key, value)
|
||||
return updated
|
||||
|
||||
def setup(
|
||||
|
||||
Reference in New Issue
Block a user