# Extension Development Guide A guide for creating Spec Kit extensions. --- ## Quick Start ### 1. Create Extension Directory ```bash mkdir my-extension cd my-extension ``` ### 2. Create `extension.yml` Manifest ```yaml schema_version: "1.0" extension: id: "my-ext" # Lowercase, alphanumeric + hyphens only name: "My Extension" version: "1.0.0" # Semantic versioning description: "My custom extension" author: "Your Name" repository: "https://github.com/you/spec-kit-my-ext" license: "MIT" requires: speckit_version: ">=0.1.0" # Minimum spec-kit version tools: # Optional: External tools required - name: "my-tool" required: true version: ">=1.0.0" commands: # Optional: Core commands needed - "speckit.tasks" provides: commands: - name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd} file: "commands/hello.md" description: "Say hello" aliases: ["speckit.hello"] # Optional aliases config: # Optional: Config files - name: "my-ext-config.yml" template: "my-ext-config.template.yml" description: "Extension configuration" required: false hooks: # Optional: Integration hooks after_tasks: command: "speckit.my-ext.hello" optional: true prompt: "Run hello command?" tags: # Optional: For catalog search - "example" - "utility" ``` ### 3. Create Commands Directory ```bash mkdir commands ``` ### 4. Create Command File **File**: `commands/hello.md` ```markdown --- description: "Say hello command" tools: # Optional: AI tools this command uses - 'some-tool/function' scripts: # Optional: Helper scripts sh: ../../scripts/bash/helper.sh ps: ../../scripts/powershell/helper.ps1 --- # Hello Command This command says hello! ## User Input $ARGUMENTS ## Steps 1. Greet the user 2. Show extension is working ```bash echo "Hello from my extension!" echo "Arguments: $ARGUMENTS" ``` ## Extension Configuration Load extension config from `.specify/extensions/my-ext/my-ext-config.yml`. ### 5. Test Locally ```bash cd /path/to/spec-kit-project specify extension add --dev /path/to/my-extension ``` ### 6. Verify Installation ```bash specify extension list # Should show: # ✓ My Extension (v1.0.0) # My custom extension # Commands: 1 | Hooks: 1 | Status: Enabled ``` ### 7. Test Command If using Claude: ```bash claude > /speckit.my-ext.hello world ``` The command will be available in `.claude/commands/speckit.my-ext.hello.md`. --- ## Manifest Schema Reference ### Required Fields #### `schema_version` Extension manifest schema version. Currently: `"1.0"` #### `extension` Extension metadata block. **Required sub-fields**: - `id`: Extension identifier (lowercase, alphanumeric, hyphens) - `name`: Human-readable name - `version`: Semantic version (e.g., "1.0.0") - `description`: Short description **Optional sub-fields**: - `author`: Extension author - `repository`: Source code URL - `license`: SPDX license identifier - `homepage`: Extension homepage URL #### `requires` Compatibility requirements. **Required sub-fields**: - `speckit_version`: Semantic version specifier (e.g., ">=0.1.0,<2.0.0") **Optional sub-fields**: - `tools`: External tools required (array of tool objects) - `commands`: Core spec-kit commands needed (array of command names) - `scripts`: Core scripts required (array of script names) #### `provides` What the extension provides. **Required sub-fields**: - `commands`: Array of command objects (must have at least one) **Command object**: - `name`: Command name (must match `speckit.{ext-id}.{command}`) - `file`: Path to command file (relative to extension root) - `description`: Command description (optional) - `aliases`: Alternative command names (optional, array) ### Optional Fields #### `hooks` Integration hooks for automatic execution. Available hook points: - `after_tasks`: After `/speckit.tasks` completes - `after_implement`: After `/speckit.implement` completes (future) Hook object: - `command`: Command to execute (must be in `provides.commands`) - `optional`: If true, prompt user before executing - `prompt`: Prompt text for optional hooks - `description`: Hook description - `condition`: Execution condition (future) #### `tags` Array of tags for catalog discovery. #### `defaults` Default extension configuration values. #### `config_schema` JSON Schema for validating extension configuration. --- ## Command File Format ### Frontmatter (YAML) ```yaml --- description: "Command description" # Required tools: # Optional - 'tool-name/function' scripts: # Optional sh: ../../scripts/bash/helper.sh ps: ../../scripts/powershell/helper.ps1 --- ``` ### Body (Markdown) Use standard Markdown with special placeholders: - `$ARGUMENTS`: User-provided arguments - `{SCRIPT}`: Replaced with script path during registration **Example**: ````markdown ## Steps 1. Parse arguments 2. Execute logic ```bash args="$ARGUMENTS" echo "Running with args: $args" ``` ```` ### Script Path Rewriting Extension commands use relative paths that get rewritten during registration: **In extension**: ```yaml scripts: sh: ../../scripts/bash/helper.sh ``` **After registration**: ```yaml scripts: sh: .specify/scripts/bash/helper.sh ``` This allows scripts to reference core spec-kit scripts. --- ## Configuration Files ### Config Template **File**: `my-ext-config.template.yml` ```yaml # My Extension Configuration # Copy this to my-ext-config.yml and customize # Example configuration api: endpoint: "https://api.example.com" timeout: 30 features: feature_a: true feature_b: false credentials: # DO NOT commit credentials! # Use environment variables instead api_key: "${MY_EXT_API_KEY}" ``` ### Config Loading In your command, load config with layered precedence: 1. Extension defaults (`extension.yml` → `defaults`) 2. Project config (`.specify/extensions/my-ext/my-ext-config.yml`) 3. Local overrides (`.specify/extensions/my-ext/my-ext-config.local.yml` - gitignored) 4. Environment variables (`SPECKIT_MY_EXT_*`) **Example loading script**: ```bash #!/usr/bin/env bash EXT_DIR=".specify/extensions/my-ext" # Load and merge config config=$(yq eval '.' "$EXT_DIR/my-ext-config.yml" -o=json) # Apply env overrides if [ -n "${SPECKIT_MY_EXT_API_KEY:-}" ]; then config=$(echo "$config" | jq ".api.api_key = \"$SPECKIT_MY_EXT_API_KEY\"") fi echo "$config" ``` --- ## Excluding Files with `.extensionignore` Extension authors can create a `.extensionignore` file in the extension root to exclude files and folders from being copied when a user installs the extension with `specify extension add`. This is useful for keeping development-only files (tests, CI configs, docs source, etc.) out of the installed copy. ### Format The file uses `.gitignore`-compatible patterns (one per line), powered by the [`pathspec`](https://pypi.org/project/pathspec/) library: - Blank lines are ignored - Lines starting with `#` are comments - `*` matches anything **except** `/` (does not cross directory boundaries) - `**` matches zero or more directories (e.g., `docs/**/*.draft.md`) - `?` matches any single character except `/` - A trailing `/` restricts a pattern to directories only - Patterns containing `/` (other than a trailing slash) are anchored to the extension root - Patterns without `/` match at any depth in the tree - `!` negates a previously excluded pattern (re-includes a file) - Backslashes in patterns are normalised to forward slashes for cross-platform compatibility - The `.extensionignore` file itself is always excluded automatically ### Example ```gitignore # .extensionignore # Development files tests/ .github/ .gitignore # Build artifacts __pycache__/ *.pyc dist/ # Documentation source (keep only the built README) docs/ CONTRIBUTING.md ``` ### Pattern Matching | Pattern | Matches | Does NOT match | |---------|---------|----------------| | `*.pyc` | Any `.pyc` file in any directory | — | | `tests/` | The `tests` directory (and all its contents) | A file named `tests` | | `docs/*.draft.md` | `docs/api.draft.md` (directly inside `docs/`) | `docs/sub/api.draft.md` (nested) | | `.env` | The `.env` file at any level | — | | `!README.md` | Re-includes `README.md` even if matched by an earlier pattern | — | | `docs/**/*.draft.md` | `docs/api.draft.md`, `docs/sub/api.draft.md` | — | ### Unsupported Features The following `.gitignore` features are **not applicable** in this context: - **Multiple `.extensionignore` files**: Only a single file at the extension root is supported (`.gitignore` supports files in subdirectories) - **`$GIT_DIR/info/exclude` and `core.excludesFile`**: These are Git-specific and have no equivalent here - **Negation inside excluded directories**: Because file copying uses `shutil.copytree`, excluding a directory prevents recursion into it entirely. A negation pattern cannot re-include a file inside a directory that was itself excluded. For example, the combination `tests/` followed by `!tests/important.py` will **not** preserve `tests/important.py` — the `tests/` directory is skipped at the root level and its contents are never evaluated. To work around this, exclude the directory's contents individually instead of the directory itself (e.g., `tests/*.pyc` and `tests/.cache/` rather than `tests/`). --- ## Validation Rules ### Extension ID - **Pattern**: `^[a-z0-9-]+$` - **Valid**: `my-ext`, `tool-123`, `awesome-plugin` - **Invalid**: `MyExt` (uppercase), `my_ext` (underscore), `my ext` (space) ### Extension Version - **Format**: Semantic versioning (MAJOR.MINOR.PATCH) - **Valid**: `1.0.0`, `0.1.0`, `2.5.3` - **Invalid**: `1.0`, `v1.0.0`, `1.0.0-beta` ### Command Name - **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$` - **Valid**: `speckit.my-ext.hello`, `speckit.tool.cmd` - **Invalid**: `my-ext.hello` (missing prefix), `speckit.hello` (no extension namespace) ### Command File Path - **Must be** relative to extension root - **Valid**: `commands/hello.md`, `commands/subdir/cmd.md` - **Invalid**: `/absolute/path.md`, `../outside.md` --- ## Testing Extensions ### Manual Testing 1. **Create test extension** 2. **Install locally**: ```bash specify extension add --dev /path/to/extension ``` 3. **Verify installation**: ```bash specify extension list ``` 4. **Test commands** with your AI agent 5. **Check command registration**: ```bash ls .claude/commands/speckit.my-ext.* ``` 6. **Remove extension**: ```bash specify extension remove my-ext ``` ### Automated Testing Create tests for your extension: ```python # tests/test_my_extension.py import pytest from pathlib import Path from specify_cli.extensions import ExtensionManifest def test_manifest_valid(): """Test extension manifest is valid.""" manifest = ExtensionManifest(Path("extension.yml")) assert manifest.id == "my-ext" assert len(manifest.commands) >= 1 def test_command_files_exist(): """Test all command files exist.""" manifest = ExtensionManifest(Path("extension.yml")) for cmd in manifest.commands: cmd_file = Path(cmd["file"]) assert cmd_file.exists(), f"Command file not found: {cmd_file}" ``` --- ## Distribution ### Option 1: GitHub Repository 1. **Create repository**: `spec-kit-my-ext` 2. **Add files**: ```text spec-kit-my-ext/ ├── extension.yml ├── commands/ ├── scripts/ ├── docs/ ├── README.md ├── LICENSE └── CHANGELOG.md ``` 3. **Create release**: Tag with version (e.g., `v1.0.0`) 4. **Install from repo**: ```bash git clone https://github.com/you/spec-kit-my-ext specify extension add --dev spec-kit-my-ext/ ``` ### Option 2: ZIP Archive (Future) Create ZIP archive and host on GitHub Releases: ```bash zip -r spec-kit-my-ext-1.0.0.zip extension.yml commands/ scripts/ docs/ ``` Users install with: ```bash specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip ``` ### Option 3: Community Reference Catalog Submit to the community catalog for public discovery: 1. **Fork** spec-kit repository 2. **Add entry** to `extensions/catalog.community.json` 3. **Update** `extensions/README.md` with your extension 4. **Create PR** following the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) 5. **After merge**, your extension becomes available: - Users can browse `catalog.community.json` to discover your extension - Users copy the entry to their own `catalog.json` - Users install with: `specify extension add my-ext` (from their catalog) See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed submission instructions. --- ## Best Practices ### Naming Conventions - **Extension ID**: Use descriptive, hyphenated names (`jira-integration`, not `ji`) - **Commands**: Use verb-noun pattern (`create-issue`, `sync-status`) - **Config files**: Match extension ID (`jira-config.yml`) ### Documentation - **README.md**: Overview, installation, usage - **CHANGELOG.md**: Version history - **docs/**: Detailed guides - **Command descriptions**: Clear, concise ### Versioning - **Follow SemVer**: `MAJOR.MINOR.PATCH` - **MAJOR**: Breaking changes - **MINOR**: New features - **PATCH**: Bug fixes ### Security - **Never commit secrets**: Use environment variables - **Validate input**: Sanitize user arguments - **Document permissions**: What files/APIs are accessed ### Compatibility - **Specify version range**: Don't require exact version - **Test with multiple versions**: Ensure compatibility - **Graceful degradation**: Handle missing features --- ## Example Extensions ### Minimal Extension Smallest possible extension: ```yaml # extension.yml schema_version: "1.0" extension: id: "minimal" name: "Minimal Extension" version: "1.0.0" description: "Minimal example" requires: speckit_version: ">=0.1.0" provides: commands: - name: "speckit.minimal.hello" file: "commands/hello.md" ``` ````markdown --- description: "Hello command" --- # Hello World ```bash echo "Hello, $ARGUMENTS!" ``` ```` ### Extension with Config Extension using configuration: ```yaml # extension.yml # ... metadata ... provides: config: - name: "tool-config.yml" template: "tool-config.template.yml" required: true ``` ```yaml # tool-config.template.yml api_endpoint: "https://api.example.com" timeout: 30 ``` ````markdown # Use Config Load config: ```bash config_file=".specify/extensions/tool/tool-config.yml" endpoint=$(yq eval '.api_endpoint' "$config_file") echo "Using endpoint: $endpoint" ``` ```` ### Extension with Hooks Extension that runs automatically: ```yaml # extension.yml hooks: after_tasks: command: "speckit.auto.analyze" optional: false # Always run description: "Analyze tasks after generation" ``` --- ## Troubleshooting ### Extension won't install **Error**: `Invalid extension ID` - **Fix**: Use lowercase, alphanumeric + hyphens only **Error**: `Extension requires spec-kit >=0.2.0` - **Fix**: Update spec-kit with `uv tool install specify-cli --force` **Error**: `Command file not found` - **Fix**: Ensure command files exist at paths specified in manifest ### Commands not registered **Symptom**: Commands don't appear in AI agent **Check**: 1. `.claude/commands/` directory exists 2. Extension installed successfully 3. Commands registered in registry: ```bash cat .specify/extensions/.registry ``` **Fix**: Reinstall extension to trigger registration ### Config not loading **Check**: 1. Config file exists: `.specify/extensions/{ext-id}/{ext-id}-config.yml` 2. YAML syntax is valid: `yq eval '.' config.yml` 3. Environment variables set correctly --- ## Getting Help - **Issues**: Report bugs at GitHub repository - **Discussions**: Ask questions in GitHub Discussions - **Examples**: See `spec-kit-jira` for full-featured example (Phase B) --- ## Next Steps 1. **Create your extension** following this guide 2. **Test locally** with `--dev` flag 3. **Share with community** (GitHub, catalog) 4. **Iterate** based on feedback Happy extending! 🚀