feat: add --skills support to opencode integration#2494
Conversation
--skills support to opencode integration
5b3b00c to
ac6514e
Compare
There was a problem hiding this comment.
Pull request overview
Adds an opt-in --skills mode to the opencode integration so scaffolding can produce agentskills.io-style SKILL.md files under .opencode/skills/<skill>/SKILL.md, with accompanying tests.
Changes:
- Added a
--skillsintegration option toOpencodeIntegrationand asetup()branch to scaffold skills-format outputs. - Added Opencode skills-mode tests validating SKILL.md creation, frontmatter shape, and that default markdown mode remains unchanged.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/specify_cli/integrations/opencode/__init__.py |
Adds --skills option and delegates setup to SkillsIntegration when enabled. |
tests/integrations/test_integration_opencode.py |
Adds a new test class covering skills-mode scaffolding behavior and SKILL.md frontmatter. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ac6514e to
da2958b
Compare
| def effective_invoke_separator( | ||
| self, parsed_options: dict[str, Any] | None = None | ||
| ) -> str: | ||
| if parsed_options and parsed_options.get("skills"): | ||
| return "-" | ||
| if self._skills_mode: | ||
| return "-" | ||
| return self.invoke_separator # default: "." | ||
|
|
| assert skills_dir.is_dir(), "Skills directory was not created" | ||
| plan_skill = skills_dir / "speckit-plan" / "SKILL.md" | ||
| assert plan_skill.exists(), "speckit-plan/SKILL.md not found" | ||
| assert not list((project / ".opencode" / "command").glob("*.md")) if (project / ".opencode" / "command").exists() else True |
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
da2958b to
9e1a396
Compare
| if not self._skills_mode and project_root: | ||
| skills_dir = project_root / ".opencode" / "skills" | ||
| if skills_dir.is_dir(): | ||
| self._skills_mode = any( | ||
| d.is_dir() and (d / "SKILL.md").is_file() | ||
| for d in skills_dir.glob("speckit-*") | ||
| ) | ||
| return super().dispatch_command( | ||
| command_name, args, | ||
| project_root=project_root, model=model, timeout=timeout, stream=stream, | ||
| ) | ||
|
|
|
Please address Copilot feedback |
9e1a396 to
0d9fd06
Compare
| self._skills_mode = bool(parsed_options.get("skills")) | ||
| if self._skills_mode: |
| parsed_options = parsed_options or {} | ||
| self._skills_mode = bool(parsed_options.get("skills")) | ||
| if self._skills_mode: |
| parsed_options = parsed_options or {} | ||
| self._skills_mode = bool(parsed_options.get("skills")) | ||
| if self._skills_mode: |
| ) -> list[Path]: | ||
| parsed_options = parsed_options or {} | ||
| self._skills_mode = bool(parsed_options.get("skills")) | ||
| if self._skills_mode: |
|
Please address Copilot feedback. If not applicable, please explain why |
0d9fd06 to
1e37a4c
Compare
|
I adressed the changes and aligned the skill integration with how it was done for Copilot |
| # Persist the CLI options so later operations (e.g. extension install, preset add) | ||
| # can adapt their behaviour without re-scanning the filesystem. | ||
| # Must be saved BEFORE extension and preset install so _get_skills_dir() works. | ||
| init_opts = { |
| result = CliRunner().invoke(app, [ | ||
| "init", "--here", "--integration", "opencode", | ||
| "--integration-options", "--skills", | ||
| "--script", "sh", "--no-git", "--ignore-agent-tools", | ||
| ], catch_exceptions=False) |
| # Detect skills mode from project layout when not already set via setup(). | ||
| # Kept local so the singleton's _skills_mode is never mutated here. | ||
| skills_mode = self._skills_mode | ||
| if not skills_mode and project_root: | ||
| skills_dir = project_root / ".opencode" / "skills" | ||
| if skills_dir.is_dir(): |
| @classmethod | ||
| def options(cls) -> list[IntegrationOption]: | ||
| return [ | ||
| IntegrationOption( | ||
| "--skills", | ||
| is_flag=True, | ||
| default=False, | ||
| help="Scaffold commands as agent skills (speckit-<name>/SKILL.md) instead of .md files", | ||
| ), |
| # Ensure ai_skills is set for SkillsIntegration so downstream | ||
| # tools (extensions, presets) emit SKILL.md overrides correctly. | ||
| # Also set for integrations running in skills mode (e.g. Copilot | ||
| # with --skills or Opencode with --skills). | ||
| from .integrations.base import SkillsIntegration as _SkillsPersist | ||
| if isinstance(resolved_integration, _SkillsPersist) or getattr(resolved_integration, "_skills_mode", False): | ||
| init_opts["ai_skills"] = True |
1e37a4c to
9a0b836
Compare
Adds opt-in `--skills` support to OpencodeIntegration, producing `speckit-<name>/SKILL.md` files under `.opencode/skills/` instead of flat `.md` files. Opencode natively supports this format (https://opencode.ai/docs/skills/). Activate via: `specify init --integration opencode --integration-options="--skills"`
9a0b836 to
1fecdea
Compare
| # Also set for integrations running in skills mode (e.g. Copilot | ||
| # with --skills or Opencode with --skills). | ||
| # Use parsed_options as the source of truth for skills mode, not | ||
| # _skills_mode (a mutable runtime flag), so the setting persists | ||
| # correctly even if the integration is restored without setup(). | ||
| from .integrations.base import SkillsIntegration as _SkillsPersist | ||
| if isinstance(resolved_integration, _SkillsPersist) or integration_parsed_options.get("skills"): |
| integration._skills_mode = True # stale flag from prior skills setup | ||
|
|
||
| project = tmp_path / "regular-project" | ||
| project.mkdir() | ||
| (project / ".opencode").mkdir() | ||
| # No .opencode/skills/ — this is a non-skills project | ||
|
|
||
| with mock.patch("subprocess.run") as mock_run: | ||
| mock_run.return_value = mock.MagicMock(returncode=0) | ||
| integration.dispatch_command( | ||
| "speckit.plan", "test args", project_root=project, stream=False, | ||
| ) | ||
|
|
||
| called_args = mock_run.call_args[0][0] | ||
| assert "--command" in called_args | ||
| # Should use dotted invocation despite stale _skills_mode=True | ||
| assert called_args[called_args.index("--command") + 1] == "speckit.plan" | ||
| # singleton _skills_mode remains unchanged | ||
| assert integration._skills_mode is True |
| from .integrations.base import IntegrationBase as _IntegrationBase | ||
| body = _IntegrationBase.resolve_command_refs(body, "-") |
|
Please address Copilot feedback. Note if opencode is only a skills based integration you do not have to make it accept --integration-options="--skills". Some of the other coding agents initially had commands and added skills support later so they needed a way to express that they want skills vs commands. |
Description
Adds opt-in
--skillssupport toOpencodeIntegration, mirroring the pattern used byClaudeIntegration. When the flagis passed, commands are scaffolded as
speckit-<name>/SKILL.mdfiles under.opencode/skills/instead of flat.mdfiles under
.opencode/command/.Opencode natively supports the agentskills.io SKILL.md format at
.opencode/skills/<name>/SKILL.md(seehttps://opencode.ai/docs/skills/), with the same frontmatter (
name,description,compatibility,metadata) thatCommandRegistrar.build_skill_frontmatter()already produces — so no custom post-processing was needed.Activate via:
specify init --integration opencode --integration-options="--skills"Testing
uv run specify --helpuv sync --extra test && uv run pytestAll 28 opencode integration tests pass, including 5 new tests in
TestOpencodeSkillsModecovering:--skillsoption declaration (is_flag=True,default=False).opencode/skills/.mdcommand files created in skills modename,description,compatibility,metadata.author)Agent config consistency check (
tests/test_agent_config_consistency.py) also passes (26/26).Manual smoke test:
AI Disclosure
This PR was written with Claude Code (claude-sonnet-4-6). The implementation approach, code, and tests were generated
with AI assistance and reviewed and validated by the author.