You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 integration install
Typer subcommand group
Noun form for subcommand (matches specify extension, specify preset)
Integration
What we call Copilot/Claude/Gemini etc.
Not "agent", not "AI"
Current State
AGENT_CONFIG in __init__.py — metadata dict (name, folder, install_url, requires_cli) for 25 agents
CommandRegistrar.AGENT_CONFIGS in agents.py — registration dict (dir, format, args, extension), must stay in sync
core_pack/agents/<agent>/ — 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.
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 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:
Verify --ai copilot still 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.
cursor-agent (AGENT_CONFIG key) ↔ cursor (CommandRegistrar key). The
AGENT_CONFIG key cursor-agent is canonical (matches the CLI tool name).
The registrar will be updated to cursor-agent and the integration subpackage
will be integrations/cursor-agent/.
Options: --skills (flag, default=true since v1.20.5)
Commands deprecated, skills mode forced when selected interactively
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
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-<name>/SKILL.md per command)
REMOVED:
src/specify_cli/core_pack/agents/ # templates moved into integrations/<key>/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
--ai-commands-dir removed (now --integration-options="--commands-dir ..." on generic)
AGENT_SKILLS_MIGRATIONS dict removed (absorbed into integration modules)
--offline flag 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/<key>/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" ]];thenecho"ERROR: No update-context script in .specify/agent.json">&2exit 1
fiexec"$REPO_ROOT/$UPDATE_SCRIPT""$@"
Each integration ships its own scripts/update-context.sh that:
Sources shared common.sh for 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 .mdc frontmatter)
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
--ai alias works identically to --integration
Old flags (--ai-skills, --ai-commands-dir) emit deprecation warnings
Scaffolding works without network access (no regression from ZIP removal)
What Stays Unchanged Throughout
CommandRegistrar in agents.py — still used by extensions and presets
Extension system — completely independent
Preset system — completely independent
common.sh / common.ps1 — shared utility functions, never agent-specific
All existing tests pass at every stage
Design Principles
Zero disruption — --ai works exactly as before for non-migrated integrations
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
--integration copilotintegrations/packagespecify integration installspecify extension,specify preset)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/<agent>/— bundled template files per agentTarget State
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
.claude/commands/speckit.plan.mdCLAUDE.md,.github/copilot-instructions.md.prompt.md,.vscode/settings.jsonupdate-context.sh/.ps1common.sh)--commands-dir,--skills.specify/scripts/,.specify/templates/,.specify/memory/.specify/agent.json— Project-Level Integration ConfigMaintained 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 scriptpaths 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.shwith its case statement still works for
--aiagents. Theagent.json-based dispatchreplaces the case statement 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:
How it works
Each integration declares accepted options via a class method:
What this eliminates from core CLI
--ai-commands-dir--commands-dirgeneric--ai-skills--skillsBenefits
specify init --helpgenericrequires--commands-dir, others don'tgenericbecomes a regular integration — not a special case in core CLIManifest Design
Stored at
.specify/integrations/<key>.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..." } }_framework.manifest.jsonStage 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:key,config,registrar_config,context_fileinstall(),uninstall(),setup(),teardown(),templates_dir()options()→ returns list ofIntegrationOptionthe integration acceptssetup()copies templates and records in manifestsetup()receivesparsed_options: dictfrom--integration-optionsparsingIntegrationOptiondataclass:name,is_flag,required,default,helpMarkdownIntegration(IntegrationBase)— concrete base for standard markdown integrations:key,config,registrar_config(and optionallycontext_file)setup()that handles markdown command generation + path rewriting__init__.pyintegrations/manifest.py—IntegrationManifestwith:record_file(rel_path, content)— write file + store sha256 hashrecord_existing(rel_path)— hash an already-written fileuninstall()→ returns(removed, skipped).specify/integrations/<key>.manifest.jsonTests
What stays unchanged
Stage 2 — Copilot Proof of Concept
Goal: Migrate copilot as the first integration. Validate the architecture.
Deliverables
integrations/copilot/__init__.py—CopilotIntegration(IntegrationBase)with:.prompt.mdgeneration.vscode/settings.jsonmergecontext_file = ".github/copilot-instructions.md"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--ai copilot→ prints migration nudge, auto-promotes to new path--integration copilot→ uses new plugin path directly--ai <anything-else>→ old path, unchanged_install_shared_infra()factored out:.specify/scripts/,.specify/templates/,.specify/memory/_framework.manifest.jsonTests
--ai copilotstill works (via auto-promote)Stage 3 — Standard Markdown Integrations
Goal: Migrate all standard markdown integrations. These subclass
MarkdownIntegrationwith 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). TheAGENT_CONFIG key
cursor-agentis canonical (matches the CLI tool name).The registrar will be updated to
cursor-agentand the integration subpackagewill be
integrations/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)
Example:
claude/__init__.pyDeliverables
integrations/<key>/subpackagecore_pack/agents/<key>/→integrations/<key>/templates/INTEGRATION_REGISTRY--ai <key>auto-promotes with nudge--integration <key>uses new pathTests
--ai <key>auto-promote for every migrated integrationStage 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{{args}}instead of$ARGUMENTS'''or escaped basic string)Directory structure
Both may share a
TomlIntegrationbase class (inbase.py) if the renderinglogic 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/subpackagescore_pack/agents/gemini/andcore_pack/agents/tabnine/Tests
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-<name>/SKILL.md)--skills(flag, default=true for codex)kimi— Kimi Code (.kimi/skills/speckit-<name>/SKILL.md)--skills(flag),--migrate-legacy(flag)speckit.foo→speckit-foo)agy— Antigravity (.agent/commands/deprecated → skills)--skills(flag, default=true since v1.20.5)generic— Bring your own agent--commands-dir(required, replaces old--ai-commands-dir)What's different
These integrations declare their own options via
options()and receiveparsed results in
setup(). The core CLI no longer needs--ai-skillsor
--ai-commands-dir— those concerns are fully owned by the integrations.Skills integrations override
setup()to:speckit-<name>/SKILL.mdper command){SCRIPT}placeholders based on init optionsGeneric integration overrides
setup()to:--commands-diras the output directoryDirectory structure
Example:
codex/__init__.pyExample:
generic/__init__.pyDeliverables
SkillsIntegration(IntegrationBase)base class inbase.pyIntegrationOptiondataclass inbase.pyintegrations/codex/,integrations/kimi/,integrations/agy/,integrations/generic/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
--commands-diroptionDesign Note: Why Every Integration Needs Its Own Directory
distributed as a standalone catalog entry (like extensions today)
couples integrations to the core CLI
Stage 6 — Complete Migration, Remove Old Path
Goal: Old scaffold code removed.
--aibecomes alias for--integration.Release ZIP bundles retired — the wheel is the distribution.
Target state (after Stage 6)
What's gone (compared to current state):
Project output (what
specify init my-project --integration copilotcreates):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:
create-release-packages.shcreate-github-release.shscaffold_from_core_pack()integration.install()download_and_extract_template()core_pack/agents/integrations/<key>/templates/update-agent-context.sh→ config-based dispatchThe old case-statement script is replaced by a thin dispatcher that reads
.specify/agent.jsonand runs the integration's own update-context script:Each integration ships its own
scripts/update-context.shthat:common.shfor plan parsing + content generation.mdcfrontmatter)common.sh)update-context.sh)parse_plan_data()extract_plan_field()format_technology_stack()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-clidelivers all integrations. The--offlineflagbecomes meaningless because there's no network path to skip — everything
is local by default.
Tests
--aialias works identically to--integration--ai-skills,--ai-commands-dir) emit deprecation warningsWhat Stays Unchanged Throughout
CommandRegistrarinagents.py— still used by extensions and presetscommon.sh/common.ps1— shared utility functions, never agent-specificDesign Principles
--aiworks exactly as before for non-migrated integrations--ai copilotlearn about--integrationnaturally--ai <migrated>doesn't break, it nudges and proceeds via new path.specify/scriptsor templatesImplementation Progress
Follow-up Issues
specify integrationsubcommand for post-init integration management #2065 —specify integrationsubcommand (post-init integration management)