feat(vibe): migrate to SkillsIntegration from the old prompts-based MarkdownIntegration#2336
feat(vibe): migrate to SkillsIntegration from the old prompts-based MarkdownIntegration#2336Fango2007 wants to merge 2 commits intogithub:mainfrom
Conversation
…ontmatter 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.
There was a problem hiding this comment.
Pull request overview
Migrates the Mistral Vibe integration from the legacy prompts-based Markdown command layout to the skills-based speckit-<name>/SKILL.md layout required by newer Vibe versions, and post-processes generated skills to add Vibe-specific frontmatter.
Changes:
- Switch
VibeIntegrationto subclassSkillsIntegration, updating config/registrar settings to.vibe/skills/speckit-<name>/SKILL.md. - Add Vibe-specific
post_process_skill_content()and asetup()post-pass to injectuser-invocable: true. - Update the Vibe integration test mixin from Markdown-based tests to skills-based tests.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/integrations/vibe/__init__.py |
Converts Vibe integration to skills layout and injects user-invocable into generated SKILL.md files. |
tests/integrations/test_integration_vibe.py |
Updates integration test harness to validate Vibe as a SkillsIntegration. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 2
| 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 |
There was a problem hiding this comment.
The new user-invocable injection logic isn’t covered by tests for the Vibe integration. Since this behavior is the core purpose of the change (skills should be directly callable), please add an assertion in the Vibe integration tests that generated .vibe/skills/speckit-*/SKILL.md files contain user-invocable: true in frontmatter (similar to the existing Claude integration tests).
There was a problem hiding this comment.
Acknowledged.
An assertion test has been added in the Vibe Integration tests.
MacBook-Pro:spec-kit *$ python -m pytest tests/integrations/test_integration_vibe.py -v 2>&1
================================== test session starts ==============
platform darwin -- Python 3.12.12, pytest-9.0.3, pluggy-1.6.0 -- /Users/*/*/Projects/codebase/clone/spec-kit/.venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/Fango/DEV/Projects/codebase/clone/spec-kit
configfile: pyproject.toml
plugins: cov-7.1.0
collected 27 items
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_registered <- tests/integrations/test_integration_base_skills.py PASSED [ 3%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_is_skills_integration <- tests/integrations/test_integration_base_skills.py PASSED [ 7%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_config_folder <- tests/integrations/test_integration_base_skills.py PASSED [ 11%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_config_commands_subdir <- tests/integrations/test_integration_base_skills.py PASSED [ 14%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_registrar_config <- tests/integrations/test_integration_base_skills.py PASSED [ 18%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_context_file <- tests/integrations/test_integration_base_skills.py PASSED [ 22%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_setup_creates_files <- tests/integrations/test_integration_base_skills.py PASSED [ 25%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_setup_writes_to_correct_directory <- tests/integrations/test_integration_base_skills.py PASSED [ 29%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_skill_directory_structure <- tests/integrations/test_integration_base_skills.py PASSED [ 33%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_skill_frontmatter_structure <- tests/integrations/test_integration_base_skills.py PASSED [ 37%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_skill_uses_template_descriptions <- tests/integrations/test_integration_base_skills.py PASSED [ 40%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_templates_are_processed <- tests/integrations/test_integration_base_skills.py PASSED [ 44%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_skill_body_has_content <- tests/integrations/test_integration_base_skills.py PASSED [ 48%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_plan_references_correct_context_file <- tests/integrations/test_integration_base_skills.py PASSED [ 51%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_all_files_tracked_in_manifest <- tests/integrations/test_integration_base_skills.py PASSED [ 55%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_install_uninstall_roundtrip <- tests/integrations/test_integration_base_skills.py PASSED [ 59%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_modified_file_survives_uninstall <- tests/integrations/test_integration_base_skills.py PASSED [ 62%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_pre_existing_skills_not_removed <- tests/integrations/test_integration_base_skills.py PASSED [ 66%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_setup_upserts_context_section <- tests/integrations/test_integration_base_skills.py PASSED [ 70%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_teardown_removes_context_section <- tests/integrations/test_integration_base_skills.py PASSED [ 74%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_ai_flag_auto_promotes <- tests/integrations/test_integration_base_skills.py PASSED [ 77%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_integration_flag_creates_files <- tests/integrations/test_integration_base_skills.py PASSED [ 81%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_init_options_includes_context_file <- tests/integrations/test_integration_base_skills.py PASSED [ 85%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_options_include_skills_flag <- tests/integrations/test_integration_base_skills.py PASSED [ 88%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_complete_file_inventory_sh <- tests/integrations/test_integration_base_skills.py PASSED [ 92%]
tests/integrations/test_integration_vibe.py::TestVibeIntegration::test_complete_file_inventory_ps <- tests/integrations/test_integration_base_skills.py PASSED [ 96%]
tests/integrations/test_integration_vibe.py::TestVibeUserInvocable::test_all_skills_have_user_invocable PASSED [100%]
====================== 27 passed in 0.37s ====
(specify-cli) MacBook-Pro:spec-kit Fango$ uv run python -m pytest tests/test_agent_config_consistency.py -q
================================== test session starts ==================================
platform darwin -- Python 3.12.12, pytest-9.0.3, pluggy-1.6.0
rootdir: /Users/Fango/DEV/Projects/codebase/clone/spec-kit
configfile: pyproject.toml
plugins: cov-7.1.0
collected 24 items
tests/test_agent_config_consistency.py ........................ [100%]
================================== 24 passed in 0.05s ===
| 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) | ||
|
|
||
|
|
There was a problem hiding this comment.
_inject_frontmatter_flag() duplicates the same frontmatter-injection helper that already exists in ClaudeIntegration. To avoid drift between integrations (and reduce future bug-fix duplication), consider moving this helper to a shared location (e.g., SkillsIntegration/IntegrationBase) and reusing it from both integrations.
| 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) | |
| Reuse the shared frontmatter injection helper from the base integration | |
| to avoid drift between integrations. | |
| """ | |
| return SkillsIntegration._inject_frontmatter_flag(content, key, value) |
There was a problem hiding this comment.
Acknowledged.
Moving _inject_frontmatter_flag to SkillsIntegration would require updating ClaudeIntegration class. This code is maintained by someone else and has significantly more complexity than VibeIntegration (argument hints, hook notes, model invocation flags).
To avoid risk to that integration, the helper is kept local to VibeIntegration, at least for now. A follow-up refactor is up to the Claude integration maintainer who can consolidate it if desired.
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback. If it is truly shareable code between all integrations using skills please pull it up into the SkillIngration
|
Done |
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 1
| content = f.read_text(encoding="utf-8") | ||
| parts = content.split("---", 2) |
There was a problem hiding this comment.
The frontmatter parsing here can raise an IndexError if a SKILL.md is generated without a valid --- ... --- block (since parts[1] is accessed unconditionally). Consider asserting content.startswith('---') and len(parts) >= 3 (with a helpful failure message) before calling yaml.safe_load, so the test fails cleanly when frontmatter is missing/malformed.
| content = f.read_text(encoding="utf-8") | |
| parts = content.split("---", 2) | |
| content = f.read_text(encoding="utf-8") | |
| assert content.startswith("---"), ( | |
| f"{f.parent.name}/SKILL.md is missing the opening frontmatter delimiter '---'" | |
| ) | |
| parts = content.split("---", 2) | |
| assert len(parts) >= 3, ( | |
| f"{f.parent.name}/SKILL.md has malformed frontmatter; expected a '--- ... ---' block" | |
| ) |
|
@Fango2007 Please address Copilot feedback |
PR purpose : Updating Mistral Vibe integration
Since v2.0.0, Mistral Vibe supports custom slash commands through skills system https://github.com/mistralai/mistral-vibe#custom-slash-commands-via-skills
Summary of changes
Switches VibeIntegration from the old prompts-based MarkdownIntegration to SkillsIntegration, adopting the .vibe/skills/speckit-/SKILL.md layout required by Mistral Vibe v2.0.0+.
Post-processes each generated SKILL.md to inject
user-invocable: trueso skills are directly callable by users, not just by other agents.Testing
uv run specify --helpuv sync && uv run pytestAI Disclosure
This PR was drafted with the help of Claude. The code was then manually refined, largely by aligning it with the Claude integration. It also helped me understand the underlying logic and validate the implementation.