mirror of
https://github.com/github/spec-kit.git
synced 2026-07-06 05:53:12 +08:00
* feat(vibe): migrate to SkillsIntegration and inject user-invocable frontmatter Switches VibeIntegration from the old prompts-based MarkdownIntegration to SkillsIntegration, adopting the .vibe/skills/speckit-<name>/SKILL.md layout required by Mistral Vibe v2.0.0+. Post-processes each generated SKILL.md to inject `user-invocable: true` so skills are directly callable by users, not just by other agents. * test(vibe): assert user- invocable: true is present in all generated SKILL.md files * Update tests/integrations/test_integration_vibe.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
134 lines
4.0 KiB
Python
134 lines
4.0 KiB
Python
"""
|
|
Mistral Vibe CLI integration — skills-based agent.
|
|
|
|
Vibe uses ``.vibe/skills/speckit-<name>/SKILL.md`` layout (enforced since v2.0.0).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ..base import IntegrationOption, SkillsIntegration
|
|
from ..manifest import IntegrationManifest
|
|
|
|
|
|
class VibeIntegration(SkillsIntegration):
|
|
key = "vibe"
|
|
config = {
|
|
"name": "Mistral Vibe",
|
|
"folder": ".vibe/",
|
|
"commands_subdir": "skills",
|
|
"install_url": "https://github.com/mistralai/mistral-vibe",
|
|
"requires_cli": True,
|
|
}
|
|
registrar_config = {
|
|
"dir": ".vibe/skills",
|
|
"format": "markdown",
|
|
"args": "$ARGUMENTS",
|
|
"extension": "/SKILL.md",
|
|
}
|
|
context_file = "AGENTS.md"
|
|
|
|
@classmethod
|
|
def options(cls) -> list[IntegrationOption]:
|
|
return [
|
|
IntegrationOption(
|
|
"--skills",
|
|
is_flag=True,
|
|
default=True,
|
|
help="Install as agent skills",
|
|
),
|
|
]
|
|
|
|
@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
|
|
"""
|
|
lines = content.splitlines(keepends=True)
|
|
|
|
# Pre-scan: bail out if already present in frontmatter
|
|
dash_count = 0
|
|
for line in lines:
|
|
stripped = line.rstrip("\n\r")
|
|
if stripped == "---":
|
|
dash_count += 1
|
|
if dash_count == 2:
|
|
break
|
|
continue
|
|
if dash_count == 1 and stripped.startswith(f"{key}:"):
|
|
return content
|
|
|
|
# Inject before the closing --- of frontmatter
|
|
out: list[str] = []
|
|
dash_count = 0
|
|
injected = False
|
|
for line in lines:
|
|
stripped = line.rstrip("\n\r")
|
|
if stripped == "---":
|
|
dash_count += 1
|
|
if dash_count == 2 and not injected:
|
|
if line.endswith("\r\n"):
|
|
eol = "\r\n"
|
|
elif line.endswith("\n"):
|
|
eol = "\n"
|
|
else:
|
|
eol = ""
|
|
out.append(f"{key}: {value}{eol}")
|
|
injected = True
|
|
out.append(line)
|
|
return "".join(out)
|
|
|
|
|
|
def post_process_skill_content(self, content: str) -> str:
|
|
"""
|
|
Inject Vibe-specific frontmatter flags:
|
|
- user-invocable: allows the skill to be invoked by the user (not just other agents)
|
|
"""
|
|
updated = self._inject_frontmatter_flag(content, "user-invocable")
|
|
return updated
|
|
|
|
def setup(
|
|
self,
|
|
project_root: Path,
|
|
manifest: IntegrationManifest,
|
|
parsed_options: dict[str, Any] | None = None,
|
|
**opts: Any,
|
|
) -> list[Path]:
|
|
"""Install Vibe skills then inject Vibe-specific flags"""
|
|
import click
|
|
|
|
click.secho(
|
|
"Warning: The .vibe/skills layout requires Mistral Vibe v2.0.0 or newer. "
|
|
"Please ensure your installation is up to date.",
|
|
fg="yellow",
|
|
err=True,
|
|
)
|
|
|
|
created = super().setup(project_root, manifest, parsed_options=parsed_options, **opts)
|
|
|
|
# Post-process generated skill files
|
|
skills_dir = self.skills_dest(project_root).resolve()
|
|
|
|
for path in created:
|
|
# Only touch SKILL.md files under the skills directory
|
|
try:
|
|
path.resolve().relative_to(skills_dir)
|
|
except ValueError:
|
|
continue
|
|
if path.name != "SKILL.md":
|
|
continue
|
|
|
|
content_bytes = path.read_bytes()
|
|
content = content_bytes.decode("utf-8")
|
|
|
|
updated = self.post_process_skill_content(content)
|
|
|
|
if updated != content:
|
|
path.write_bytes(updated.encode("utf-8"))
|
|
self.record_file_in_manifest(path, project_root, manifest)
|
|
|
|
return created
|