extensions/RFC-EXTENSION-SYSTEM.md
Status: Implemented Author: Stats Perform Engineering Created: 2026-01-28 Updated: 2026-03-11
Introduce an extension system to Spec Kit that allows modular integration with external tools (Jira, Linear, Azure DevOps, etc.) without bloating the core framework. Extensions are self-contained packages installed into .specify/extensions/ with declarative manifests, versioned independently, and discoverable through a central catalog.
Monolithic Growth: Adding Jira integration to core spec-kit creates:
Limited Flexibility: Different organizations use different tools:
Maintenance Burden: Every integration adds:
Community Friction: External contributors can't easily add integrations without core repo PR approval and release cycles.
.specify/extensions/{name}/)extension.yml)speckit.{extension}.{command})specify extension add jiraproject/
├── .specify/
│ ├── scripts/ # Core scripts (unchanged)
│ ├── templates/ # Core templates (unchanged)
│ ├── memory/ # Session memory
│ ├── extensions/ # Extensions directory (NEW)
│ │ ├── .registry # Installed extensions metadata (NEW)
│ │ ├── jira/ # Jira extension
│ │ │ ├── extension.yml # Manifest
│ │ │ ├── jira-config.yml # Extension config
│ │ │ ├── commands/ # Command files
│ │ │ ├── scripts/ # Helper scripts
│ │ │ └── docs/ # Documentation
│ │ └── linear/ # Linear extension (example)
│ └── extensions.yml # Project extension configuration (NEW)
└── .gitignore # Ignore local extension configs
┌─────────────────────────────────────────────────────────┐
│ Spec Kit Core │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CLI (specify) │ │
│ │ - init, check │ │
│ │ - extension add/remove/list/update ← NEW │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Extension Manager ← NEW │ │
│ │ - Discovery, Installation, Validation │ │
│ │ - Command Registration, Hook Execution │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Core Commands │ │
│ │ - /speckit.specify │ │
│ │ - /speckit.tasks │ │
│ │ - /speckit.implement │ │
│ └─────────┬────────────────────────────────────────┘ │
└────────────┼────────────────────────────────────────────┘
│ Hook Points (after_tasks, after_implement)
↓
┌─────────────────────────────────────────────────────────┐
│ Extensions │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Jira Extension │ │
│ │ - /speckit.jira.specstoissues │ │
│ │ - /speckit.jira.discover-fields │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Linear Extension │ │
│ │ - /speckit.linear.sync │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ Calls external tools
↓
┌─────────────────────────────────────────────────────────┐
│ External Tools │
│ - Jira MCP Server │
│ - Linear API │
│ - GitHub API │
└─────────────────────────────────────────────────────────┘
extension.yml# Extension Manifest Schema v1.0
# All extensions MUST include this file at root
# Schema version for compatibility
schema_version: "1.0"
# Extension metadata (REQUIRED)
extension:
id: "jira" # Unique identifier (lowercase, alphanumeric, hyphens)
name: "Jira Integration" # Human-readable name
version: "1.0.0" # Semantic version
description: "Create Jira Epics, Stories, and Issues from spec-kit artifacts"
author: "Stats Perform" # Author/organization
repository: "https://github.com/statsperform/spec-kit-jira"
license: "MIT" # SPDX license identifier
homepage: "https://github.com/statsperform/spec-kit-jira/blob/main/README.md"
# Compatibility requirements (REQUIRED)
requires:
# Spec-kit version (semantic version range)
speckit_version: ">=0.1.0,<2.0.0"
# External tools required by extension
tools:
- name: "jira-mcp-server"
required: true
version: ">=1.0.0" # Optional: version constraint
description: "Jira MCP server for API access"
install_url: "https://github.com/your-org/jira-mcp-server"
check_command: "jira --version" # Optional: CLI command to verify
# Core spec-kit commands this extension depends on
commands:
- "speckit.tasks" # Extension needs tasks command
# Core scripts required
scripts:
- "check-prerequisites.sh"
# What this extension provides (REQUIRED)
provides:
# Commands added to AI agent
commands:
- name: "speckit.jira.specstoissues"
file: "commands/specstoissues.md"
description: "Create Jira hierarchy from spec and tasks"
aliases: ["speckit.jira.sync"] # Alternate names
- name: "speckit.jira.discover-fields"
file: "commands/discover-fields.md"
description: "Discover Jira custom fields for configuration"
- name: "speckit.jira.sync-status"
file: "commands/sync-status.md"
description: "Sync task completion status to Jira"
# Configuration files
config:
- name: "jira-config.yml"
template: "jira-config.template.yml"
description: "Jira integration configuration"
required: true # User must configure before use
# Helper scripts
scripts:
- name: "parse-jira-config.sh"
file: "scripts/parse-jira-config.sh"
description: "Parse jira-config.yml to JSON"
executable: true # Make executable on install
# Extension configuration defaults (OPTIONAL)
defaults:
project:
key: null # No default, user must configure
hierarchy:
issue_type: "subtask"
update_behavior:
mode: "update"
sync_completion: true
# Configuration schema for validation (OPTIONAL)
config_schema:
type: "object"
required: ["project"]
properties:
project:
type: "object"
required: ["key"]
properties:
key:
type: "string"
pattern: "^[A-Z]{2,10}$"
description: "Jira project key (e.g., MSATS)"
# Integration hooks (OPTIONAL)
hooks:
# Hook fired after /speckit.tasks completes
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy after task generation"
# Hook fired after /speckit.implement completes
after_implement:
command: "speckit.jira.sync-status"
optional: true
prompt: "Sync completion status to Jira?"
# Tags for discovery (OPTIONAL)
tags:
- "issue-tracking"
- "jira"
- "atlassian"
- "project-management"
# Changelog URL (OPTIONAL)
changelog: "https://github.com/statsperform/spec-kit-jira/blob/main/CHANGELOG.md"
# Support information (OPTIONAL)
support:
documentation: "https://github.com/statsperform/spec-kit-jira/blob/main/docs/"
issues: "https://github.com/statsperform/spec-kit-jira/issues"
discussions: "https://github.com/statsperform/spec-kit-jira/discussions"
email: "[email protected]"
schema_version, extension, requires, providesversionid (no conflicts with other extensions)config_schema if extension uses configsupport informationfile paths MUST be relative to extension rootcommand names MUST match a command in provides.commandsspecify extension search jira
# Searches catalog for extensions matching "jira"
Process:
specify extension add jira
Process:
.specify/extensions/jira/.specify/extensions/.registryRegistry Format (.specify/extensions/.registry):
{
"schema_version": "1.0",
"extensions": {
"jira": {
"version": "1.0.0",
"installed_at": "2026-01-28T14:30:00Z",
"source": "catalog",
"manifest_hash": "sha256:abc123...",
"enabled": true,
"priority": 10
}
}
}
Priority Field: Extensions are ordered by priority (lower = higher precedence). Default is 10. Used for template resolution when multiple extensions provide the same template.
# User edits extension config
vim .specify/extensions/jira/jira-config.yml
Config discovery order:
extension.yml → defaults)jira-config.yml)jira-config.local.yml - gitignored)SPECKIT_JIRA_*)claude
> /speckit.jira.specstoissues
Command resolution:
.claude/commands/speckit.jira.specstoissues.mdspecify extension update jira
Process:
specify extension remove jira
Process:
.specify/extensions/jira/Extensions provide universal command format (Markdown-based), and CLI converts to agent-specific format during registration.
Location: Extension's commands/specstoissues.md
---
# Universal metadata (parsed by all agents)
description: "Create Jira hierarchy from spec and tasks"
tools:
- 'jira-mcp-server/epic_create'
- 'jira-mcp-server/story_create'
scripts:
sh: ../../scripts/bash/check-prerequisites.sh --json
ps: ../../scripts/powershell/check-prerequisites.ps1 -Json
---
# Command implementation
## User Input
$ARGUMENTS
## Steps
1. Load jira-config.yml
2. Parse spec.md and tasks.md
3. Create Jira items
Output: .claude/commands/speckit.jira.specstoissues.md
---
description: "Create Jira hierarchy from spec and tasks"
tools:
- 'jira-mcp-server/epic_create'
- 'jira-mcp-server/story_create'
scripts:
sh: .specify/scripts/bash/check-prerequisites.sh --json
ps: .specify/scripts/powershell/check-prerequisites.ps1 -Json
---
# Command implementation (copied from extension)
## User Input
$ARGUMENTS
## Steps
1. Load jira-config.yml from .specify/extensions/jira/
2. Parse spec.md and tasks.md
3. Create Jira items
Transformation:
Output: .gemini/commands/speckit.jira.specstoissues.toml
[command]
name = "speckit.jira.specstoissues"
description = "Create Jira hierarchy from spec and tasks"
[command.tools]
tools = [
"jira-mcp-server/epic_create",
"jira-mcp-server/story_create"
]
[command.script]
sh = ".specify/scripts/bash/check-prerequisites.sh --json"
ps = ".specify/scripts/powershell/check-prerequisites.ps1 -Json"
[command.template]
content = """
# Command implementation
## User Input
{{args}}
## Steps
1. Load jira-config.yml from .specify/extensions/jira/
2. Parse spec.md and tasks.md
3. Create Jira items
"""
Transformation:
$ARGUMENTS to {{args}}Location: src/specify_cli/extensions.py
def register_extension_commands(
project_path: Path,
ai_assistant: str,
manifest: dict
) -> None:
"""Register extension commands with AI agent."""
agent_config = AGENT_CONFIG.get(ai_assistant)
if not agent_config:
console.print(f"[yellow]Unknown agent: {ai_assistant}[/yellow]")
return
ext_id = manifest['extension']['id']
ext_dir = project_path / ".specify" / "extensions" / ext_id
agent_commands_dir = project_path / agent_config['folder'].rstrip('/') / "commands"
agent_commands_dir.mkdir(parents=True, exist_ok=True)
for cmd_info in manifest['provides']['commands']:
cmd_name = cmd_info['name']
source_file = ext_dir / cmd_info['file']
if not source_file.exists():
console.print(f"[red]Command file not found:[/red] {cmd_info['file']}")
continue
# Convert to agent-specific format
if ai_assistant == "claude":
dest_file = agent_commands_dir / f"{cmd_name}.md"
convert_to_claude(source_file, dest_file, ext_dir)
elif ai_assistant == "gemini":
dest_file = agent_commands_dir / f"{cmd_name}.toml"
convert_to_gemini(source_file, dest_file, ext_dir)
elif ai_assistant == "copilot":
dest_file = agent_commands_dir / f"{cmd_name}.md"
convert_to_copilot(source_file, dest_file, ext_dir)
# ... other agents
console.print(f" ✓ Registered: {cmd_name}")
def convert_to_claude(
source: Path,
dest: Path,
ext_dir: Path
) -> None:
"""Convert universal command to Claude format."""
# Parse universal command
content = source.read_text()
frontmatter, body = parse_frontmatter(content)
# Adjust script paths (relative to repo root)
if 'scripts' in frontmatter:
for key in frontmatter['scripts']:
frontmatter['scripts'][key] = adjust_path_for_repo_root(
frontmatter['scripts'][key]
)
# Inject extension context
body = inject_extension_context(body, ext_dir)
# Write Claude command
dest.write_text(render_frontmatter(frontmatter) + "\n" + body)
# .specify/extensions/jira/jira-config.yml (Project config)
project:
key: "MSATS"
hierarchy:
issue_type: "subtask"
defaults:
epic:
labels: ["spec-driven", "typescript"]
# .specify/extensions/jira/jira-config.local.yml (Local overrides - gitignored)
project:
key: "MYTEST" # Override for local testing
# Environment variables (highest precedence)
export SPECKIT_JIRA_PROJECT_KEY="DEVTEST"
Location: Extension command (e.g., commands/specstoissues.md)
## Load Configuration
1. Run helper script to load and merge config:
```bash
config_json=$(bash .specify/extensions/jira/scripts/parse-jira-config.sh)
echo "$config_json"
```
1. Parse JSON and use in subsequent steps
Script: .specify/extensions/jira/scripts/parse-jira-config.sh
#!/usr/bin/env bash
set -euo pipefail
EXT_DIR=".specify/extensions/jira"
CONFIG_FILE="$EXT_DIR/jira-config.yml"
LOCAL_CONFIG="$EXT_DIR/jira-config.local.yml"
# Start with defaults from extension.yml
defaults=$(yq eval '.defaults' "$EXT_DIR/extension.yml" -o=json)
# Merge project config
if [ -f "$CONFIG_FILE" ]; then
project_config=$(yq eval '.' "$CONFIG_FILE" -o=json)
defaults=$(echo "$defaults $project_config" | jq -s '.[0] * .[1]')
fi
# Merge local config
if [ -f "$LOCAL_CONFIG" ]; then
local_config=$(yq eval '.' "$LOCAL_CONFIG" -o=json)
defaults=$(echo "$defaults $local_config" | jq -s '.[0] * .[1]')
fi
# Apply environment variable overrides
if [ -n "${SPECKIT_JIRA_PROJECT_KEY:-}" ]; then
defaults=$(echo "$defaults" | jq ".project.key = \"$SPECKIT_JIRA_PROJECT_KEY\"")
fi
# Output merged config as JSON
echo "$defaults"
In command file:
## Validate Configuration
1. Load config (from previous step)
2. Validate against schema from extension.yml:
```python
import jsonschema
schema = load_yaml(".specify/extensions/jira/extension.yml")['config_schema']
config = json.loads(config_json)
try:
jsonschema.validate(config, schema)
except jsonschema.ValidationError as e:
print(f"❌ Invalid jira-config.yml: {e.message}")
print(f" Path: {'.'.join(str(p) for p in e.path)}")
exit(1)
```
1. Proceed with validated config
In extension.yml:
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy"
condition: "config.project.key is set"
During extension installation, record hooks in project config:
File: .specify/extensions.yml (project-level extension config)
# Extensions installed in this project
installed:
- jira
- linear
# Global extension settings
settings:
auto_execute_hooks: true # Prompt for optional hooks after commands
# Hook configuration
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
after_implement:
- extension: jira
command: speckit.jira.sync-status
enabled: true
optional: true
prompt: "Sync completion status to Jira?"
In core command (e.g., templates/commands/tasks.md):
Add at end of command:
## Extension Hooks
After task generation completes, check for registered hooks:
```bash
# Check if extensions.yml exists and has after_tasks hooks
if [ -f ".specify/extensions.yml" ]; then
# Parse hooks for after_tasks
hooks=$(yq eval '.hooks.after_tasks[] | select(.enabled == true)' .specify/extensions.yml -o=json)
if [ -n "$hooks" ]; then
echo ""
echo "📦 Extension hooks available:"
# Iterate hooks
echo "$hooks" | jq -c '.' | while read -r hook; do
extension=$(echo "$hook" | jq -r '.extension')
command=$(echo "$hook" | jq -r '.command')
optional=$(echo "$hook" | jq -r '.optional')
prompt_text=$(echo "$hook" | jq -r '.prompt')
if [ "$optional" = "true" ]; then
# Prompt user
echo ""
read -p "$prompt_text (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "▶ Executing: $command"
# Let AI agent execute the command
# (AI agent will see this and execute)
echo "EXECUTE_COMMAND: $command"
fi
else
# Auto-execute mandatory hooks
echo "▶ Executing: $command (required)"
echo "EXECUTE_COMMAND: $command"
fi
done
fi
fi
```
AI Agent Handling:
The AI agent sees EXECUTE_COMMAND: speckit.jira.specstoissues in output and automatically invokes that command.
Alternative: Direct call in agent context (if agent supports it):
# In AI agent's command execution engine
def execute_command_with_hooks(command_name: str, args: str):
# Execute main command
result = execute_command(command_name, args)
# Check for hooks
hooks = load_hooks_for_phase(f"after_{command_name}")
for hook in hooks:
if hook.optional:
if confirm(hook.prompt):
execute_command(hook.command, args)
else:
execute_command(hook.command, args)
return result
Extensions can specify conditions for hooks:
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
condition: "config.project.key is set and config.enabled == true"
Condition evaluation (in hook executor):
def should_execute_hook(hook: dict, config: dict) -> bool:
"""Evaluate hook condition."""
condition = hook.get('condition')
if not condition:
return True # No condition = always eligible
# Simple expression evaluator
# "config.project.key is set" → check if config['project']['key'] exists
# "config.enabled == true" → check if config['enabled'] is True
return eval_condition(condition, config)
Spec Kit uses two catalog files with different purposes:
catalog.json)URL: https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json
install_allowed: true) in the default stackcatalog.community.json)URL: https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json
verified: false initiallyinstall_allowed: false) in the default stack — discovery onlyHow It Works (default stack):
specify extension search searches both catalogs — community extensions appear automaticallycatalog.json, or add to .specify/extension-catalogs.yml with install_allowed: truespecify extension add <name> — only allowed from install_allowed: true catalogsThis approach gives organizations full control over which extensions can be installed while still providing community discoverability out of the box.
Format (same for both catalogs):
{
"schema_version": "1.0",
"updated_at": "2026-01-28T14:30:00Z",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"description": "Create Jira Epics, Stories, and Issues from spec-kit artifacts",
"author": "Stats Perform",
"version": "1.0.0",
"download_url": "https://github.com/statsperform/spec-kit-jira/releases/download/v1.0.0/spec-kit-jira-1.0.0.zip",
"repository": "https://github.com/statsperform/spec-kit-jira",
"homepage": "https://github.com/statsperform/spec-kit-jira/blob/main/README.md",
"documentation": "https://github.com/statsperform/spec-kit-jira/blob/main/docs/",
"changelog": "https://github.com/statsperform/spec-kit-jira/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0,<2.0.0",
"tools": [
{
"name": "jira-mcp-server",
"version": ">=1.0.0"
}
]
},
"tags": ["issue-tracking", "jira", "atlassian", "project-management"],
"verified": true,
"downloads": 1250,
"stars": 45
},
"linear": {
"name": "Linear Integration",
"id": "linear",
"description": "Sync spec-kit tasks with Linear issues",
"author": "Community",
"version": "0.9.0",
"download_url": "https://github.com/example/spec-kit-linear/releases/download/v0.9.0/spec-kit-linear-0.9.0.zip",
"repository": "https://github.com/example/spec-kit-linear",
"requires": {
"speckit_version": ">=0.1.0"
},
"tags": ["issue-tracking", "linear"],
"verified": false
}
}
}
# List all available extensions
specify extension search
# Search by keyword
specify extension search jira
# Search by tag
specify extension search --tag issue-tracking
# Show extension details
specify extension info jira
Spec Kit supports a catalog stack — an ordered list of catalogs that the CLI merges and searches across. This allows organizations to maintain their own org-approved extensions alongside an internal catalog and community discovery, all at once.
The active catalog stack is resolved in this order (first match wins):
SPECKIT_CATALOG_URL environment variable — single catalog replacing all defaults (backward compat).specify/extension-catalogs.yml — full control for the project~/.specify/extension-catalogs.yml — personal defaultscatalog.json (install_allowed: true) + catalog.community.json (install_allowed: false)When no config file exists, the CLI uses:
| Priority | Catalog | install_allowed | Purpose |
|---|---|---|---|
| 1 | catalog.json (default) | true | Curated extensions available for installation |
| 2 | catalog.community.json (community) | false | Discovery only — browse but not install |
This means specify extension search surfaces community extensions out of the box, while specify extension add is still restricted to entries from catalogs with install_allowed: true.
.specify/extension-catalogs.yml Config Filecatalogs:
- name: "default"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
priority: 1 # Highest — only approved entries can be installed
install_allowed: true
description: "Built-in catalog of installable extensions"
- name: "internal"
url: "https://internal.company.com/spec-kit/catalog.json"
priority: 2
install_allowed: true
description: "Internal company extensions"
- name: "community"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
priority: 3 # Lowest — discovery only, not installable
install_allowed: false
description: "Community-contributed extensions (discovery only)"
A user-level equivalent lives at ~/.specify/extension-catalogs.yml. When a project-level config is present with one or more catalog entries, it takes full control and the built-in defaults are not applied. An empty catalogs: [] list is treated the same as no config file, falling back to defaults.
# List active catalogs with name, URL, priority, and install_allowed
specify extension catalog list
# Add a catalog (project-scoped)
specify extension catalog add --name "internal" --install-allowed \
https://internal.company.com/spec-kit/catalog.json
# Add a discovery-only catalog
specify extension catalog add --name "community" \
https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json
# Remove a catalog
specify extension catalog remove internal
# Show which catalog an extension came from
specify extension info jira
# → Source catalog: default
When the same extension id appears in multiple catalogs, the higher-priority (lower priority number) catalog wins. Extensions from lower-priority catalogs with the same id are ignored.
install_allowed: false BehaviorExtensions from discovery-only catalogs are shown in specify extension search results but cannot be installed directly:
⚠ 'linear' is available in the 'community' catalog but installation is not allowed from that catalog.
To enable installation, add 'linear' to an approved catalog (install_allowed: true) in .specify/extension-catalogs.yml.
SPECKIT_CATALOG_URL (Backward Compatibility)The SPECKIT_CATALOG_URL environment variable still works — it is treated as a single install_allowed: true catalog, replacing both defaults for full backward compatibility:
# Point to your organization's catalog
export SPECKIT_CATALOG_URL="https://internal.company.com/spec-kit/catalog.json"
# All extension commands now use your custom catalog
specify extension search # Uses custom catalog
specify extension add jira # Installs from custom catalog
Requirements:
Example for testing:
# Test with localhost during development
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
specify extension search
specify extension Subcommandsspecify extension listList installed extensions in current project.
$ specify extension list
Installed Extensions:
✓ Jira Integration (v1.0.0)
jira
Create Jira issues from spec-kit artifacts
Commands: 3 | Hooks: 2 | Priority: 10 | Status: Enabled
✓ Linear Integration (v0.9.0)
linear
Create Linear issues from spec-kit artifacts
Commands: 1 | Hooks: 1 | Priority: 10 | Status: Enabled
Options:
--available: Show available (not installed) extensions from catalog--all: Show both installed and availablespecify extension search [QUERY]Search extension catalog.
$ specify extension search jira
Found 1 extension:
┌─────────────────────────────────────────────────────────┐
│ jira (v1.0.0) ✓ Verified │
│ Jira Integration │
│ │
│ Create Jira Epics, Stories, and Issues from spec-kit │
│ artifacts │
│ │
│ Author: Stats Perform │
│ Tags: issue-tracking, jira, atlassian │
│ Downloads: 1,250 │
│ │
│ Repository: github.com/statsperform/spec-kit-jira │
│ Documentation: github.com/.../docs │
└─────────────────────────────────────────────────────────┘
Install: specify extension add jira
Options:
--tag TAG: Filter by tag--author AUTHOR: Filter by author--verified: Show only verified extensionsspecify extension info NAMEShow detailed information about an extension.
$ specify extension info jira
Jira Integration (jira) v1.0.0
Description:
Create Jira Epics, Stories, and Issues from spec-kit artifacts
Author: Stats Perform
License: MIT
Repository: https://github.com/statsperform/spec-kit-jira
Documentation: https://github.com/statsperform/spec-kit-jira/blob/main/docs/
Requirements:
• Spec Kit: >=0.1.0,<2.0.0
• Tools: jira-mcp-server (>=1.0.0)
Provides:
Commands:
• speckit.jira.specstoissues - Create Jira hierarchy from spec and tasks
• speckit.jira.discover-fields - Discover Jira custom fields
• speckit.jira.sync-status - Sync task completion status
Hooks:
• after_tasks - Prompt to create Jira issues
• after_implement - Prompt to sync status
Tags: issue-tracking, jira, atlassian, project-management
Downloads: 1,250 | Stars: 45 | Verified: ✓
Install: specify extension add jira
specify extension add NAMEInstall an extension.
$ specify extension add jira
Installing extension: Jira Integration
✓ Downloaded spec-kit-jira-1.0.0.zip (245 KB)
✓ Validated manifest
✓ Checked compatibility (spec-kit 0.1.0 ≥ 0.1.0)
✓ Extracted to .specify/extensions/jira/
✓ Registered 3 commands with claude
✓ Installed config template (jira-config.yml)
⚠ Configuration required:
Edit .specify/extensions/jira/jira-config.yml to set your Jira project key
Extension installed successfully!
Next steps:
1. Configure: vim .specify/extensions/jira/jira-config.yml
2. Discover fields: /speckit.jira.discover-fields
3. Use commands: /speckit.jira.specstoissues
Options:
--from URL: Install from a remote URL (archive). Does not accept Git repositories directly.--dev: Install from a local path in development mode (the PATH is the positional extension argument).--priority NUMBER: Set resolution priority (lower = higher precedence, default 10)specify extension remove NAMEUninstall an extension.
$ specify extension remove jira
⚠ This will remove:
• 3 commands from AI agent
• Extension directory: .specify/extensions/jira/
• Config file: jira-config.yml (will be backed up)
Continue? (yes/no): yes
✓ Unregistered commands
✓ Backed up config to .specify/extensions/.backup/jira-config.yml
✓ Removed extension directory
✓ Updated registry
Extension removed successfully.
To reinstall: specify extension add jira
Options:
--keep-config: Don't remove config file--force: Skip confirmationspecify extension update [NAME]Update extension(s) to latest version.
$ specify extension update jira
Checking for updates...
jira: 1.0.0 → 1.1.0 available
Changes in v1.1.0:
• Added support for custom workflows
• Fixed issue with parallel tasks
• Improved error messages
Update? (yes/no): yes
✓ Downloaded spec-kit-jira-1.1.0.zip
✓ Validated manifest
✓ Backed up current version
✓ Extracted new version
✓ Preserved config file
✓ Re-registered commands
Extension updated successfully!
Changelog: https://github.com/statsperform/spec-kit-jira/blob/main/CHANGELOG.md#v110
Options:
--all: Update all extensions--check: Check for updates without installing--force: Force update even if already latestspecify extension enable/disable NAMEEnable or disable an extension without removing it.
$ specify extension disable jira
✓ Disabled extension: jira
• Commands unregistered (but files preserved)
• Hooks will not execute
To re-enable: specify extension enable jira
specify extension set-priority NAME PRIORITYChange the resolution priority of an installed extension.
$ specify extension set-priority jira 5
✓ Extension 'Jira Integration' priority changed: 10 → 5
Lower priority = higher precedence in template resolution
Priority Values:
Use Cases:
Extensions follow SemVer 2.0.0:
At installation:
def check_compatibility(extension_manifest: dict) -> bool:
"""Check if extension is compatible with current environment."""
requires = extension_manifest['requires']
# 1. Check spec-kit version
current_speckit = get_speckit_version() # e.g., "0.1.5"
required_speckit = requires['speckit_version'] # e.g., ">=0.1.0,<2.0.0"
if not version_satisfies(current_speckit, required_speckit):
raise IncompatibleVersionError(
f"Extension requires spec-kit {required_speckit}, "
f"but {current_speckit} is installed. "
f"Upgrade spec-kit with: uv tool install specify-cli --force"
)
# 2. Check required tools
for tool in requires.get('tools', []):
tool_name = tool['name']
tool_version = tool.get('version')
if tool.get('required', True):
if not check_tool(tool_name):
raise MissingToolError(
f"Extension requires tool: {tool_name}\n"
f"Install from: {tool.get('install_url', 'N/A')}"
)
if tool_version:
installed = get_tool_version(tool_name, tool.get('check_command'))
if not version_satisfies(installed, tool_version):
raise IncompatibleToolVersionError(
f"Extension requires {tool_name} {tool_version}, "
f"but {installed} is installed"
)
# 3. Check required commands
for cmd in requires.get('commands', []):
if not command_exists(cmd):
raise MissingCommandError(
f"Extension requires core command: {cmd}\n"
f"Update spec-kit to latest version"
)
return True
Extension manifest can mark features as deprecated:
provides:
commands:
- name: "speckit.jira.old-command"
file: "commands/old-command.md"
deprecated: true
deprecated_message: "Use speckit.jira.new-command instead"
removal_version: "2.0.0"
At runtime, show warning:
⚠️ Warning: /speckit.jira.old-command is deprecated
Use /speckit.jira.new-command instead
This command will be removed in v2.0.0
Extensions run with same privileges as AI agent:
Trust boundary: User must trust extension author.
Verified Extensions (in catalog):
Community Extensions:
Not verified, use at own risk
Show warning during installation:
⚠️ This extension is not verified.
Review code before installing: https://github.com/...
Continue? (yes/no):
Phase 2 (not in initial release):
filesystem:read, network:external, env:read# Future extension.yml
permissions:
- "filesystem:read:.specify/extensions/jira/" # Can only read own config
- "filesystem:write:.specify/memory/" # Can write to memory
- "network:external:*.atlassian.net" # Can call Jira API
- "env:read:SPECKIT_JIRA_*" # Can read own env vars
Future: Sign extension packages with GPG/Sigstore
# catalog.json
"jira": {
"download_url": "...",
"checksum": "sha256:abc123...",
"signature": "https://github.com/.../spec-kit-jira-1.0.0.sig",
"signing_key": "https://github.com/statsperform.gpg"
}
CLI verifies signature before extraction.
Goal: Existing spec-kit projects work without changes.
Strategy:
Core commands unchanged: /speckit.tasks, /speckit.implement, etc. remain in core
Optional extensions: Users opt-in to extensions
Gradual migration: Existing taskstoissues stays in core, Jira extension is alternative
Deprecation timeline:
taskstoissuestaskstoissues as "legacy" (still works)taskstoissues in favor of extensionScenario 1: User has no taskstoissues usage
Scenario 2: User uses core taskstoissues (GitHub Issues)
github-projects extension for more featuresScenario 3: User wants Jira (new requirement)
specify extension add jiraScenario 4: User has custom scripts calling taskstoissues
For extension authors (if core command becomes extension):
# Old (core command)
/speckit.taskstoissues
# New (extension command)
specify extension add github-projects
/speckit.github.taskstoissues
Migration alias (if needed):
# extension.yml
provides:
commands:
- name: "speckit.github.taskstoissues"
file: "commands/taskstoissues.md"
aliases: ["speckit.github.sync-taskstoissues"] # Alternate namespaced entry point
AI agents register both names, so callers can migrate to the alternate alias without relying on deprecated global shortcuts like /speckit.taskstoissues.
Goal: Basic extension infrastructure
Deliverables:
extension.yml)specify extension listspecify extension add (from URL and local --dev)specify extension remove.specify/extensions/.registry)Testing:
Goal: First production extension
Deliverables:
spec-kit-jira repositoryjira-config.yml templatespecstoissues.mddiscover-fields.mdsync-status.mdTesting:
eng-msa-ts projectGoal: Discovery and distribution
Deliverables:
extensions/catalog.json in spec-kit repo)extensions/catalog.community.json)specify extension searchspecify extension infospecify extension catalog listspecify extension catalog addspecify extension catalog removeTesting:
Goal: Hooks, updates, multi-agent support
Deliverables:
hooks in extension.yml).specify/extensions.yml)specify extension update (with atomic backup/restore)specify extension enable/disableAdditional features implemented beyond original RFC:
extension info falls back to local info when catalog unavailable_install_allowed flag: Discovery-only catalogs can't be used for installationSPECKIT_CATALOG_URL changesTesting:
Goal: Production ready
Deliverables:
Testing:
The following questions from the original RFC have been resolved during implementation:
Question: Should extension commands use namespace prefix?
Decision: Option C - Both prefixed and aliases are supported. Commands use speckit.{extension}.{command} as canonical name, with optional aliases defined in manifest.
Implementation: The aliases field in extension.yml allows extensions to register additional command names.
Question: Where should extension configs live?
Decision: Option A - Extension directory (.specify/extensions/{ext-id}/{ext-id}-config.yml). This keeps extensions self-contained and easier to manage.
Implementation: Each extension has its own config file within its directory, with layered resolution (defaults → project → local → env vars).
Question: Should extensions use universal format or agent-specific?
Decision: Option A - Universal Markdown format. Extensions write commands once, CLI converts to agent-specific format during registration.
Implementation: CommandRegistrar class handles conversion to 15+ agent formats (Claude, Copilot, Gemini, Cursor, etc.).
Question: How should hooks execute?
Decision: Option A - Hooks are registered in .specify/extensions.yml and executed by the AI agent when it sees the hook trigger. Hook state (enabled/disabled) is managed per-extension.
Implementation: HookExecutor class manages hook registration and state in extensions.yml.
Question: How should extensions be packaged?
Decision: Option A - ZIP archives downloaded from GitHub releases (via catalog download_url). Local development uses --dev flag with directory path.
Implementation: ExtensionManager.install_from_zip() handles ZIP extraction and validation.
Question: Can multiple versions of same extension coexist?
Decision: Option A - Single version only. Updates replace the existing version with atomic rollback on failure.
Implementation: extension update performs atomic backup/restore to ensure safe updates.
Question: Should extensions declare required permissions?
Options:
filesystem:read, network:external, etc.Status: Deferred to future version. Currently using trust-based model where users trust extension authors.
Question: Should extensions be cryptographically signed?
Options:
Status: Deferred to future version. checksum field is available in catalog schema but not enforced.
Complete structure of spec-kit-jira extension:
spec-kit-jira/
├── README.md # Overview, features, installation
├── LICENSE # MIT license
├── CHANGELOG.md # Version history
├── .gitignore # Ignore local configs
│
├── extension.yml # Extension manifest (required)
├── jira-config.template.yml # Config template
│
├── commands/ # Command files
│ ├── specstoissues.md # Main command
│ ├── discover-fields.md # Helper: Discover custom fields
│ └── sync-status.md # Helper: Sync completion status
│
├── scripts/ # Helper scripts
│ ├── parse-jira-config.sh # Config loader (bash)
│ ├── parse-jira-config.ps1 # Config loader (PowerShell)
│ └── validate-jira-connection.sh # Connection test
│
├── docs/ # Documentation
│ ├── installation.md # Installation guide
│ ├── configuration.md # Configuration reference
│ ├── usage.md # Usage examples
│ ├── troubleshooting.md # Common issues
│ └── examples/
│ ├── eng-msa-ts-config.yml # Real-world config example
│ └── simple-project.yml # Minimal config example
│
├── tests/ # Tests (optional)
│ ├── test-extension.sh # Extension validation
│ └── test-commands.sh # Command execution tests
│
└── .github/ # GitHub integration
└── workflows/
└── release.yml # Automated releases
Documentation for creating new extensions:
Getting Started
Extension Manifest
Command Development
Configuration
Hooks
Testing
--dev flagPublishing
Examples
Planned support matrix:
| Extension Feature | Spec Kit Version | AI Agent Support |
|---|---|---|
| Basic commands | 0.2.0+ | Claude, Gemini, Copilot |
| Hooks (after_tasks) | 0.3.0+ | Claude, Gemini |
| Config validation | 0.2.0+ | All |
| Multiple catalogs | 0.4.0+ | All |
| Permissions (sandboxing) | 1.0.0+ | TBD |
Full schema for catalog.json:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["schema_version", "updated_at", "extensions"],
"properties": {
"schema_version": {
"type": "string",
"pattern": "^\\d+\\.\\d+$"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"extensions": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object",
"required": ["name", "id", "version", "download_url", "repository"],
"properties": {
"name": { "type": "string" },
"id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
"description": { "type": "string" },
"author": { "type": "string" },
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
"download_url": { "type": "string", "format": "uri" },
"repository": { "type": "string", "format": "uri" },
"homepage": { "type": "string", "format": "uri" },
"documentation": { "type": "string", "format": "uri" },
"changelog": { "type": "string", "format": "uri" },
"license": { "type": "string" },
"requires": {
"type": "object",
"properties": {
"speckit_version": { "type": "string" },
"tools": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"version": { "type": "string" }
}
}
}
}
},
"tags": {
"type": "array",
"items": { "type": "string" }
},
"verified": { "type": "boolean" },
"downloads": { "type": "integer" },
"stars": { "type": "integer" },
"checksum": { "type": "string" }
}
}
}
}
}
}
This RFC proposes a comprehensive extension system for Spec Kit that: