Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| [Trae](https://www.trae.ai/) | `trae` | Skills-based integration; skills are installed automatically |
| [ZCode](https://zcode.z.ai/) | `zcode` | Skills-based integration; installs skills into `.zcode/skills/` and invokes them as `$speckit-<command>` |
| [Zed](https://zed.dev/) | `zed` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `/speckit-<command>` |
| Generic | `generic` | Bring your own agent — use `--integration generic --integration-options="--commands-dir <path>"` for AI coding agents not listed above |
| Generic | `generic` | Bring your own agent — use `--integration generic --integration-options="--commands-dir <path>"` for AI coding agents not listed above; add `--skills` for the `speckit-<name>/SKILL.md` layout |

## List Available Integrations

Expand Down Expand Up @@ -237,6 +237,7 @@ Some integrations accept additional options via `--integration-options`:
| Integration | Option | Description |
| ----------- | ------------------- | -------------------------------------------------------------- |
| `generic` | `--commands-dir` | Required. Directory for command files |
| `generic` | `--skills` | Render commands as `speckit-<name>/SKILL.md` directories under `--commands-dir` instead of flat `speckit.<name>.md` files. Command references and next-step guidance switch to `/speckit-<name>`. Generic's output directory is a runtime option rather than a static per-agent folder, so this does not enable extension/preset add-on skill registration in either layout. |
| `kimi` | `--migrate-legacy` | Migrate legacy `.kimi/skills/` installs to `.kimi-code/skills/` (including dotted→hyphenated skill naming, e.g. `speckit.xxx` → `speckit-xxx`) |
| `copilot` | `--commands` | Scaffold `.github/agents/*.agent.md` commands with `.github/prompts/*.prompt.md` companions and merge `.vscode/settings.json` instead of using the default skills layout. |
| `copilot` | `--skills` | Force the default skills layout, overriding an existing commands layout during an explicit migration. |
Expand All @@ -245,6 +246,7 @@ Example:

```bash
specify integration install generic --integration-options="--commands-dir .myagent/cmds"
specify integration install generic --integration-options="--commands-dir .myagent/skills --skills"
```

## Scaffold a New Integration
Expand Down
9 changes: 9 additions & 0 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,15 @@ def resolve_active_skills_dir(project_root: Path) -> Path | None:
if not isinstance(agent, str) or not agent:
return None

# generic's output directory is a runtime --commands-dir CLI option, not
# a static per-agent folder (its config["folder"] is None), so there is
# no directory extension/preset skill registration could safely resolve
# here even when the project was scaffolded with --skills. Registration
# stays disabled for generic in both layouts, matching flat-mode generic
# (which never persists ai_skills=True and so never reaches this point).
if agent == "generic":
return None

ai_skills_enabled = _is_ai_skills_enabled(opts)
if not ai_skills_enabled and agent != "kimi":
return None
Expand Down
1 change: 1 addition & 0 deletions src/specify_cli/_invocation_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"claude",
"copilot",
"cursor-agent",
"generic",
"hermes",
"lingma",
"rovodev",
Expand Down
115 changes: 113 additions & 2 deletions src/specify_cli/integrations/generic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,35 @@

Requires ``--commands-dir`` to specify the output directory for command
files. No longer special-cased in the core CLI — just another
integration with its own required option.
integration with its own required option. ``--skills`` renders the same
templates as ``speckit-<name>/SKILL.md`` directories under that same
directory instead of flat ``speckit.<name>.md`` files.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from ..base import IntegrationOption, MarkdownIntegration
import yaml

from ..base import IntegrationOption, MarkdownIntegration, SkillsIntegration, yaml_quote
from ..manifest import IntegrationManifest


class _GenericSkillsHelper(SkillsIntegration):
"""Internal helper supplying skills-mode post-processing for
``GenericIntegration`` (e.g. the dot-to-hyphen hook invocation note).

Not registered in the integration registry — ``GenericIntegration``
itself renders skills content directly in ``_build_skill_content()``
and only delegates to this helper's ``post_process_skill_content()``,
mirroring the pattern ``CopilotIntegration`` uses for its skills mode.
"""

key = "generic"


class GenericIntegration(MarkdownIntegration):
"""Integration for user-specified (generic) agents."""

Expand All @@ -32,6 +49,17 @@ class GenericIntegration(MarkdownIntegration):
"extension": ".md",
}

def effective_invoke_separator(
self,
parsed_options: dict[str, Any] | None = None,
project_root: Path | None = None,
) -> str:
"""``"-"`` for the ``--skills`` SKILL.md layout, ``"."`` for the
default flat ``speckit.<name>.md`` layout — mirrors the separator
``_build_skill_content()`` already uses to process each template.
"""
return "-" if self.is_skills_mode(parsed_options, project_root) else "."

@classmethod
def options(cls) -> list[IntegrationOption]:
return [
Expand All @@ -40,6 +68,16 @@ def options(cls) -> list[IntegrationOption]:
required=True,
help="Directory for command files (e.g. .myagent/commands/)",
),
IntegrationOption(
"--skills",
is_flag=True,
default=False,
help=(
Comment thread
mnriem marked this conversation as resolved.
"Render commands as speckit-<name>/SKILL.md directories "
"under --commands-dir instead of flat speckit.<name>.md "
"files"
),
),
]

@staticmethod
Expand Down Expand Up @@ -84,6 +122,67 @@ def _resolve_commands_dir(
"--commands-dir is required for the generic integration"
)

def _build_skill_content(
self, src_file: Path, script_type: str, project_root: Path
) -> tuple[str, str]:
"""Render *src_file* as a SKILL.md body.

Returns ``(skill_name, content)``. Mirrors the frontmatter and
body shape ``SkillsIntegration.setup()`` produces for other
skills-format agents, so ``speckit-<name>/SKILL.md`` files
emitted here follow the same `agentskills.io
<https://agentskills.io/specification>`_ layout.
"""
raw = src_file.read_text(encoding="utf-8")
command_name = src_file.stem
skill_name = f"speckit-{command_name.replace('.', '-')}"

frontmatter: dict[str, Any] = {}
if raw.startswith("---"):
fm_lines = raw.splitlines(keepends=True)
fm_close = next(
(i for i in range(1, len(fm_lines)) if fm_lines[i].rstrip() == "---"),
None,
)
if fm_close is not None:
try:
fm = yaml.safe_load("".join(fm_lines[1:fm_close]))
if isinstance(fm, dict):
frontmatter = fm
except yaml.YAMLError:
pass

processed_body = self.process_template(
raw, self.key, script_type, "$ARGUMENTS",
project_root=project_root,
invoke_separator="-",
)
if processed_body.startswith("---"):
body_lines = processed_body.splitlines(keepends=True)
close_idx = next(
(i for i in range(1, len(body_lines)) if body_lines[i].rstrip() == "---"),
None,
)
if close_idx is not None:
processed_body = body_lines[close_idx][3:] + "".join(
body_lines[close_idx + 1:]
)

description = frontmatter.get("description") or f"Spec Kit: {command_name} workflow"
skill_content = (
f"---\n"
f"name: {yaml_quote(skill_name)}\n"
f"description: {yaml_quote(description)}\n"
f"compatibility: {yaml_quote('Requires spec-kit project structure with .specify/ directory')}\n"
f"metadata:\n"
f" author: {yaml_quote('github-spec-kit')}\n"
f" source: {yaml_quote('templates/commands/' + src_file.name)}\n"
f"---\n"
f"{processed_body}"
)
skill_content = _GenericSkillsHelper().post_process_skill_content(skill_content)
return skill_name, skill_content

def commands_dest(self, project_root: Path) -> Path:
"""Not supported for GenericIntegration — use setup() directly.

Expand Down Expand Up @@ -129,9 +228,21 @@ def setup(

script_type = opts.get("script_type", "sh")
arg_placeholder = "$ARGUMENTS"
skills_enabled = bool((parsed_options or {}).get("skills"))
created: list[Path] = []

for src_file in templates:
if skills_enabled:
Comment thread
mnriem marked this conversation as resolved.
skill_name, skill_content = self._build_skill_content(
src_file, script_type, project_root
)
dst_file = self.write_file_and_record(
skill_content, dest / skill_name / "SKILL.md",
project_root, manifest
)
created.append(dst_file)
continue

raw = src_file.read_text(encoding="utf-8")
processed = self.process_template(
raw, self.key, script_type, arg_placeholder,
Expand Down
Loading