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
40 changes: 16 additions & 24 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,13 @@ The base classes handle most work automatically. Override only when the agent de
| Override | When to use | Example |
|---|---|---|
| `command_filename(template_name)` | Custom file naming or extension | Copilot → `speckit.{name}.agent.md` |
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag, Copilot → `--skills` flag |
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `.agent.md` + `.prompt.md` + `.vscode/settings.json` (default) or `speckit-<name>/SKILL.md` (skills mode) |
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag, Copilot → `--commands` flag |
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `speckit-<name>/SKILL.md` (default) or `.agent.md` + `.prompt.md` + `.vscode/settings.json` (`--commands`) |
| `teardown()` | Custom uninstall logic | Rarely needed; base handles manifest-tracked files |

**Example — Copilot (fully custom `setup`):**

Copilot extends `IntegrationBase` directly because it creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. It also supports a `--skills` mode that scaffolds `speckit-<name>/SKILL.md` under `.github/skills/` using composition with an internal `_CopilotSkillsHelper`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.
Copilot extends `IntegrationBase` directly because it supports two layouts. It scaffolds `speckit-<name>/SKILL.md` under `.github/skills/` by default using composition with an internal `_CopilotSkillsHelper`. Its `--commands` mode creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.

### 7. Update Devcontainer files (Optional)

Expand Down Expand Up @@ -415,36 +415,28 @@ Some agents require custom processing beyond the standard template transformatio

### Copilot Integration

GitHub Copilot has unique requirements:
GitHub Copilot uses skills by default, scaffolded as
`speckit-<name>/SKILL.md` under `.github/skills/`.

- Commands use `.agent.md` extension (not `.md`)
- Each command gets a companion `.prompt.md` file in `.github/prompts/`
- Installs `.vscode/settings.json` with prompt file recommendations
- Context file lives at `.github/copilot-instructions.md`

Implementation: Extends `IntegrationBase` with custom `setup()` method that:
**Commands mode (`--commands`):** Copilot also supports a commands-based layout
via `--integration-options="--commands"`. When enabled:

1. Processes templates with `process_template()`
2. Generates companion `.prompt.md` files
3. Merges VS Code settings

**Skills mode (`--skills`):** Copilot also supports an alternative skills-based layout
via `--integration-options="--skills"`. When enabled:
- Commands use `.agent.md` extension under `.github/agents/`
- Each command gets a companion `.prompt.md` file in `.github/prompts/`
- `.vscode/settings.json` is merged with prompt file recommendations
- `build_command_invocation()` returns bare args for `--agent` dispatch

- Commands are scaffolded as `speckit-<name>/SKILL.md` under `.github/skills/`
- No companion `.prompt.md` files are generated
- No `.vscode/settings.json` merge
- `post_process_skill_content()` injects a `mode: speckit.<stem>` frontmatter field
- `build_command_invocation()` returns `/speckit-<stem>` instead of bare args
In the default skills mode, no companion prompts or VS Code settings merge are
created, and `build_command_invocation()` returns `/speckit-<stem>`.

The two modes are mutually exclusive — a project uses one or the other:

```bash
# Default mode: .agent.md agents + .prompt.md companions + settings merge
# Default skills mode: speckit-<name>/SKILL.md under .github/skills/
specify init my-project --integration copilot

# Skills mode: speckit-<name>/SKILL.md under .github/skills/
specify init my-project --integration copilot --integration-options="--skills"
# Commands mode: .agent.md agents + .prompt.md companions + settings merge
specify init my-project --integration copilot --integration-options="--commands"
```

### Forge Integration
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ |
| [Forge](https://forgecode.dev/) | `forge` | |
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | |
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | Defaults to legacy markdown mode: `.agent.md` command files under `.github/agents/`, companion `.prompt.md` files under `.github/prompts/`, and a `.vscode/settings.json` merge. Pass `--integration-options="--skills"` to scaffold skills as `speckit-<command>/SKILL.md` under `.github/skills/` instead. Legacy markdown mode is deprecated and will stop being the default in a future release. |
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | Skills-based by default; installs `speckit-<command>/SKILL.md` under `.github/skills/`. Pass `--integration-options="--commands"` to use the supported commands layout: `.agent.md` files under `.github/agents/`, companion `.prompt.md` files under `.github/prompts/`, and a `.vscode/settings.json` merge. |
| [Goose](https://goose-docs.ai/) | `goose` | Uses YAML recipe format in `.goose/recipes/` |
| [Grok Build](https://docs.x.ai/build/overview) | `grok` | Skills-based integration; installs skills into `.grok/skills` and invokes them as `/speckit-<command>` |
| [Hermes](https://github.com/NousResearch/hermes-agent) | `hermes` | Skills-based integration; installs skills globally into `~/.hermes/skills/` |
Expand Down Expand Up @@ -234,7 +234,8 @@ Some integrations accept additional options via `--integration-options`:
| ----------- | ------------------- | -------------------------------------------------------------- |
| `generic` | `--commands-dir` | Required. Directory for command files |
| `kimi` | `--migrate-legacy` | Migrate legacy `.kimi/skills/` installs to `.kimi-code/skills/` (including dotted→hyphenated skill naming, e.g. `speckit.xxx` → `speckit-xxx`) |
| `copilot` | `--skills` | Scaffold commands as agent skills (`speckit-<command>/SKILL.md` under `.github/skills/`, invoked as `/speckit-<command>`) instead of the default legacy markdown mode (`.github/agents/*.agent.md` plus `.github/prompts/*.prompt.md` and a `.vscode/settings.json` merge). Without this flag, install warns that legacy markdown mode is deprecated. |
| `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. |

Example:

Expand Down
4 changes: 2 additions & 2 deletions src/specify_cli/integration_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ def with_integration_setting(
# ``script_type`` changes (``parsed_options`` and ``raw_options`` both
# None), the previously-stored ``parsed_options`` are retained above, so
# deriving the separator from the argument (None) would drop an
# options-dependent separator (e.g. Copilot ``--skills`` -> "-") back to
# the default ".".
# options-dependent separator (e.g. Copilot ``--commands`` -> ".") back to
# the default "-".
current["invoke_separator"] = integration.effective_invoke_separator(
current.get("parsed_options"), project_root
)
Expand Down
38 changes: 21 additions & 17 deletions src/specify_cli/integrations/_migrate_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,14 @@ def integration_switch(

selected_script = _resolve_script_type(project_root, script)

# Resolve and validate target options before uninstalling the current
# integration. Invalid options must not leave the project partially
# switched with the previous integration already removed.
target_raw_options, target_parsed_options = _resolve_integration_options(
target_integration, current, target, integration_options
)
target_integration.is_skills_mode(target_parsed_options, project_root)

# Phase 1: Uninstall current integration (if any)
if installed_key:
current_integration = get_integration(installed_key)
Expand Down Expand Up @@ -403,7 +411,10 @@ def integration_switch(
fallback_key = installed_keys[0]
fallback_integration = get_integration(fallback_key)
if fallback_integration is not None:
raw_options, parsed_options = _resolve_integration_options(
(
fallback_raw_options,
fallback_parsed_options,
) = _resolve_integration_options(
fallback_integration, current, fallback_key, None
)
_set_default_integration_or_exit(
Expand All @@ -412,8 +423,8 @@ def integration_switch(
fallback_key,
fallback_integration,
installed_keys,
raw_options=raw_options,
parsed_options=parsed_options,
raw_options=fallback_raw_options,
parsed_options=fallback_parsed_options,
)
else:
_write_integration_json(
Expand All @@ -423,13 +434,6 @@ def integration_switch(
_remove_integration_json(project_root)
current = _read_integration_json(project_root)

# Build parsed options from --integration-options so the integration
# can determine its effective invoke separator before shared infra
# is installed.
raw_options, parsed_options = _resolve_integration_options(
target_integration, current, target, integration_options
)

# Refresh shared infrastructure to the current CLI version. Switching
# integrations is exactly when stale vendored shared scripts (e.g.
# update-agent-context.sh that pre-dates the target integration's
Expand All @@ -445,11 +449,11 @@ def integration_switch(
force=refresh_shared_infra,
refresh_managed=True,
invoke_separator=_invoke_separator_for_integration(
target_integration, current, target, parsed_options,
target_integration, current, target, target_parsed_options,
project_root=project_root,
),
invoke_prefix=_invoke_prefix_for_integration(
target_integration, target, parsed_options, project_root
target_integration, target, target_parsed_options, project_root
),
refresh_hint=(
"To overwrite customizations, re-run with "
Expand All @@ -471,14 +475,14 @@ def integration_switch(
target_integration.key,
target_integration.config,
project_root,
parsed_options,
target_parsed_options,
)
try:
target_integration.setup(
project_root, manifest,
parsed_options=parsed_options,
parsed_options=target_parsed_options,
script_type=selected_script,
raw_options=raw_options,
raw_options=target_raw_options,
events=events_map,
)
manifest.save()
Expand All @@ -489,8 +493,8 @@ def integration_switch(
target_integration,
_dedupe_integration_keys([*installed_keys, target_integration.key]),
script_type=selected_script,
raw_options=raw_options,
parsed_options=parsed_options,
raw_options=target_raw_options,
parsed_options=target_parsed_options,
)

except Exception as exc:
Expand Down
4 changes: 2 additions & 2 deletions src/specify_cli/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ def is_skills_mode(
on-disk layout to avoid silently migrating an existing project to a
different mode. The default ignores it.
The default (command-first integrations, e.g. Copilot's default
layout) is skills mode only when ``--skills`` was requested.
The default for command-first integrations is skills mode only when
``--skills`` was requested.
``SkillsIntegration`` overrides this to return ``True`` by default;
skills-first integrations that expose a legacy opt-out (e.g. Bob)
override it to honor their own flag.
Expand Down
Loading