-
Notifications
You must be signed in to change notification settings - Fork 7.2k
From Agents to Integrations #1924
Description
Summary
Migrate Spec Kit's agent scaffolding system from monolithic dictionaries (AGENT_CONFIG, CommandRegistrar.AGENT_CONFIGS) to a plugin-based integration architecture where each coding assistant (Copilot, Claude, Gemini, etc.) delivers its own setup/teardown logic. Files are hash-tracked so only unmodified files are removed on uninstall.
Terminology
| User-facing | Internal code | Notes |
|---|---|---|
--integration copilot |
integrations/ package |
Noun form for CLI flag |
specify integrate install |
Typer subcommand group | Verb form for subcommand |
| Integration | What we call Copilot/Claude/Gemini etc. | Not "agent", not "AI" |
Current State
AGENT_CONFIGin__init__.py— metadata dict (name, folder, install_url, requires_cli) for 25 agentsCommandRegistrar.AGENT_CONFIGSinagents.py— registration dict (dir, format, args, extension), must stay in synccore_pack/agents//— bundled template files per agent- Agent-specific logic scattered in if/elif chains (copilot prompt files, TOML format, skills rendering)
- No install tracking — no safe way to uninstall or switch agents
Target State
src/specify_cli/
integrations/
__init__.py # Registry, discover(), INTEGRATION_REGISTRY
base.py # IntegrationBase ABC + MarkdownIntegration base class
manifest.py # Hash-tracked install/uninstall
copilot/
__init__.py # CopilotIntegration — companion prompts, vscode settings
templates/ # command templates
claude/
__init__.py # ClaudeIntegration(MarkdownIntegration)
templates/
gemini/
__init__.py # GeminiIntegration — TOML format override
templates/
... # one subpackage per integration (all 25+)
agents.py # CommandRegistrar (unchanged, still used by extensions/presets)
__init__.py # init() routes --ai (legacy) vs --integration (new)
Every integration is a self-contained subpackage. No integration logic lives in the
core CLI — only the base classes, manifest tracker, and registry. This means any
integration can be extracted from the wheel and distributed via the catalog without
code changes.
What an Integration Owns
| Layer | Examples | Shared or Per-Integration |
|---|---|---|
| Commands | .claude/commands/speckit.plan.md |
Per-integration (format, paths, placeholders differ) |
| Context file | CLAUDE.md, .github/copilot-instructions.md |
Per-integration (different file, different path) |
| Companion files | Copilot .prompt.md, .vscode/settings.json |
Per-integration (only some have these) |
| Scripts | update-context.sh / .ps1 |
Per-integration (each ships its own, sources shared common.sh) |
| Options | --commands-dir, --skills |
Per-integration (declared + parsed by each integration) |
| Shared infra | .specify/scripts/, .specify/templates/, .specify/memory/ |
Shared — owned by framework, not any integration |
.specify/agent.json — Project-Level Integration Config
Maintained by install() / uninstall() / switch. Read by shared scripts at runtime.
{
"integration": "copilot",
"version": "0.5.2",
"scripts": {
"update-context": ".specify/integrations/copilot/scripts/update-context.sh"
}
}Who writes it: integration.install() creates/updates, integration.uninstall() clears.
Who reads it: Shared shell scripts (e.g. update-agent-context.sh) read the script
paths and dispatch to the integration's own script instead of using a case statement.
Migration note: During gradual migration (Stages 2–5), the old update-agent-context.sh
with its case statement still works for --ai agents. The agent.json-based dispatch
replaces the case statement in Stage 7 once all integrations are migrated.
Integration Options (--integration-options)
Each integration declares its own set of options. The core CLI doesn't know about
agent-specific flags — it passes them through verbatim:
# Core CLI only knows --integration and --integration-options
specify init my-project --integration copilot
specify init my-project --integration generic --integration-options="--commands-dir .myagent/commands/"
specify init my-project --integration codex --integration-options="--skills"
specify init my-project --integration kimi --integration-options="--skills --migrate-legacy"How it works
Each integration declares accepted options via a class method:
class IntegrationBase(ABC):
@classmethod
def options(cls) -> list[IntegrationOption]:
"""Options this integration accepts. Default: none."""
return []
def setup(self, project_root: Path, manifest: IntegrationManifest,
parsed_options: dict[str, Any] = None, **opts) -> None:
...class GenericIntegration(MarkdownIntegration):
@classmethod
def options(cls):
return [
IntegrationOption("--commands-dir", required=True,
help="Directory for command files (e.g. .myagent/commands/)"),
]
def setup(self, project_root, manifest, parsed_options=None, **opts):
commands_dir = parsed_options["commands_dir"]
# Use commands_dir instead of hardcoded path
...class CodexIntegration(SkillsIntegration):
@classmethod
def options(cls):
return [
IntegrationOption("--skills", is_flag=True, default=True,
help="Install as agent skills (default for Codex)"),
]What this eliminates from core CLI
| Old core CLI flag | Moves to | Integration |
|---|---|---|
--ai-commands-dir |
--commands-dir |
generic |
--ai-skills |
--skills |
Skills-capable integrations (codex, kimi, agy) |
Benefits
- Core CLI stays minimal — no agent-specific flags leak into
specify init --help - Integrations control their own UX — each declares exactly what it needs
- Community integrations can add options — no core CLI changes required
- Validation is per-integration —
genericrequires--commands-dir, others don't genericbecomes a regular integration — not a special case in core CLI
Manifest Design
Stored at .specify/integrations/.manifest.json:
{
"agent": "copilot",
"version": "0.5.2",
"installed_at": "2026-03-30T12:00:00Z",
"files": {
".github/agents/speckit.specify.agent.md": "sha256:a1b2c3...",
".github/agents/speckit.plan.agent.md": "sha256:d4e5f6...",
".github/prompts/speckit.specify.prompt.md": "sha256:789abc..."
}
}- On uninstall, only files whose current hash matches the recorded hash are removed
- Modified files are skipped and reported to the user
- Empty parent directories are cleaned up
- Shared infra tracked under
_framework.manifest.json
Stage 1 — Foundation
Goal: Ship the base classes and manifest system. No behavior changes.
Deliverables
integrations/__init__.py— emptyINTEGRATION_REGISTRYdictintegrations/base.py—IntegrationBaseABC +MarkdownIntegrationbase class:IntegrationBaseABC with:- Properties:
key,config,registrar_config,context_file - Methods:
install(),uninstall(),setup(),teardown(),templates_dir() - Class method:
options()→ returns list ofIntegrationOptionthe integration accepts - Default
setup()copies templates and records in manifest setup()receivesparsed_options: dictfrom--integration-optionsparsing
- Properties:
IntegrationOptiondataclass:name,is_flag,required,default,help
MarkdownIntegration(IntegrationBase)— concrete base for standard markdown integrations:- Subclass only needs to set
key,config,registrar_config(and optionallycontext_file) - Provides
setup()that handles markdown command generation + path rewriting - ~20 integrations subclass this with config-only overrides in their own
__init__.py
- Subclass only needs to set
integrations/manifest.py—IntegrationManifestwith:record_file(rel_path, content)— write file + store sha256 hashrecord_existing(rel_path)— hash an already-written fileuninstall()→ returns(removed, skipped)- Persists to
.specify/integrations/.manifest.json
Tests
- Unit tests for manifest: write/hash/uninstall/skip-modified round-trips
What stays unchanged
- Everything. This stage is purely additive.
Stage 2 — Copilot Proof of Concept
Goal: Migrate copilot as the first integration. Validate the architecture.
Deliverables
integrations/copilot/__init__.py—CopilotIntegration(IntegrationBase)with:- Companion
.prompt.mdgeneration .vscode/settings.jsonmergecontext_file = ".github/copilot-instructions.md"
- Companion
integrations/copilot/templates/— command templates (moved fromcore_pack/agents/copilot/)integrations/copilot/scripts/— integration-specific scripts:update-context.sh— sourcescommon.sh, writes to.github/copilot-instructions.mdupdate-context.ps1— PowerShell equivalent
--integrationflag added toinit()commandinstall()writes.specify/agent.jsonwith integration key + script paths- Routing logic:
--ai copilot→ prints migration nudge, auto-promotes to new path--integration copilot→ uses new plugin path directly--ai→ old path, unchanged
_install_shared_infra()factored out:.specify/scripts/,.specify/templates/,.specify/memory/- Tracked under
_framework.manifest.json
Tests
- CopilotIntegration install/uninstall round-trip
- Verify
--ai copilotstill works (via auto-promote) - Verify modified files survive uninstall
Stage 3 — Standard Markdown Integrations
Goal: Migrate all standard markdown integrations. These subclass MarkdownIntegration
with config-only overrides — no custom logic, ~10 lines per __init__.py.
Integrations in this stage (18)
claude, qwen, opencode, junie, kilocode, auggie, roo, codebuddy, qodercli,
amp, shai, bob, trae, pi, iflow, kiro-cli, windsurf, vibe
Key mismatch to resolve
cursor-agent(AGENT_CONFIG key) ↔cursor(CommandRegistrar key). The
AGENT_CONFIG keycursor-agentis canonical (matches the CLI tool name).
The registrar will be updated tocursor-agentand the integration subpackage
will beintegrations/cursor-agent/.
NOT in this stage (verified against codebase)
copilot— Stage 2 (custom.agent.mdextension, companion.prompt.md,.vscode/settings.json)gemini,tabnine— Stage 4 (TOML format,{{args}}placeholders)codex— Stage 5 (skills format + own--skillsoption, migration logic)kimi— Stage 5 (skills format + own--skills --migrate-legacyoptions)agy— Stage 5 (commands deprecated, own--skillsoption)generic— Stage 5 (own--commands-dirrequired option, no longer special-cased in core CLI)
Directory structure (per integration)
integrations/
claude/
__init__.py # ClaudeIntegration(MarkdownIntegration) — config-only
templates/ # command templates (moved from core_pack/agents/claude/)
qwen/
__init__.py # QwenIntegration(MarkdownIntegration) — config-only
templates/
cursor-agent/
__init__.py # CursorAgentIntegration(MarkdownIntegration) — config-only
templates/
... # same pattern for all 18 standard integrations
Example: claude/__init__.py
from ..base import MarkdownIntegration
class ClaudeIntegration(MarkdownIntegration):
key = "claude"
config = {
"name": "Claude Code",
"folder": ".claude/",
"commands_subdir": "commands",
"install_url": "https://docs.anthropic.com/...",
"requires_cli": True,
}
registrar_config = {
"dir": ".claude/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = "CLAUDE.md"Deliverables
- Per integration:
- Create
integrations//subpackage - Move templates from
core_pack/agents//→integrations//templates/ - Register in
INTEGRATION_REGISTRY --aiauto-promotes with nudge--integrationuses new path
- Create
Tests
- Each migrated integration install/uninstall
- Verify
--aiauto-promote for every migrated integration
Stage 4 — TOML Integrations
Goal: Migrate integrations that render commands in TOML format instead of Markdown.
Integrations in this stage (2)
gemini— Gemini CLI (.gemini/commands/, format=toml, args={{args}}, ext=.toml)tabnine— Tabnine CLI (.tabnine/agent/commands/, format=toml, args={{args}}, ext=.toml)
What's different
These override setup() to render TOML instead of Markdown:
description = "..."as top-level keyprompt = """..."""multiline string for body- Argument placeholder:
{{args}}instead of$ARGUMENTS - Handles triple-quote escaping (falls back to
'''or escaped basic string)
Directory structure
integrations/
gemini/
__init__.py # GeminiIntegration(IntegrationBase) — TOML rendering
templates/
tabnine/
__init__.py # TabnineIntegration(IntegrationBase) — TOML rendering
templates/
Both may share a TomlIntegration base class (in base.py) if the rendering
logic is identical, or each can implement their own setup() if they diverge.
Deliverables
TomlIntegration(IntegrationBase)base class inbase.py(if warranted)integrations/gemini/andintegrations/tabnine/subpackages- Templates moved from
core_pack/agents/gemini/andcore_pack/agents/tabnine/
Tests
- TOML output format validation (valid TOML, correct placeholders)
- Install/uninstall round-trips for both
Stage 5 — Skills, Generic & Option-Driven Integrations
Goal: Migrate integrations that need their own --integration-options — skills agents,
the generic agent, and any agent with custom setup parameters.
Integrations in this stage (4)
codex— Codex CLI (.agents/skills/speckit-/SKILL.md)- Options:
--skills(flag, default=true for codex) - Has deprecation logic: commands deprecated, skills required
- Options:
kimi— Kimi Code (.kimi/skills/speckit-/SKILL.md)- Options:
--skills(flag),--migrate-legacy(flag) - Handles legacy dotted-name migration (
speckit.foo→speckit-foo)
- Options:
agy— Antigravity (.agent/commands/deprecated → skills)- Options:
--skills(flag, default=true since v1.20.5) - Commands deprecated, skills mode forced when selected interactively
- Options:
generic— Bring your own agent- Options:
--commands-dir(required, replaces old--ai-commands-dir) - No longer special-cased in core CLI — just another integration with its own option
- Options:
What's different
These integrations declare their own options via options() and receive
parsed results in setup(). The core CLI no longer needs --ai-skills
or --ai-commands-dir — those concerns are fully owned by the integrations.
Skills integrations override setup() to:
- Create skill directories (
speckit-/SKILL.mdper command) - Generate skill-specific frontmatter (name, description, compatibility, metadata)
- Resolve
{SCRIPT}placeholders based on init options - Handle deprecation logic (codex, agy default to skills=true)
- Kimi handles legacy dotted-name migration
Generic integration overrides setup() to:
- Use the user-provided
--commands-diras the output directory - Apply standard markdown rendering to that custom path
Directory structure
integrations/
codex/
__init__.py # CodexIntegration(SkillsIntegration) — skills + options
templates/
kimi/
__init__.py # KimiIntegration(SkillsIntegration) — skills + migration
templates/
agy/
__init__.py # AgyIntegration(SkillsIntegration) — deprecated commands
templates/
generic/
__init__.py # GenericIntegration(MarkdownIntegration) — --commands-dir
templates/
Example: codex/__init__.py
from ..base import SkillsIntegration, IntegrationOption
class CodexIntegration(SkillsIntegration):
key = "codex"
config = { ... }
registrar_config = { ... }
@classmethod
def options(cls):
return [
IntegrationOption("--skills", is_flag=True, default=True,
help="Install as agent skills (default for Codex)"),
]
def setup(self, project_root, manifest, parsed_options=None, **opts):
# Skills mode is the default; --no-skills would error with deprecation msg
super().setup(project_root, manifest, parsed_options=parsed_options, **opts)Example: generic/__init__.py
from ..base import MarkdownIntegration, IntegrationOption
class GenericIntegration(MarkdownIntegration):
key = "generic"
config = {
"name": "Generic (bring your own agent)",
"folder": None, # Set from --commands-dir
"commands_subdir": "commands",
"install_url": None,
"requires_cli": False,
}
@classmethod
def options(cls):
return [
IntegrationOption("--commands-dir", required=True,
help="Directory for command files"),
]
def setup(self, project_root, manifest, parsed_options=None, **opts):
commands_dir = parsed_options["commands_dir"]
# Override registrar_config dir with user-provided path
...Deliverables
SkillsIntegration(IntegrationBase)base class inbase.pyIntegrationOptiondataclass inbase.pyintegrations/codex/,integrations/kimi/,integrations/agy/,integrations/generic/- Templates moved from
core_pack/agents/{codex,kimi,agy}/ AGENT_SKILLS_MIGRATIONSdict entries absorbed into integration modules--ai-skillsand--ai-commands-dirflags deprecated from core CLI
(still accepted with warning, forwarded to--integration-optionsinternally)
Tests
- SKILL.md directory structure validation
- Skill frontmatter correctness
- Kimi legacy migration (dotted → hyphenated)
- Codex/agy default-to-skills behavior
- Generic with
--commands-diroption - Invalid/missing required options error handling
- Install/uninstall round-trips for all four
Design Note: Why Every Integration Needs Its Own Directory
- Catalog extraction: Any integration can be removed from the wheel and
distributed as a standalone catalog entry (like extensions today) - Self-contained: Templates, config, and custom logic live together
- No shared mutable state: No factory function or defaults module that
couples integrations to the core CLI - Community parity: Third-party integrations follow the exact same structure
Stage 6 — specify integrate Subcommand
Goal: Post-init integration management.
Deliverables
specify integrate list # show available + installed status
specify integrate install copilot # install into existing project
specify integrate uninstall copilot # hash-safe removal
specify integrate switch copilot claude # uninstall + installinstallwrites files + manifestuninstallchecks hashes — removes unmodified, reports modifiedswitch= uninstall old + install new, shared infra untouched
Tests
- Full lifecycle: install → modify file → uninstall → verify modified file kept
- Switch between integrations
Stage 7 — Complete Migration, Remove Old Path
Goal: Old scaffold code removed. --ai becomes alias for --integration.
Release ZIP bundles retired — the wheel is the distribution.
Target state (after Stage 7)
src/specify_cli/
__init__.py # init() uses INTEGRATION_REGISTRY only; --ai is hidden alias
agents.py # CommandRegistrar (unchanged, used by extensions/presets)
integrations/
__init__.py # INTEGRATION_REGISTRY (single source of truth)
base.py # IntegrationBase, MarkdownIntegration, TomlIntegration,
# SkillsIntegration, IntegrationOption
manifest.py # IntegrationManifest (hash-tracked install/uninstall)
copilot/
__init__.py # CopilotIntegration
templates/ # command templates
scripts/
update-context.sh # writes to .github/copilot-instructions.md
update-context.ps1
claude/
__init__.py # ClaudeIntegration(MarkdownIntegration)
templates/
scripts/
update-context.sh # writes to CLAUDE.md
update-context.ps1
cursor-agent/
__init__.py # CursorAgentIntegration(MarkdownIntegration)
templates/
scripts/
update-context.sh # writes to .cursor/rules/specify-rules.mdc (with frontmatter)
update-context.ps1
gemini/
__init__.py # GeminiIntegration(TomlIntegration)
templates/
scripts/
update-context.sh # writes to GEMINI.md
update-context.ps1
codex/
__init__.py # CodexIntegration(SkillsIntegration)
templates/
scripts/
update-context.sh # writes to AGENTS.md
update-context.ps1
generic/
__init__.py # GenericIntegration — --commands-dir option
templates/
scripts/
update-context.sh # writes to user-configured path
update-context.ps1
... # all 25+ integrations, same pattern
scripts/
bash/
common.sh # shared utility functions (plan parsing, content generation)
update-agent-context.sh # thin dispatcher: reads .specify/agent.json, runs integration script
check-prerequisites.sh # unchanged
create-new-feature.sh # unchanged
setup-plan.sh # unchanged
powershell/
common.ps1 # shared utility functions
update-agent-context.ps1 # thin dispatcher
... # unchanged
templates/ # page templates only (spec, plan, tasks, checklist, constitution)
# NO command templates — those live in integrations//templates/
What's gone (compared to current state):
REMOVED:
src/specify_cli/core_pack/agents/ # templates moved into integrations//templates/
.github/workflows/scripts/
create-release-packages.sh # no more ZIP bundles
create-github-release.sh # no more ZIP attachments
52 release ZIP artifacts # wheel contains everything
REMOVED from __init__.py:
AGENT_CONFIG dict # derived from INTEGRATION_REGISTRY
AGENT_SKILLS_MIGRATIONS dict # absorbed into integration modules
download_and_extract_template() # no more GitHub downloads
scaffold_from_core_pack() # replaced by integration.install()
--ai-skills flag # now --integration-options="--skills"
--ai-commands-dir flag # now --integration-options="--commands-dir ..."
--offline flag # always local, no network path
Project output (what specify init my-project --integration copilot creates):
my-project/
.specify/
agent.json # {"integration": "copilot", "scripts": {"update-context": "..."}}
integrations/
copilot.manifest.json # hash-tracked file list
_framework.manifest.json
copilot/
scripts/
update-context.sh # integration's own update script (installed from wheel)
update-context.ps1
scripts/
bash/
common.sh # shared utilities
update-agent-context.sh # dispatcher
...
powershell/
...
templates/ # page templates
memory/
constitution.md
.github/
agents/ # copilot command files (manifest-tracked)
prompts/ # companion .prompt.md files (manifest-tracked)
.vscode/
settings.json # copilot settings (manifest-tracked)
Deliverables
AGENT_CONFIGin__init__.py→ derived fromINTEGRATION_REGISTRYCommandRegistrar.AGENT_CONFIGS→ derived fromINTEGRATION_REGISTRYdownload_and_extract_template()/scaffold_from_core_pack()agent logic removed--aikept as hidden alias for--integration(one release cycle), then removed--ai-skillsremoved (now--integration-options="--skills")--ai-commands-dirremoved (now--integration-options="--commands-dir ..."on generic)AGENT_SKILLS_MIGRATIONSdict removed (absorbed into integration modules)--offlineflag removed (all scaffolding is now from bundled integration modules — always "offline")
Release artifact cleanup
The GitHub release ZIP bundles (52 ZIPs: 26 agents × 2 script types) are no longer needed:
| Removed | Reason |
|---|---|
create-release-packages.sh |
Integration modules ship their own templates in the wheel |
create-github-release.sh |
No more ZIPs to attach to releases |
scaffold_from_core_pack() |
Replaced by integration.install() |
download_and_extract_template() |
No more GitHub ZIP downloads |
core_pack/agents/ |
Templates moved into integrations//templates/ |
| 52 release ZIP artifacts | Wheel contains everything |
update-agent-context.sh → config-based dispatch
The old case-statement script is replaced by a thin dispatcher that reads
.specify/agent.json and runs the integration's own update-context script:
#!/usr/bin/env bash
# update-agent-context.sh — dispatches to integration's own script
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
CONFIG="$REPO_ROOT/.specify/agent.json"
UPDATE_SCRIPT=$(jq -r '.scripts["update-context"] // empty' "$CONFIG")
if [[ -z "$UPDATE_SCRIPT" ]]; then
echo "ERROR: No update-context script in .specify/agent.json" >&2
exit 1
fi
exec "$REPO_ROOT/$UPDATE_SCRIPT" "$@"Each integration ships its own scripts/update-context.sh that:
- Sources shared
common.shfor plan parsing + content generation - Writes to its own context file path (the only agent-specific part)
- Handles any format-specific needs (e.g. Cursor's
.mdcfrontmatter)
Shared (common.sh) |
Per-integration (update-context.sh) |
|---|---|
parse_plan_data() |
Target file path |
extract_plan_field() |
File format (markdown, mdc, etc.) |
format_technology_stack() |
Any agent-specific pre/post-processing |
update_agent_file() |
|
create_new_agent_file() |
Same pattern for PowerShell (update-context.ps1 + common.ps1).
Why this works: Each integration's update script is ~5 lines — source common,
call one function with its target path. Zero agent knowledge in shared code.
Community integrations ship their own script; no core changes ever.
Each integration subpackage bundles its own templates as package data.
pip install specify-cli delivers all integrations. The --offline flag
becomes meaningless because there's no network path to skip — everything
is local by default.
Tests
- All existing tests pass with derived dicts
--aialias works identically to--integration- Old flags (
--ai-skills,--ai-commands-dir) emit deprecation warnings - Scaffolding works without network access (no regression from ZIP removal)
Stage 8 — Integration Catalog
Goal: Catalog system for built-in and community integrations.
Deliverables
specify integrate list # list installed + bundled integrations
specify integrate list --catalog # browse full catalog (built-in + community)
specify integrate install acme-coder # install from catalog
specify integrate upgrade copilot # diff-aware via manifest hashesintegrations/catalog.json— built-in integrations metadataintegrations/catalog.community.json— community-contributed integrationsintegration.ymldescriptor (mirrorsextension.ymlpattern)- Version pinning, compatibility checks
- Diff-aware upgrades via manifest hash comparison
Tests
- Catalog listing includes bundled and community integrations
- Install from catalog creates correct files + manifest
- Upgrade detects version changes, handles modified files
- Invalid/missing catalog entries produce clear errors
Stage 9 — Adding New Integrations (Developer Guide)
Goal: Document the process for adding integrations — both built-in (shipped in the
wheel) and community (distributed via catalog).
Adding a built-in integration
Create a subpackage under src/specify_cli/integrations/:
integrations/
new-agent/
__init__.py # NewAgentIntegration class
templates/ # command templates
speckit.specify.md
speckit.plan.md
speckit.implement.md
...
scripts/
update-context.sh # writes to agent's native context file
update-context.ps1
1. __init__.py — the integration class
Standard markdown agent (~15 lines):
from ..base import MarkdownIntegration
class NewAgentIntegration(MarkdownIntegration):
key = "new-agent" # must match actual CLI tool name
config = {
"name": "New Agent",
"folder": ".newagent/",
"commands_subdir": "commands",
"install_url": "https://example.com/install",
"requires_cli": True,
}
registrar_config = {
"dir": ".newagent/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = "NEWAGENT.md"Agent with custom behavior (TOML, skills, companion files, etc.):
from ..base import IntegrationBase, IntegrationOption
class NewAgentIntegration(IntegrationBase):
key = "new-agent"
# ... config, registrar_config ...
@classmethod
def options(cls):
return [
IntegrationOption("--custom-flag", is_flag=True, default=False,
help="Enable custom behavior"),
]
def setup(self, project_root, manifest, parsed_options=None, **opts):
# Custom install logic
...
def teardown(self, project_root, manifest):
# Custom cleanup logic
...2. templates/ — command templates
Copy from an existing integration and adjust. Templates are standard markdown
with {SCRIPT}, $ARGUMENTS, and __AGENT__ placeholders.
3. scripts/update-context.sh
#!/usr/bin/env bash
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../scripts/bash/common.sh"
_paths_output=$(get_feature_paths) || exit 1
eval "$_paths_output"
parse_plan_data "$IMPL_PLAN"
update_agent_file "$REPO_ROOT/NEWAGENT.md" "New Agent"4. Register in INTEGRATION_REGISTRY
# integrations/__init__.py
from .new_agent import NewAgentIntegration
_register(NewAgentIntegration())5. Tests
- Install creates expected files in correct format
- Uninstall removes unmodified files, keeps modified
update-context.shwrites to correct path- Command templates render correctly
Adding a community integration
Community integrations follow the exact same structure but are distributed
as standalone packages instead of bundled in the wheel.
1. Create the integration package
my-integration/
integration.yml # descriptor (like extension.yml)
__init__.py # MyIntegration(MarkdownIntegration)
templates/
speckit.specify.md
speckit.plan.md
...
scripts/
update-context.sh
update-context.ps1
2. integration.yml — the catalog descriptor
key: my-agent
name: My Custom Agent
version: 1.0.0
author: my-org
description: Integration for My Custom Agent
homepage: https://github.com/my-org/my-agent
requires_cli: true
install_url: https://example.com/install
format: markdown
context_file: MY-AGENT.md
commands:
- name: speckit.specify
file: templates/speckit.specify.md
- name: speckit.plan
file: templates/speckit.plan.md
- name: speckit.implement
file: templates/speckit.implement.md3. Publish to the community catalog
Submit a PR to add an entry to integrations/catalog.community.json:
{
"key": "my-agent",
"name": "My Custom Agent",
"version": "1.0.0",
"author": "my-org",
"source": "https://github.com/my-org/speckit-my-agent",
"description": "Integration for My Custom Agent"
}4. Users install from catalog
specify integrate install my-agent # from catalog
specify integrate install ./path/to/my-integration # from local directory
specify integrate install https://github.com/my-org/repo # from git URLChecklist for new integrations
-
keymatches actual CLI tool name (no shorthand) -
__init__.pywith correctconfig,registrar_config,context_file -
templates/with all 9 command templates -
scripts/update-context.shand.ps1 -
integration.yml(community) or registry entry (built-in) - Tests: install, uninstall, update-context, command rendering
- README entry in Supported Integrations table
- AGENTS.md updated (if built-in)
What Stays Unchanged Throughout
CommandRegistrarinagents.py— still used by extensions and presets- Extension system — completely independent
- Preset system — completely independent
update-agent-context.sh/.ps1— case statement works as-is for--aiagents
during migration; replaced withagent.jsondispatch in Stage 7common.sh/common.ps1— shared utility functions, never agent-specific- All existing tests pass at every stage
Design Principles
- Zero disruption —
--aiworks exactly as before for non-migrated integrations - Self-documenting migration — users hitting
--ai copilotlearn about--integrationnaturally - Auto-promote —
--aidoesn't break, it nudges and proceeds via new path - One at a time — each migration is a small PR: add module, add to registry, done
- Hash-safe uninstall — only pristine files removed, modified files reported and kept
- Shared infra decoupled — switching integrations doesn't touch
.specify/scriptsor templates
Implementation Progress
- Stage 1 — Foundation (base classes, manifest, registry) — Stage 1: Integration foundation — base classes, manifest system, and registry #1925
- Stage 2 — Copilot Proof of Concept — Stage 2: Copilot integration — proof of concept with shared template primitives #2035
- Stage 3 — Standard Markdown Integrations (18 agents)
- Stage 4 — TOML Integrations (gemini, tabnine)
- Stage 5 — Skills, Generic & Option-Driven Integrations (codex, kimi, agy, generic)
- Stage 6 —
specify integrateSubcommand - Stage 7 — Complete Migration, Remove Old Path
- Stage 8 — Integration Catalog
- Stage 9 — Adding New Integrations (Developer Guide)