Skip to content

From Agents to Integrations #1924

@mnriem

Description

@mnriem

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_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.

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

  1. Core CLI stays minimal — no agent-specific flags leak into specify init --help
  2. Integrations control their own UX — each declares exactly what it needs
  3. Community integrations can add options — no core CLI changes required
  4. Validation is per-integrationgeneric requires --commands-dir, others don't
  5. generic becomes a regular integration — not a special case in core CLI

Manifest 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..."
  }
}
  • 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 — empty INTEGRATION_REGISTRY dict
  • integrations/base.pyIntegrationBase ABC + MarkdownIntegration base class:
    • IntegrationBase ABC with:
      • Properties: key, config, registrar_config, context_file
      • Methods: install(), uninstall(), setup(), teardown(), templates_dir()
      • Class method: options() → returns list of IntegrationOption the integration accepts
      • Default setup() copies templates and records in manifest
      • setup() receives parsed_options: dict from --integration-options parsing
    • IntegrationOption dataclass:
      • name, is_flag, required, default, help
    • MarkdownIntegration(IntegrationBase) — concrete base for standard markdown integrations:
      • Subclass only needs to set key, config, registrar_config (and optionally context_file)
      • Provides setup() that handles markdown command generation + path rewriting
      • ~20 integrations subclass this with config-only overrides in their own __init__.py
  • integrations/manifest.pyIntegrationManifest with:
    • record_file(rel_path, content) — write file + store sha256 hash
    • record_existing(rel_path) — hash an already-written file
    • uninstall() → returns (removed, skipped)
    • Persists to .specify/integrations/<key>.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__.pyCopilotIntegration(IntegrationBase) with:
    • Companion .prompt.md generation
    • .vscode/settings.json merge
    • context_file = ".github/copilot-instructions.md"
  • integrations/copilot/templates/ — command templates (moved from core_pack/agents/copilot/)
  • integrations/copilot/scripts/ — integration-specific scripts:
    • update-context.sh — sources common.sh, writes to .github/copilot-instructions.md
    • update-context.ps1 — PowerShell equivalent
  • --integration flag added to init() command
  • install() writes .specify/agent.json with integration key + script paths
  • Routing logic:
    • --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/
    • Tracked under _framework.manifest.json

Tests

  • CopilotIntegration install/uninstall round-trip
  • 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.

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 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/.

NOT in this stage (verified against codebase)

  • copilot — Stage 2 (custom .agent.md extension, companion .prompt.md, .vscode/settings.json)
  • gemini, tabnine — Stage 4 (TOML format, {{args}} placeholders)
  • codex — Stage 5 (skills format + own --skills option, migration logic)
  • kimi — Stage 5 (skills format + own --skills --migrate-legacy options)
  • agy — Stage 5 (commands deprecated, own --skills option)
  • generic — Stage 5 (own --commands-dir required 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:
    1. Create integrations/<key>/ subpackage
    2. Move templates from core_pack/agents/<key>/integrations/<key>/templates/
    3. Register in INTEGRATION_REGISTRY
    4. --ai <key> auto-promotes with nudge
    5. --integration <key> uses new path

Tests

  • Each migrated integration install/uninstall
  • Verify --ai <key> auto-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 key
  • prompt = """...""" 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 in base.py (if warranted)
  • integrations/gemini/ and integrations/tabnine/ subpackages
  • Templates moved from core_pack/agents/gemini/ and core_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-<name>/SKILL.md)
    • Options: --skills (flag, default=true for codex)
    • Has deprecation logic: commands deprecated, skills required
  • kimi — Kimi Code (.kimi/skills/speckit-<name>/SKILL.md)
    • Options: --skills (flag), --migrate-legacy (flag)
    • Handles legacy dotted-name migration (speckit.foospeckit-foo)
  • agy — Antigravity (.agent/commands/ deprecated → skills)
    • 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)
  • 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-dir as 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 in base.py
  • IntegrationOption dataclass in base.py
  • integrations/codex/, integrations/kimi/, integrations/agy/, integrations/generic/
  • Templates moved from core_pack/agents/{codex,kimi,agy}/
  • AGENT_SKILLS_MIGRATIONS dict entries absorbed into integration modules
  • --ai-skills and --ai-commands-dir flags deprecated from core CLI
    (still accepted with warning, forwarded to --integration-options internally)

Tests

  • SKILL.md directory structure validation
  • Skill frontmatter correctness
  • Kimi legacy migration (dotted → hyphenated)
  • Codex/agy default-to-skills behavior
  • Generic with --commands-dir option
  • Invalid/missing required options error handling
  • Install/uninstall round-trips for all four

Design Note: Why Every Integration Needs Its Own Directory

  1. Catalog extraction: Any integration can be removed from the wheel and
    distributed as a standalone catalog entry (like extensions today)
  2. Self-contained: Templates, config, and custom logic live together
  3. No shared mutable state: No factory function or defaults module that
    couples integrations to the core CLI
  4. 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 + install
  • install writes files + manifest
  • uninstall checks hashes — removes unmodified, reports modified
  • switch = 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/<key>/templates/

What's gone (compared to current state):

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

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_CONFIG in __init__.py → derived from INTEGRATION_REGISTRY
  • CommandRegistrar.AGENT_CONFIGS → derived from INTEGRATION_REGISTRY
  • download_and_extract_template() / scaffold_from_core_pack() agent logic removed
  • --ai kept as hidden alias for --integration (one release cycle), then removed
  • --ai-skills removed (now --integration-options="--skills")
  • --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" ]]; 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.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)

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 hashes
  • integrations/catalog.json — built-in integrations metadata
  • integrations/catalog.community.json — community-contributed integrations
  • integration.yml descriptor (mirrors extension.yml pattern)
  • 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.sh writes 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.md

3. 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 URL

Checklist for new integrations

  • key matches actual CLI tool name (no shorthand)
  • __init__.py with correct config, registrar_config, context_file
  • templates/ with all 9 command templates
  • scripts/update-context.sh and .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

  • CommandRegistrar in agents.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 --ai agents
    during migration; replaced with agent.json dispatch in Stage 7
  • common.sh / common.ps1 — shared utility functions, never agent-specific
  • All existing tests pass at every stage

Design Principles

  1. Zero disruption--ai works exactly as before for non-migrated integrations
  2. Self-documenting migration — users hitting --ai copilot learn about --integration naturally
  3. Auto-promote--ai <migrated> doesn't break, it nudges and proceeds via new path
  4. One at a time — each migration is a small PR: add module, add to registry, done
  5. Hash-safe uninstall — only pristine files removed, modified files reported and kept
  6. Shared infra decoupled — switching integrations doesn't touch .specify/scripts or templates

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions