The Integration Hierarchy: One Codebase, 25+ AI Agents
Prerequisites
- ›Article 1: Architecture and Project Navigation
- ›Article 2: The Init Command Deep Dive
- ›Familiarity with OOP design patterns (Template Method, Strategy)
- ›Understanding of YAML frontmatter
The Integration Hierarchy: One Codebase, 25+ AI Agents
When specify init --integration windsurf runs, it produces Markdown files in .windsurf/workflows/. When you use --integration gemini, it produces TOML files in .gemini/commands/. For --integration claude, it creates skill directories under .claude/skills/speckit-*/SKILL.md with custom frontmatter. All three start from the same 9 command templates. The integration class hierarchy is how Spec Kit pulls this off — and it's the most architecturally interesting part of the codebase.
The 4-Tier Class Hierarchy
The integration system is built on classical OOP with the Template Method pattern at its core. Four base classes provide increasingly specific behavior:
classDiagram
class IntegrationBase {
<<abstract>>
+key: str
+config: dict
+registrar_config: dict
+context_file: str
+setup()*
+teardown()
+process_template()
+copy_command_to_directory()
+record_file_in_manifest()
+write_file_and_record()
+install_scripts()
}
class MarkdownIntegration {
+setup()
"~20 agents: Windsurf, Cursor, Roo, Amp..."
}
class TomlIntegration {
+setup()
+command_filename()
-_extract_description()
-_render_toml()
"2 agents: Gemini, Tabnine"
}
class SkillsIntegration {
+setup()
+skills_dest()
"3+ agents: Claude, Codex, Kimi"
}
class CopilotIntegration {
+setup()
+command_filename()
"1 agent: fully custom"
}
IntegrationBase <|-- MarkdownIntegration
IntegrationBase <|-- TomlIntegration
IntegrationBase <|-- SkillsIntegration
IntegrationBase <|-- CopilotIntegration
The hierarchy is defined in integrations/base.py. IntegrationBase provides the granular primitives — file operations, template processing, manifest tracking. The three concrete bases (MarkdownIntegration, TomlIntegration, SkillsIntegration) each implement setup() with format-specific logic. And CopilotIntegration extends IntegrationBase directly for its unique requirements.
The design philosophy: most integrations should require zero method overrides. Set three class attributes, inherit everything else.
IntegrationBase: The Granular Primitives
The abstract base class at base.py#L54-L67 defines four required class attributes:
key— unique identifier matching the CLI tool name (e.g.,"windsurf","kiro-cli")config— metadata dict withname,folder,commands_subdir,install_url,requires_cliregistrar_config— format specification withdir,format,args,extensioncontext_file— optional path to the agent's context/instructions file
The building-block methods in base.py#L164-L362 are all static or instance methods that subclasses compose in their setup():
sequenceDiagram
participant Setup as setup()
participant List as list_command_templates()
participant Proc as process_template()
participant Write as write_file_and_record()
participant Scripts as install_scripts()
Setup->>List: Get sorted .md files from shared commands dir
loop For each template
Setup->>Proc: Transform raw markdown → agent-ready content
Setup->>Write: Write to dest dir + record SHA-256 hash
end
Setup->>Scripts: Copy integration-specific scripts
The write_file_and_record() method normalizes line endings to \n before writing and immediately records the SHA-256 hash in the manifest. This ensures that the manifest always reflects the exact bytes on disk, which is critical for the safe uninstall behavior we'll cover later.
Tip: The
shared_commands_dir()method uses the same dual-path resolution pattern from Part 1 — checkingcore_pack/commands/first (wheel install), thentemplates/commands/(source checkout). Every base class method that touches assets follows this convention.
The 7-Step process_template() Pipeline
The process_template() static method is the heart of the format conversion. It transforms a raw command template into agent-ready content through seven sequential steps:
flowchart TD
S1["1. Extract scripts.{type} from YAML frontmatter"] --> S2["2. Replace {SCRIPT} with extracted command"]
S2 --> S3["3. Extract agent_scripts.{type}, replace {AGENT_SCRIPT}"]
S3 --> S4["4. Strip scripts: and agent_scripts: sections from frontmatter"]
S4 --> S5["5. Replace {ARGS} with agent-specific placeholder"]
S5 --> S6["6. Replace __AGENT__ with agent name"]
S6 --> S7["7. Rewrite paths: scripts/ → .specify/scripts/"]
Let's trace what happens to the plan.md template when processed for Windsurf with sh scripts:
Input frontmatter contains both script variants:
scripts:
sh: scripts/bash/setup-plan.sh --json
ps: scripts/powershell/setup-plan.ps1 -Json
agent_scripts:
sh: scripts/bash/update-agent-context.sh __AGENT__
ps: scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__
After step 1-2: {SCRIPT} → scripts/bash/setup-plan.sh --json
After step 3: {AGENT_SCRIPT} → scripts/bash/update-agent-context.sh __AGENT__
After step 4: The scripts: and agent_scripts: YAML blocks are removed from frontmatter
After step 5: {ARGS} → $ARGUMENTS (Windsurf's placeholder)
After step 6: __AGENT__ → windsurf
After step 7: scripts/bash/setup-plan.sh → .specify/scripts/bash/setup-plan.sh
The path rewriting in step 7 delegates to CommandRegistrar.rewrite_project_relative_paths(), which handles the transition from repository-relative paths (used in templates) to project-relative paths (used in scaffolded output).
Case Study: Windsurf — The 4-Attribute Integration
Windsurf demonstrates the hierarchy's power. The entire integration is 22 lines — integrations/windsurf/__init__.py:
class WindsurfIntegration(MarkdownIntegration):
key = "windsurf"
config = {
"name": "Windsurf",
"folder": ".windsurf/",
"commands_subdir": "workflows",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".windsurf/workflows",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = ".windsurf/rules/specify-rules.md"
Zero method overrides. The MarkdownIntegration.setup() at base.py#L455-L508 handles everything: iterates templates, calls process_template(), writes processed files, installs scripts. About 20 of the 27 integrations follow this exact pattern — differing only in their class attributes.
Case Study: Copilot — Fully Custom Integration
Copilot is the exception that proves the rule. It extends IntegrationBase directly because it has three requirements no base class handles — integrations/copilot/__init__.py#L24-L38:
-
.agent.mdextension instead of plain.md:def command_filename(self, template_name: str) -> str: return f"speckit.{template_name}.agent.md" -
Companion
.prompt.mdfiles in.github/prompts/— one per command, each containing a YAML header that references the agent command:prompt_content = f"---\nagent: {cmd_name}\n---\n" -
VS Code
settings.jsonmerge — the integration ships a template settings file and merges it into any existing settings without overwriting user preferences. The merge atcopilot/__init__.py#L134-L186handles edge cases like JSONC files with comments (which can't be parsed byjson.loads, so the merge is skipped with a warning).
flowchart LR
A["Command templates"] --> B[".github/agents/<br/>speckit.plan.agent.md"]
A --> C[".github/prompts/<br/>speckit.plan.prompt.md"]
A --> D[".vscode/settings.json<br/>(merged)"]
A --> E[".specify/integrations/<br/>copilot/scripts/"]
Despite the custom setup(), Copilot still uses the same base class primitives: process_template(), write_file_and_record(), record_file_in_manifest(), and install_scripts(). The hierarchy provides escape hatches without forcing you to rewrite everything.
Case Study: Claude and Gemini — Skills and TOML
Claude uses SkillsIntegration, which creates a directory-per-command structure following the agentskills.io spec. But Claude adds post-processing — integrations/claude/__init__.py:
After the base SkillsIntegration.setup() creates the skill files, Claude's override iterates the created files and injects three pieces of frontmatter:
| Frontmatter key | Value | Purpose |
|---|---|---|
user-invocable |
true |
Makes the skill accessible via /command in Claude Code |
disable-model-invocation |
true |
Prevents the model from auto-triggering the skill |
argument-hint |
"Describe the feature..." |
Shows inline hint text when user invokes the command |
The argument-hint values come from the ARGUMENT_HINTS dict, mapping command stems to human-friendly prompts.
Gemini uses TomlIntegration at base.py#L515-L684, which runs the same process_template() pipeline but then converts the result to TOML. The conversion extracts the description from frontmatter (using yaml.safe_load to handle block scalars), strips the frontmatter, and renders a TOML file with description and prompt keys. The TOML rendering handles edge cases like bodies containing triple-quote delimiters by falling back to literal strings or escaped basic strings.
IntegrationManifest: Hash-Tracked Safe Uninstall
Every file created during setup is tracked by IntegrationManifest — a JSON file at .specify/integrations/<key>.manifest.json containing:
{
"integration": "claude",
"version": "0.6.2.dev0",
"installed_at": "2026-04-12T10:30:00+00:00",
"files": {
".claude/skills/speckit-plan/SKILL.md": "a1b2c3d4e5f6...",
".claude/skills/speckit-specify/SKILL.md": "f6e5d4c3b2a1..."
}
}
Each value is the SHA-256 hex digest of the file's content at install time. On uninstall, manifest.uninstall() only removes files whose current hash still matches the recorded hash. Modified files are skipped and reported. This prevents accidental deletion of user customizations.
flowchart TD
A["uninstall() called"] --> B["For each tracked file"]
B --> C{"File exists?"}
C -->|No| D["Skip (already gone)"]
C -->|Yes| E{"SHA-256 matches?"}
E -->|Yes| F["Delete file + clean empty parents"]
E -->|No| G{"--force flag?"}
G -->|Yes| F
G -->|No| H["Skip (user modified)"]
F --> I["Remove manifest JSON"]
Tip: The manifest also cleans up empty parent directories after deleting files. This prevents leaving behind a tree of empty
.claude/skills/speckit-plan/directories after uninstall.
Registration and Adding New Integrations
The _register_builtins() function in integrations/__init__.py follows a strict convention: imports are alphabetical, registrations are alphabetical. Each integration subpackage is a Python-safe directory name — hyphens in keys become underscores in directory names (kiro-cli → kiro_cli/), while the key attribute retains the hyphenated form.
Adding a new integration is a three-step process:
- Create the subpackage —
src/specify_cli/integrations/myagent/__init__.pywith a class inheriting fromMarkdownIntegration(most common case) - Register in
_register_builtins()— add the import and_register(MyAgentIntegration())call - Add a test —
tests/integrations/test_integration_myagent.pyverifying registration, config, and placeholder replacement
For most agents, the entire implementation is under 25 lines. The class hierarchy does the heavy lifting.
What's Next
The integrations produce agent-specific command files, but what's inside those files? In Part 4, we'll examine the 9 slash command templates that form Spec Kit's declarative workflow engine — how YAML frontmatter creates a workflow DAG, how shell scripts provide the operational layer, and how document templates constrain AI output quality through structured prompts.