Compare commits

..

1 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
52394c6447 Initial plan 2025-10-22 03:26:21 +00:00
54 changed files with 290 additions and 10484 deletions

View File

@@ -1,11 +0,0 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -17,6 +17,4 @@ jobs:
- name: Run markdownlint-cli2 - name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@v19 uses: DavidAnson/markdownlint-cli2-action@v19
with: with:
globs: | globs: '**/*.md'
'**/*.md'
!extensions/**/*.md

View File

@@ -40,17 +40,9 @@ gh release create "$VERSION" \
.genreleases/spec-kit-template-roo-ps-"$VERSION".zip \ .genreleases/spec-kit-template-roo-ps-"$VERSION".zip \
.genreleases/spec-kit-template-codebuddy-sh-"$VERSION".zip \ .genreleases/spec-kit-template-codebuddy-sh-"$VERSION".zip \
.genreleases/spec-kit-template-codebuddy-ps-"$VERSION".zip \ .genreleases/spec-kit-template-codebuddy-ps-"$VERSION".zip \
.genreleases/spec-kit-template-qoder-sh-"$VERSION".zip \
.genreleases/spec-kit-template-qoder-ps-"$VERSION".zip \
.genreleases/spec-kit-template-amp-sh-"$VERSION".zip \ .genreleases/spec-kit-template-amp-sh-"$VERSION".zip \
.genreleases/spec-kit-template-amp-ps-"$VERSION".zip \ .genreleases/spec-kit-template-amp-ps-"$VERSION".zip \
.genreleases/spec-kit-template-shai-sh-"$VERSION".zip \
.genreleases/spec-kit-template-shai-ps-"$VERSION".zip \
.genreleases/spec-kit-template-q-sh-"$VERSION".zip \ .genreleases/spec-kit-template-q-sh-"$VERSION".zip \
.genreleases/spec-kit-template-q-ps-"$VERSION".zip \ .genreleases/spec-kit-template-q-ps-"$VERSION".zip \
.genreleases/spec-kit-template-agy-sh-"$VERSION".zip \
.genreleases/spec-kit-template-agy-ps-"$VERSION".zip \
.genreleases/spec-kit-template-bob-sh-"$VERSION".zip \
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
--title "Spec Kit Templates - $VERSION_NO_V" \ --title "Spec Kit Templates - $VERSION_NO_V" \
--notes-file release_notes.md --notes-file release_notes.md

View File

@@ -1,424 +0,0 @@
#!/usr/bin/env pwsh
#requires -Version 7.0
<#
.SYNOPSIS
Build Spec Kit template release archives for each supported AI assistant and script type.
.DESCRIPTION
create-release-packages.ps1 (workflow-local)
Build Spec Kit template release archives for each supported AI assistant and script type.
.PARAMETER Version
Version string with leading 'v' (e.g., v0.2.0)
.PARAMETER Agents
Comma or space separated subset of agents to build (default: all)
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, q, bob, qoder
.PARAMETER Scripts
Comma or space separated subset of script types to build (default: both)
Valid scripts: sh, ps
.EXAMPLE
.\create-release-packages.ps1 -Version v0.2.0
.EXAMPLE
.\create-release-packages.ps1 -Version v0.2.0 -Agents claude,copilot -Scripts sh
.EXAMPLE
.\create-release-packages.ps1 -Version v0.2.0 -Agents claude -Scripts ps
#>
param(
[Parameter(Mandatory=$true, Position=0)]
[string]$Version,
[Parameter(Mandatory=$false)]
[string]$Agents = "",
[Parameter(Mandatory=$false)]
[string]$Scripts = ""
)
$ErrorActionPreference = "Stop"
# Validate version format
if ($Version -notmatch '^v\d+\.\d+\.\d+$') {
Write-Error "Version must look like v0.0.0"
exit 1
}
Write-Host "Building release packages for $Version"
# Create and use .genreleases directory for all build artifacts
$GenReleasesDir = ".genreleases"
if (Test-Path $GenReleasesDir) {
Remove-Item -Path $GenReleasesDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Path $GenReleasesDir -Force | Out-Null
function Rewrite-Paths {
param([string]$Content)
$Content = $Content -replace '(/?)\bmemory/', '.specify/memory/'
$Content = $Content -replace '(/?)\bscripts/', '.specify/scripts/'
$Content = $Content -replace '(/?)\btemplates/', '.specify/templates/'
return $Content
}
function Generate-Commands {
param(
[string]$Agent,
[string]$Extension,
[string]$ArgFormat,
[string]$OutputDir,
[string]$ScriptVariant
)
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
foreach ($template in $templates) {
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
# Read file content and normalize line endings
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
# Extract description from YAML frontmatter
$description = ""
if ($fileContent -match '(?m)^description:\s*(.+)$') {
$description = $matches[1]
}
# Extract script command from YAML frontmatter
$scriptCommand = ""
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
$scriptCommand = $matches[1]
}
if ([string]::IsNullOrEmpty($scriptCommand)) {
Write-Warning "No script command found for $ScriptVariant in $($template.Name)"
$scriptCommand = "(Missing script command for $ScriptVariant)"
}
# Extract agent_script command from YAML frontmatter if present
$agentScriptCommand = ""
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
$agentScriptCommand = $matches[1].Trim()
}
# Replace {SCRIPT} placeholder with the script command
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
}
# Remove the scripts: and agent_scripts: sections from frontmatter
$lines = $body -split "`n"
$outputLines = @()
$inFrontmatter = $false
$skipScripts = $false
$dashCount = 0
foreach ($line in $lines) {
if ($line -match '^---$') {
$outputLines += $line
$dashCount++
if ($dashCount -eq 1) {
$inFrontmatter = $true
} else {
$inFrontmatter = $false
}
continue
}
if ($inFrontmatter) {
if ($line -match '^(scripts|agent_scripts):$') {
$skipScripts = $true
continue
}
if ($line -match '^[a-zA-Z].*:' -and $skipScripts) {
$skipScripts = $false
}
if ($skipScripts -and $line -match '^\s+') {
continue
}
}
$outputLines += $line
}
$body = $outputLines -join "`n"
# Apply other substitutions
$body = $body -replace '\{ARGS\}', $ArgFormat
$body = $body -replace '__AGENT__', $Agent
$body = Rewrite-Paths -Content $body
# Generate output file based on extension
$outputFile = Join-Path $OutputDir "speckit.$name.$Extension"
switch ($Extension) {
'toml' {
$body = $body -replace '\\', '\\'
$output = "description = `"$description`"`n`nprompt = `"`"`"`n$body`n`"`"`""
Set-Content -Path $outputFile -Value $output -NoNewline
}
'md' {
Set-Content -Path $outputFile -Value $body -NoNewline
}
'agent.md' {
Set-Content -Path $outputFile -Value $body -NoNewline
}
}
}
}
function Generate-CopilotPrompts {
param(
[string]$AgentsDir,
[string]$PromptsDir
)
New-Item -ItemType Directory -Path $PromptsDir -Force | Out-Null
$agentFiles = Get-ChildItem -Path "$AgentsDir/speckit.*.agent.md" -File -ErrorAction SilentlyContinue
foreach ($agentFile in $agentFiles) {
$basename = $agentFile.Name -replace '\.agent\.md$', ''
$promptFile = Join-Path $PromptsDir "$basename.prompt.md"
$content = @"
---
agent: $basename
---
"@
Set-Content -Path $promptFile -Value $content
}
}
function Build-Variant {
param(
[string]$Agent,
[string]$Script
)
$baseDir = Join-Path $GenReleasesDir "sdd-${Agent}-package-${Script}"
Write-Host "Building $Agent ($Script) package..."
New-Item -ItemType Directory -Path $baseDir -Force | Out-Null
# Copy base structure but filter scripts by variant
$specDir = Join-Path $baseDir ".specify"
New-Item -ItemType Directory -Path $specDir -Force | Out-Null
# Copy memory directory
if (Test-Path "memory") {
Copy-Item -Path "memory" -Destination $specDir -Recurse -Force
Write-Host "Copied memory -> .specify"
}
# Only copy the relevant script variant directory
if (Test-Path "scripts") {
$scriptsDestDir = Join-Path $specDir "scripts"
New-Item -ItemType Directory -Path $scriptsDestDir -Force | Out-Null
switch ($Script) {
'sh' {
if (Test-Path "scripts/bash") {
Copy-Item -Path "scripts/bash" -Destination $scriptsDestDir -Recurse -Force
Write-Host "Copied scripts/bash -> .specify/scripts"
}
}
'ps' {
if (Test-Path "scripts/powershell") {
Copy-Item -Path "scripts/powershell" -Destination $scriptsDestDir -Recurse -Force
Write-Host "Copied scripts/powershell -> .specify/scripts"
}
}
}
# Copy any script files that aren't in variant-specific directories
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
}
}
# Copy templates (excluding commands directory and vscode-settings.json)
if (Test-Path "templates") {
$templatesDestDir = Join-Path $specDir "templates"
New-Item -ItemType Directory -Path $templatesDestDir -Force | Out-Null
Get-ChildItem -Path "templates" -Recurse -File | Where-Object {
$_.FullName -notmatch 'templates[/\\]commands[/\\]' -and $_.Name -ne 'vscode-settings.json'
} | ForEach-Object {
$relativePath = $_.FullName.Substring((Resolve-Path "templates").Path.Length + 1)
$destFile = Join-Path $templatesDestDir $relativePath
$destFileDir = Split-Path $destFile -Parent
New-Item -ItemType Directory -Path $destFileDir -Force | Out-Null
Copy-Item -Path $_.FullName -Destination $destFile -Force
}
Write-Host "Copied templates -> .specify/templates"
}
# Generate agent-specific command files
switch ($Agent) {
'claude' {
$cmdDir = Join-Path $baseDir ".claude/commands"
Generate-Commands -Agent 'claude' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'gemini' {
$cmdDir = Join-Path $baseDir ".gemini/commands"
Generate-Commands -Agent 'gemini' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
if (Test-Path "agent_templates/gemini/GEMINI.md") {
Copy-Item -Path "agent_templates/gemini/GEMINI.md" -Destination (Join-Path $baseDir "GEMINI.md")
}
}
'copilot' {
$agentsDir = Join-Path $baseDir ".github/agents"
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
# Generate companion prompt files
$promptsDir = Join-Path $baseDir ".github/prompts"
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
# Create VS Code workspace settings
$vscodeDir = Join-Path $baseDir ".vscode"
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
if (Test-Path "templates/vscode-settings.json") {
Copy-Item -Path "templates/vscode-settings.json" -Destination (Join-Path $vscodeDir "settings.json")
}
}
'cursor-agent' {
$cmdDir = Join-Path $baseDir ".cursor/commands"
Generate-Commands -Agent 'cursor-agent' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'qwen' {
$cmdDir = Join-Path $baseDir ".qwen/commands"
Generate-Commands -Agent 'qwen' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
if (Test-Path "agent_templates/qwen/QWEN.md") {
Copy-Item -Path "agent_templates/qwen/QWEN.md" -Destination (Join-Path $baseDir "QWEN.md")
}
}
'opencode' {
$cmdDir = Join-Path $baseDir ".opencode/command"
Generate-Commands -Agent 'opencode' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'windsurf' {
$cmdDir = Join-Path $baseDir ".windsurf/workflows"
Generate-Commands -Agent 'windsurf' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'codex' {
$cmdDir = Join-Path $baseDir ".codex/prompts"
Generate-Commands -Agent 'codex' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'kilocode' {
$cmdDir = Join-Path $baseDir ".kilocode/workflows"
Generate-Commands -Agent 'kilocode' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'auggie' {
$cmdDir = Join-Path $baseDir ".augment/commands"
Generate-Commands -Agent 'auggie' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'roo' {
$cmdDir = Join-Path $baseDir ".roo/commands"
Generate-Commands -Agent 'roo' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'codebuddy' {
$cmdDir = Join-Path $baseDir ".codebuddy/commands"
Generate-Commands -Agent 'codebuddy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'amp' {
$cmdDir = Join-Path $baseDir ".agents/commands"
Generate-Commands -Agent 'amp' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'q' {
$cmdDir = Join-Path $baseDir ".amazonq/prompts"
Generate-Commands -Agent 'q' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'bob' {
$cmdDir = Join-Path $baseDir ".bob/commands"
Generate-Commands -Agent 'bob' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'qoder' {
$cmdDir = Join-Path $baseDir ".qoder/commands"
Generate-Commands -Agent 'qoder' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
}
# Create zip archive
$zipFile = Join-Path $GenReleasesDir "spec-kit-template-${Agent}-${Script}-${Version}.zip"
Compress-Archive -Path "$baseDir/*" -DestinationPath $zipFile -Force
Write-Host "Created $zipFile"
}
# Define all agents and scripts
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'q', 'bob', 'qoder')
$AllScripts = @('sh', 'ps')
function Normalize-List {
param([string]$Input)
if ([string]::IsNullOrEmpty($Input)) {
return @()
}
# Split by comma or space and remove duplicates while preserving order
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
return $items
}
function Validate-Subset {
param(
[string]$Type,
[string[]]$Allowed,
[string[]]$Items
)
$ok = $true
foreach ($item in $Items) {
if ($item -notin $Allowed) {
Write-Error "Unknown $Type '$item' (allowed: $($Allowed -join ', '))"
$ok = $false
}
}
return $ok
}
# Determine agent list
if (-not [string]::IsNullOrEmpty($Agents)) {
$AgentList = Normalize-List -Input $Agents
if (-not (Validate-Subset -Type 'agent' -Allowed $AllAgents -Items $AgentList)) {
exit 1
}
} else {
$AgentList = $AllAgents
}
# Determine script list
if (-not [string]::IsNullOrEmpty($Scripts)) {
$ScriptList = Normalize-List -Input $Scripts
if (-not (Validate-Subset -Type 'script' -Allowed $AllScripts -Items $ScriptList)) {
exit 1
}
} else {
$ScriptList = $AllScripts
}
Write-Host "Agents: $($AgentList -join ', ')"
Write-Host "Scripts: $($ScriptList -join ', ')"
# Build all variants
foreach ($agent in $AgentList) {
foreach ($script in $ScriptList) {
Build-Variant -Agent $agent -Script $script
}
}
Write-Host "`nArchives in ${GenReleasesDir}:"
Get-ChildItem -Path $GenReleasesDir -Filter "spec-kit-template-*-${Version}.zip" | ForEach-Object {
Write-Host " $($_.Name)"
}

47
.github/workflows/scripts/create-release-packages.sh vendored Executable file → Normal file
View File

@@ -6,7 +6,7 @@ set -euo pipefail
# Usage: .github/workflows/scripts/create-release-packages.sh <version> # Usage: .github/workflows/scripts/create-release-packages.sh <version>
# Version argument should include leading 'v'. # Version argument should include leading 'v'.
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built. # Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex amp shai bob (default: all) # AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex amp (default: all)
# SCRIPTS : space or comma separated subset of: sh ps (default: both) # SCRIPTS : space or comma separated subset of: sh ps (default: both)
# Examples: # Examples:
# AGENTS=claude SCRIPTS=sh $0 v0.2.0 # AGENTS=claude SCRIPTS=sh $0 v0.2.0
@@ -34,8 +34,7 @@ rewrite_paths() {
sed -E \ sed -E \
-e 's@(/?)memory/@.specify/memory/@g' \ -e 's@(/?)memory/@.specify/memory/@g' \
-e 's@(/?)scripts/@.specify/scripts/@g' \ -e 's@(/?)scripts/@.specify/scripts/@g' \
-e 's@(/?)templates/@.specify/templates/@g' \ -e 's@(/?)templates/@.specify/templates/@g'
-e 's@\.specify\.specify/@.specify/@g'
} }
generate_commands() { generate_commands() {
@@ -96,21 +95,21 @@ generate_commands() {
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/speckit.$name.$ext" ;; { echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/speckit.$name.$ext" ;;
md) md)
echo "$body" > "$output_dir/speckit.$name.$ext" ;; echo "$body" > "$output_dir/speckit.$name.$ext" ;;
agent.md) chatmode.md)
echo "$body" > "$output_dir/speckit.$name.$ext" ;; echo "$body" > "$output_dir/speckit.$name.$ext" ;;
esac esac
done done
} }
generate_copilot_prompts() { generate_copilot_prompts() {
local agents_dir=$1 prompts_dir=$2 local chatmodes_dir=$1 prompts_dir=$2
mkdir -p "$prompts_dir" mkdir -p "$prompts_dir"
# Generate a .prompt.md file for each .agent.md file # Generate a .prompt.md file for each .chatmode.md file
for agent_file in "$agents_dir"/speckit.*.agent.md; do for chatmode_file in "$chatmodes_dir"/speckit.*.chatmode.md; do
[[ -f "$agent_file" ]] || continue [[ -f "$chatmode_file" ]] || continue
local basename=$(basename "$agent_file" .agent.md) local basename=$(basename "$chatmode_file" .chatmode.md)
local prompt_file="$prompts_dir/${basename}.prompt.md" local prompt_file="$prompts_dir/${basename}.prompt.md"
# Create prompt file with agent frontmatter # Create prompt file with agent frontmatter
@@ -167,10 +166,10 @@ build_variant() {
generate_commands gemini toml "{{args}}" "$base_dir/.gemini/commands" "$script" generate_commands gemini toml "{{args}}" "$base_dir/.gemini/commands" "$script"
[[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md "$base_dir/GEMINI.md" ;; [[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md "$base_dir/GEMINI.md" ;;
copilot) copilot)
mkdir -p "$base_dir/.github/agents" mkdir -p "$base_dir/.github/chatmodes"
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$script" generate_commands copilot chatmode.md "\$ARGUMENTS" "$base_dir/.github/chatmodes" "$script"
# Generate companion prompt files # Generate companion prompt files
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts" generate_copilot_prompts "$base_dir/.github/chatmodes" "$base_dir/.github/prompts"
# Create VS Code workspace settings # Create VS Code workspace settings
mkdir -p "$base_dir/.vscode" mkdir -p "$base_dir/.vscode"
[[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json" [[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json"
@@ -203,50 +202,38 @@ build_variant() {
codebuddy) codebuddy)
mkdir -p "$base_dir/.codebuddy/commands" mkdir -p "$base_dir/.codebuddy/commands"
generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;; generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;;
qoder)
mkdir -p "$base_dir/.qoder/commands"
generate_commands qoder md "\$ARGUMENTS" "$base_dir/.qoder/commands" "$script" ;;
amp) amp)
mkdir -p "$base_dir/.agents/commands" mkdir -p "$base_dir/.agents/commands"
generate_commands amp md "\$ARGUMENTS" "$base_dir/.agents/commands" "$script" ;; generate_commands amp md "\$ARGUMENTS" "$base_dir/.agents/commands" "$script" ;;
shai)
mkdir -p "$base_dir/.shai/commands"
generate_commands shai md "\$ARGUMENTS" "$base_dir/.shai/commands" "$script" ;;
q) q)
mkdir -p "$base_dir/.amazonq/prompts" mkdir -p "$base_dir/.amazonq/prompts"
generate_commands q md "\$ARGUMENTS" "$base_dir/.amazonq/prompts" "$script" ;; generate_commands q md "\$ARGUMENTS" "$base_dir/.amazonq/prompts" "$script" ;;
agy)
mkdir -p "$base_dir/.agent/workflows"
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/workflows" "$script" ;;
bob)
mkdir -p "$base_dir/.bob/commands"
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
esac esac
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . ) ( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
} }
# Determine agent list # Determine agent list
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai q agy bob qoder) ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp q)
ALL_SCRIPTS=(sh ps) ALL_SCRIPTS=(sh ps)
norm_list() { norm_list() {
# convert comma+space separated -> line separated unique while preserving order of first occurrence # convert comma+space separated -> space separated unique while preserving order of first occurrence
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?"\n":"") $i);out=1}}}END{printf("\n")}' tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i)}}}END{printf("\n")}'
} }
validate_subset() { validate_subset() {
local type=$1; shift; local -n allowed=$1; shift; local items=("$@") local type=$1; shift; local -n allowed=$1; shift; local items=("$@")
local invalid=0 local ok=1
for it in "${items[@]}"; do for it in "${items[@]}"; do
local found=0 local found=0
for a in "${allowed[@]}"; do [[ $it == "$a" ]] && { found=1; break; }; done for a in "${allowed[@]}"; do [[ $it == "$a" ]] && { found=1; break; }; done
if [[ $found -eq 0 ]]; then if [[ $found -eq 0 ]]; then
echo "Error: unknown $type '$it' (allowed: ${allowed[*]})" >&2 echo "Error: unknown $type '$it' (allowed: ${allowed[*]})" >&2
invalid=1 ok=0
fi fi
done done
return $invalid return $ok
} }
if [[ -n ${AGENTS:-} ]]; then if [[ -n ${AGENTS:-} ]]; then

View File

@@ -1,42 +0,0 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
workflow_dispatch: # Allow manual triggering
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
# Days of inactivity before an issue or PR becomes stale
days-before-stale: 150
# Days of inactivity before a stale issue or PR is closed (after being marked stale)
days-before-close: 30
# Stale issue settings
stale-issue-message: 'This issue has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
close-issue-message: 'This issue has been automatically closed due to inactivity (180 days total). If you believe this issue is still relevant, please reopen it or create a new issue.'
stale-issue-label: 'stale'
# Stale PR settings
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
close-pr-message: 'This pull request has been automatically closed due to inactivity (180 days total). If you believe this PR is still relevant, please reopen it or create a new PR.'
stale-pr-label: 'stale'
# Exempt issues and PRs with these labels from being marked as stale
exempt-issue-labels: 'pinned,security'
exempt-pr-labels: 'pinned,security'
# Only issues or PRs with all of these labels are checked
# Leave empty to check all issues and PRs
any-of-labels: ''
# Operations per run (helps avoid rate limits)
operations-per-run: 100

7
.gitignore vendored
View File

@@ -32,7 +32,6 @@ env/
*.swp *.swp
*.swo *.swo
.DS_Store .DS_Store
*.tmp
# Project specific # Project specific
*.log *.log
@@ -44,9 +43,3 @@ env/
.genreleases/ .genreleases/
*.zip *.zip
sdd-*/ sdd-*/
docs/dev
# Extension system
.specify/extensions/.cache/
.specify/extensions/.backup/
.specify/extensions/*/local-config.yml

View File

@@ -19,9 +19,7 @@
}, },
"MD050": { "MD050": {
"style": "asterisk" "style": "asterisk"
}, }
"MD036": false,
"MD060": false
}, },
"ignores": [ "ignores": [
".genreleases/" ".genreleases/"

View File

@@ -29,25 +29,22 @@ Specify supports multiple AI agents by generating agent-specific command files a
### Current Supported Agents ### Current Supported Agents
| Agent | Directory | Format | CLI Tool | Description | | Agent | Directory | Format | CLI Tool | Description |
| -------------------------- | ---------------------- | -------- | --------------- | --------------------------- | |-------|-----------|---------|----------|-------------|
| **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code CLI | | **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code CLI |
| **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini CLI | | **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini CLI |
| **GitHub Copilot** | `.github/agents/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code | | **GitHub Copilot** | `.github/chatmodes/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code |
| **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI | | **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI |
| **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI | | **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI |
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI | | **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
| **Codex CLI** | `.codex/commands/` | Markdown | `codex` | Codex CLI | | **Codex CLI** | `.codex/commands/` | Markdown | `codex` | Codex CLI |
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows | | **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
| **Kilo Code** | `.kilocode/rules/` | Markdown | N/A (IDE-based) | Kilo Code IDE | | **Kilo Code** | `.kilocode/rules/` | Markdown | N/A (IDE-based) | Kilo Code IDE |
| **Auggie CLI** | `.augment/rules/` | Markdown | `auggie` | Auggie CLI | | **Auggie CLI** | `.augment/rules/` | Markdown | `auggie` | Auggie CLI |
| **Roo Code** | `.roo/rules/` | Markdown | N/A (IDE-based) | Roo Code IDE | | **Roo Code** | `.roo/rules/` | Markdown | N/A (IDE-based) | Roo Code IDE |
| **CodeBuddy CLI** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy CLI | | **CodeBuddy CLI** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy CLI |
| **Qoder CLI** | `.qoder/commands/` | Markdown | `qoder` | Qoder CLI | | **Amazon Q Developer CLI** | `.amazonq/prompts/` | Markdown | `q` | Amazon Q Developer CLI |
| **Amazon Q Developer CLI** | `.amazonq/prompts/` | Markdown | `q` | Amazon Q Developer CLI | | **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
### Step-by-Step Integration Guide ### Step-by-Step Integration Guide
@@ -313,9 +310,7 @@ Require a command-line tool to be installed:
- **opencode**: `opencode` CLI - **opencode**: `opencode` CLI
- **Amazon Q Developer CLI**: `q` CLI - **Amazon Q Developer CLI**: `q` CLI
- **CodeBuddy CLI**: `codebuddy` CLI - **CodeBuddy CLI**: `codebuddy` CLI
- **Qoder CLI**: `qoder` CLI
- **Amp**: `amp` CLI - **Amp**: `amp` CLI
- **SHAI**: `shai` CLI
### IDE-Based Agents ### IDE-Based Agents
@@ -323,13 +318,12 @@ Work within integrated development environments:
- **GitHub Copilot**: Built into VS Code/compatible editors - **GitHub Copilot**: Built into VS Code/compatible editors
- **Windsurf**: Built into Windsurf IDE - **Windsurf**: Built into Windsurf IDE
- **IBM Bob**: Built into IBM Bob IDE
## Command File Formats ## Command File Formats
### Markdown Format ### Markdown Format
Used by: Claude, Cursor, opencode, Windsurf, Amazon Q Developer, Amp, SHAI, IBM Bob Used by: Claude, Cursor, opencode, Windsurf, Amazon Q Developer, Amp
**Standard format:** **Standard format:**
@@ -368,7 +362,7 @@ Command content with {SCRIPT} and {{args}} placeholders.
- **CLI agents**: Usually `.<agent-name>/commands/` - **CLI agents**: Usually `.<agent-name>/commands/`
- **IDE agents**: Follow IDE-specific patterns: - **IDE agents**: Follow IDE-specific patterns:
- Copilot: `.github/agents/` - Copilot: `.github/chatmodes/`
- Cursor: `.cursor/commands/` - Cursor: `.cursor/commands/`
- Windsurf: `.windsurf/workflows/` - Windsurf: `.windsurf/workflows/`

View File

@@ -7,128 +7,6 @@ All notable changes to the Specify CLI and templates are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] - 2026-01-28
### Added
- **Extension System**: Introduced modular extension architecture for Spec Kit
- Extensions are self-contained packages that add commands and functionality without bloating core
- Extension manifest schema (`extension.yml`) with validation
- Extension registry (`.specify/extensions/.registry`) for tracking installed extensions
- Extension manager module (`src/specify_cli/extensions.py`) for installation/removal
- New CLI commands:
- `specify extension list` - List installed extensions
- `specify extension add` - Install extension from local directory or URL
- `specify extension remove` - Uninstall extension
- `specify extension search` - Search extension catalog
- `specify extension info` - Show detailed extension information
- Semantic versioning compatibility checks
- Support for extension configuration files
- Command registration system for AI agents (Claude support initially)
- Added dependencies: `pyyaml>=6.0`, `packaging>=23.0`
- **Extension Catalog**: Extension discovery and distribution system
- Central catalog (`extensions/catalog.json`) for published extensions
- Extension catalog manager (`ExtensionCatalog` class) with:
- Catalog fetching from GitHub
- 1-hour local caching for performance
- Search by query, tag, author, or verification status
- Extension info retrieval
- Catalog cache stored in `.specify/extensions/.cache/`
- Search and info commands with rich console output
- Added 9 catalog-specific unit tests (100% pass rate)
- **Jira Extension**: First official extension for Jira integration
- Extension ID: `jira`
- Version: 1.0.0
- 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
- Comprehensive documentation (README, usage guide, examples)
- MIT licensed
- **Hook System**: Extension lifecycle hooks for automation
- `HookExecutor` class for managing extension hooks
- Hooks registered in `.specify/extensions.yml`
- Hook registration during extension installation
- Hook unregistration during extension removal
- Support for optional and mandatory hooks
- Hook execution messages for AI agent integration
- Condition support for conditional hook execution (placeholder)
- **Extension Management**: Advanced extension management commands
- `specify extension update` - Check and update extensions to latest version
- `specify extension enable` - Enable a disabled extension
- `specify extension disable` - Disable extension without removing it
- Version comparison with catalog
- Update notifications
- Preserve configuration during updates
- **Multi-Agent Support**: Extensions now work with all supported AI agents (Phase 6)
- Automatic detection and registration for all agents in project
- Support for 16+ AI agents (Claude, Gemini, Copilot, Cursor, Qwen, and more)
- Agent-specific command formats (Markdown and TOML)
- Automatic argument placeholder conversion ($ARGUMENTS → {{args}})
- Commands registered for all detected agents during installation
- Multi-agent command unregistration on extension removal
- `CommandRegistrar.register_commands_for_agent()` method
- `CommandRegistrar.register_commands_for_all_agents()` method
- **Configuration Layers**: Full configuration cascade system (Phase 6)
- **Layer 1**: Defaults from extension manifest (`extension.yml`)
- **Layer 2**: Project config (`.specify/extensions/{ext-id}/{ext-id}-config.yml`)
- **Layer 3**: Local config (`.specify/extensions/{ext-id}/local-config.yml`, gitignored)
- **Layer 4**: Environment variables (`SPECKIT_{EXT_ID}_{KEY}` pattern)
- Recursive config merging with proper precedence
- `ConfigManager` class for programmatic config access
- `get_config()`, `get_value()`, `has_value()` methods
- Support for nested configuration paths with dot-notation
- **Hook Condition Evaluation**: Smart hook execution based on runtime conditions (Phase 6)
- Config conditions: `config.key.path is set`, `config.key == 'value'`, `config.key != 'value'`
- Environment conditions: `env.VAR is set`, `env.VAR == 'value'`, `env.VAR != 'value'`
- Automatic filtering of hooks based on condition evaluation
- Safe fallback behavior on evaluation errors
- Case-insensitive pattern matching
- **Hook Integration**: Agent-level hook checking and execution (Phase 6)
- `check_hooks_for_event()` method for AI agents to query hooks after core commands
- Condition-aware hook filtering before execution
- `enable_hooks()` and `disable_hooks()` methods per extension
- Formatted hook messages for agent display
- `execute_hook()` method for hook execution information
- **Documentation Suite**: Comprehensive documentation for users and developers
- **EXTENSION-USER-GUIDE.md**: Complete user guide with installation, usage, configuration, and troubleshooting
- **EXTENSION-API-REFERENCE.md**: Technical API reference with manifest schema, Python API, and CLI commands
- **EXTENSION-PUBLISHING-GUIDE.md**: Publishing guide for extension authors
- **RFC-EXTENSION-SYSTEM.md**: Extension architecture design document
- **Extension Template**: Starter template in `extensions/template/` for creating new extensions
- Fully commented `extension.yml` manifest template
- Example command file with detailed explanations
- Configuration template with all options
- Complete project structure (README, LICENSE, CHANGELOG, .gitignore)
- EXAMPLE-README.md showing final documentation format
- **Unit Tests**: Comprehensive test suite with 39 tests covering all extension system components
- Test coverage: 83% of extension module code
- Test dependencies: `pytest>=7.0`, `pytest-cov>=4.0`
- Configured pytest in `pyproject.toml`
### Changed
- Version bumped to 0.1.0 (minor release for new feature)
## [0.0.22] - 2025-11-07
- Support for VS Code/Copilot agents, and moving away from prompts to proper agents with hand-offs.
- Move to use `AGENTS.md` for Copilot workloads, since it's already supported out-of-the-box.
- Adds support for the version command. ([#486](https://github.com/github/spec-kit/issues/486))
- Fixes potential bug with the `create-new-feature.ps1` script that ignores existing feature branches when determining next feature number ([#975](https://github.com/github/spec-kit/issues/975))
- Add graceful fallback and logging for GitHub API rate-limiting during template fetch ([#970](https://github.com/github/spec-kit/issues/970))
## [0.0.21] - 2025-10-21 ## [0.0.21] - 2025-10-21
- Fixes [#975](https://github.com/github/spec-kit/issues/975) (thank you [@fgalarraga](https://github.com/fgalarraga)). - Fixes [#975](https://github.com/github/spec-kit/issues/975) (thank you [@fgalarraga](https://github.com/fgalarraga)).

View File

@@ -23,7 +23,7 @@ include:
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or - The use of sexualized language or imagery and unwelcome sexual attention or
advances advances
- Trolling, insulting/derogatory comments, and personal or political attacks - Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment - Public or private harassment
- Publishing others' private information, such as a physical or electronic - Publishing others' private information, such as a physical or electronic

View File

@@ -32,8 +32,8 @@ On [GitHub Codespaces](https://github.com/features/codespaces) it's even simpler
## Submitting a pull request ## Submitting a pull request
> [!NOTE] >[!NOTE]
> If your pull request introduces a large change that materially impacts the work of the CLI or the rest of the repository (e.g., you're introducing new templates, arguments, or otherwise major changes), make sure that it was **discussed and agreed upon** by the project maintainers. Pull requests with large changes that did not have a prior conversation and agreement will be closed. >If your pull request introduces a large change that materially impacts the work of the CLI or the rest of the repository (e.g., you're introducing new templates, arguments, or otherwise major changes), make sure that it was **discussed and agreed upon** by the project maintainers. Pull requests with large changes that did not have a prior conversation and agreement will be closed.
1. Fork and clone the repository 1. Fork and clone the repository
1. Configure and install the dependencies: `uv sync` 1. Configure and install the dependencies: `uv sync`
@@ -71,13 +71,13 @@ To test your templates, commands, and other changes locally, follow these steps:
Run the following command to generate the local packages: Run the following command to generate the local packages:
```bash ```
./.github/workflows/scripts/create-release-packages.sh v1.0.0 ./.github/workflows/scripts/create-release-packages.sh v1.0.0
``` ```
2. **Copy the relevant package to your test project** 2. **Copy the relevant package to your test project**
```bash ```
cp -r .genreleases/sdd-copilot-package-sh/. <path-to-test-project>/ cp -r .genreleases/sdd-copilot-package-sh/. <path-to-test-project>/
``` ```

152
README.md
View File

@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="./media/logo_large.webp" alt="Spec Kit Logo" width="200" height="200"/> <img src="./media/logo_small.webp" alt="Spec Kit Logo"/>
<h1>🌱 Spec Kit</h1> <h1>🌱 Spec Kit</h1>
<h3><em>Build high-quality software faster.</em></h3> <h3><em>Build high-quality software faster.</em></h3>
</div> </div>
@@ -31,6 +31,7 @@
- [📖 Learn More](#-learn-more) - [📖 Learn More](#-learn-more)
- [📋 Detailed Process](#-detailed-process) - [📋 Detailed Process](#-detailed-process)
- [🔍 Troubleshooting](#-troubleshooting) - [🔍 Troubleshooting](#-troubleshooting)
- [👥 Maintainers](#-maintainers)
- [💬 Support](#-support) - [💬 Support](#-support)
- [🙏 Acknowledgements](#-acknowledgements) - [🙏 Acknowledgements](#-acknowledgements)
- [📄 License](#-license) - [📄 License](#-license)
@@ -56,19 +57,11 @@ uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
Then use the tool directly: Then use the tool directly:
```bash ```bash
# Create new project
specify init <PROJECT_NAME> specify init <PROJECT_NAME>
# Or initialize in existing project
specify init . --ai claude
# or
specify init --here --ai claude
# Check installed tools
specify check specify check
``` ```
To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed instructions. Quick upgrade: To upgrade specify run:
```bash ```bash
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
@@ -141,27 +134,22 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
## 🤖 Supported AI Agents ## 🤖 Supported AI Agents
| Agent | Support | Notes | | Agent | Support | Notes |
| ------------------------------------------------------------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | |-----------------------------------------------------------|---------|---------------------------------------------------|
| [Qoder CLI](https://qoder.com/cli) | ✅ | | | [Claude Code](https://www.anthropic.com/claude-code) | ✅ | |
| [Amazon Q Developer CLI](https://aws.amazon.com/developer/learning/q-developer-cli/) | ⚠️ | Amazon Q Developer CLI [does not support](https://github.com/aws/amazon-q-developer-cli/issues/3064) custom arguments for slash commands. | | [GitHub Copilot](https://code.visualstudio.com/) | ✅ | |
| [Amp](https://ampcode.com/) | ✅ | | | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | |
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | | | [Cursor](https://cursor.sh/) | ✅ | |
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | | | [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ | |
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | | | [opencode](https://opencode.ai/) | ✅ | |
| [Codex CLI](https://github.com/openai/codex) | ✅ | | | [Windsurf](https://windsurf.com/) | ✅ | |
| [Cursor](https://cursor.sh/) | ✅ | | | [Kilo Code](https://github.com/Kilo-Org/kilocode) | ✅ | |
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | | | [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | |
| [GitHub Copilot](https://code.visualstudio.com/) | ✅ | | | [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
| [IBM Bob](https://www.ibm.com/products/bob) | ✅ | IDE-based agent with slash command support | | [Roo Code](https://roocode.com/) | ✅ | |
| [Jules](https://jules.google.com/) | ✅ | | | [Codex CLI](https://github.com/openai/codex) | ✅ | |
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | ✅ | | | [Amazon Q Developer CLI](https://aws.amazon.com/developer/learning/q-developer-cli/) | ⚠️ | Amazon Q Developer CLI [does not support](https://github.com/aws/amazon-q-developer-cli/issues/3064) custom arguments for slash commands. |
| [opencode](https://opencode.ai/) | ✅ | | | [Amp](https://ampcode.com/) | ✅ | |
| [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ | |
| [Roo Code](https://roocode.com/) | ✅ | |
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
| [Windsurf](https://windsurf.com/) | ✅ | |
| [Antigravity (agy)](https://agy.ai/) | ✅ | |
## 🔧 Specify CLI Reference ## 🔧 Specify CLI Reference
@@ -169,25 +157,25 @@ The `specify` command supports the following options:
### Commands ### Commands
| Command | Description | | Command | Description |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | |-------------|----------------------------------------------------------------|
| `init` | Initialize a new Specify project from the latest template | | `init` | Initialize a new Specify project from the latest template |
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `shai`, `qoder`) | | `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`) |
### `specify init` Arguments & Options ### `specify init` Arguments & Options
| Argument/Option | Type | Description | | Argument/Option | Type | Description |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |------------------------|----------|------------------------------------------------------------------------------|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) | | `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `q`, `agy`, `bob`, or `qoder` | | `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, or `q` |
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) | | `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code | | `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
| `--no-git` | Flag | Skip git repository initialization | | `--no-git` | Flag | Skip git repository initialization |
| `--here` | Flag | Initialize project in the current directory instead of creating a new one | | `--here` | Flag | Initialize project in the current directory instead of creating a new one |
| `--force` | Flag | Force merge/overwrite when initializing in current directory (skip confirmation) | | `--force` | Flag | Force merge/overwrite when initializing in current directory (skip confirmation) |
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) | | `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
| `--debug` | Flag | Enable detailed debug output for troubleshooting | | `--debug` | Flag | Enable detailed debug output for troubleshooting |
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) | | `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
### Examples ### Examples
@@ -201,21 +189,12 @@ specify init my-project --ai claude
# Initialize with Cursor support # Initialize with Cursor support
specify init my-project --ai cursor-agent specify init my-project --ai cursor-agent
# Initialize with Qoder support
specify init my-project --ai qoder
# Initialize with Windsurf support # Initialize with Windsurf support
specify init my-project --ai windsurf specify init my-project --ai windsurf
# Initialize with Amp support # Initialize with Amp support
specify init my-project --ai amp specify init my-project --ai amp
# Initialize with SHAI support
specify init my-project --ai shai
# Initialize with IBM Bob support
specify init my-project --ai bob
# Initialize with PowerShell scripts (Windows/cross-platform) # Initialize with PowerShell scripts (Windows/cross-platform)
specify init my-project --ai copilot --script ps specify init my-project --ai copilot --script ps
@@ -250,29 +229,29 @@ After running `specify init`, your AI coding agent will have access to these sla
Essential commands for the Spec-Driven Development workflow: Essential commands for the Spec-Driven Development workflow:
| Command | Description | | Command | Description |
| ----------------------- | ------------------------------------------------------------------------ | |--------------------------|-----------------------------------------------------------------------|
| `/speckit.constitution` | Create or update project governing principles and development guidelines | | `/speckit.constitution` | Create or update project governing principles and development guidelines |
| `/speckit.specify` | Define what you want to build (requirements and user stories) | | `/speckit.specify` | Define what you want to build (requirements and user stories) |
| `/speckit.plan` | Create technical implementation plans with your chosen tech stack | | `/speckit.plan` | Create technical implementation plans with your chosen tech stack |
| `/speckit.tasks` | Generate actionable task lists for implementation | | `/speckit.tasks` | Generate actionable task lists for implementation |
| `/speckit.implement` | Execute all tasks to build the feature according to the plan | | `/speckit.implement` | Execute all tasks to build the feature according to the plan |
#### Optional Commands #### Optional Commands
Additional commands for enhanced quality and validation: Additional commands for enhanced quality and validation:
| Command | Description | | Command | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | |----------------------|-----------------------------------------------------------------------|
| `/speckit.clarify` | Clarify underspecified areas (recommended before `/speckit.plan`; formerly `/quizme`) | | `/speckit.clarify` | Clarify underspecified areas (recommended before `/speckit.plan`; formerly `/quizme`) |
| `/speckit.analyze` | Cross-artifact consistency & coverage analysis (run after `/speckit.tasks`, before `/speckit.implement`) | | `/speckit.analyze` | Cross-artifact consistency & coverage analysis (run after `/speckit.tasks`, before `/speckit.implement`) |
| `/speckit.checklist` | Generate custom quality checklists that validate requirements completeness, clarity, and consistency (like "unit tests for English") | | `/speckit.checklist` | Generate custom quality checklists that validate requirements completeness, clarity, and consistency (like "unit tests for English") |
### Environment Variables ### Environment Variables
| Variable | Description | | Variable | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |------------------|------------------------------------------------------------------------------------------------|
| `SPECIFY_FEATURE` | Override feature detection for non-Git repositories. Set to the feature directory name (e.g., `001-photo-albums`) to work on a specific feature when not using Git branches.<br/>\*\*Must be set in the context of the agent you're working with prior to using `/speckit.plan` or follow-up commands. | | `SPECIFY_FEATURE` | Override feature detection for non-Git repositories. Set to the feature directory name (e.g., `001-photo-albums`) to work on a specific feature when not using Git branches.<br/>**Must be set in the context of the agent you're working with prior to using `/speckit.plan` or follow-up commands. |
## 📚 Core Philosophy ## 📚 Core Philosophy
@@ -285,11 +264,11 @@ Spec-Driven Development is a structured process that emphasizes:
## 🌟 Development Phases ## 🌟 Development Phases
| Phase | Focus | Key Activities | | Phase | Focus | Key Activities |
| ---------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |-------|-------|----------------|
| **0-to-1 Development** ("Greenfield") | Generate from scratch | <ul><li>Start with high-level requirements</li><li>Generate specifications</li><li>Plan implementation steps</li><li>Build production-ready applications</li></ul> | | **0-to-1 Development** ("Greenfield") | Generate from scratch | <ul><li>Start with high-level requirements</li><li>Generate specifications</li><li>Plan implementation steps</li><li>Build production-ready applications</li></ul> |
| **Creative Exploration** | Parallel implementations | <ul><li>Explore diverse solutions</li><li>Support multiple technology stacks & architectures</li><li>Experiment with UX patterns</li></ul> | | **Creative Exploration** | Parallel implementations | <ul><li>Explore diverse solutions</li><li>Support multiple technology stacks & architectures</li><li>Experiment with UX patterns</li></ul> |
| **Iterative Enhancement** ("Brownfield") | Brownfield modernization | <ul><li>Add features iteratively</li><li>Modernize legacy systems</li><li>Adapt processes</li></ul> | | **Iterative Enhancement** ("Brownfield") | Brownfield modernization | <ul><li>Add features iteratively</li><li>Modernize legacy systems</li><li>Adapt processes</li></ul> |
## 🎯 Experimental Goals ## 🎯 Experimental Goals
@@ -381,7 +360,7 @@ specify init . --force --ai claude
specify init --here --force --ai claude specify init --here --force --ai claude
``` ```
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, or Amazon Q Developer CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command: The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, or Amazon Q Developer CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
```bash ```bash
specify init <project_name> --ai claude --ignore-agent-tools specify init <project_name> --ai claude --ignore-agent-tools
@@ -407,8 +386,8 @@ This step creates or updates the `.specify/memory/constitution.md` file with you
With your project principles established, you can now create the functional specifications. Use the `/speckit.specify` command and then provide the concrete requirements for the project you want to develop. With your project principles established, you can now create the functional specifications. Use the `/speckit.specify` command and then provide the concrete requirements for the project you want to develop.
> [!IMPORTANT] >[!IMPORTANT]
> Be as explicit as possible about *what* you are trying to build and *why*. **Do not focus on the tech stack at this point**. >Be as explicit as possible about *what* you are trying to build and *why*. **Do not focus on the tech stack at this point**.
An example prompt: An example prompt:
@@ -551,8 +530,8 @@ researching .NET Aspire in general and I don't think that's gonna do much for us
That's way too untargeted research. The research needs to help you solve a specific targeted question. That's way too untargeted research. The research needs to help you solve a specific targeted question.
``` ```
> [!NOTE] >[!NOTE]
> Claude Code might be over-eager and add components that you did not ask for. Ask it to clarify the rationale and the source of the change. >Claude Code might be over-eager and add components that you did not ask for. Ask it to clarify the rationale and the source of the change.
### **STEP 5:** Have Claude Code validate the plan ### **STEP 5:** Have Claude Code validate the plan
@@ -570,8 +549,8 @@ This helps refine the implementation plan and helps you avoid potential blind sp
You can also ask Claude Code (if you have the [GitHub CLI](https://docs.github.com/en/github-cli/github-cli) installed) to go ahead and create a pull request from your current branch to `main` with a detailed description, to make sure that the effort is properly tracked. You can also ask Claude Code (if you have the [GitHub CLI](https://docs.github.com/en/github-cli/github-cli) installed) to go ahead and create a pull request from your current branch to `main` with a detailed description, to make sure that the effort is properly tracked.
> [!NOTE] >[!NOTE]
> Before you have the agent implement it, it's also worth prompting Claude Code to cross-check the details to see if there are any over-engineered pieces (remember - it can be over-eager). If over-engineered components or decisions exist, you can ask Claude Code to resolve them. Ensure that Claude Code follows the [constitution](base/memory/constitution.md) as the foundational piece that it must adhere to when establishing the plan. >Before you have the agent implement it, it's also worth prompting Claude Code to cross-check the details to see if there are any over-engineered pieces (remember - it can be over-eager). If over-engineered components or decisions exist, you can ask Claude Code to resolve them. Ensure that Claude Code follows the [constitution](base/memory/constitution.md) as the foundational piece that it must adhere to when establishing the plan.
### **STEP 6:** Generate task breakdown with /speckit.tasks ### **STEP 6:** Generate task breakdown with /speckit.tasks
@@ -608,8 +587,8 @@ The `/speckit.implement` command will:
- Follow the TDD approach defined in your task plan - Follow the TDD approach defined in your task plan
- Provide progress updates and handle errors appropriately - Provide progress updates and handle errors appropriately
> [!IMPORTANT] >[!IMPORTANT]
> The AI agent will execute local CLI commands (such as `dotnet`, `npm`, etc.) - make sure you have the required tools installed on your machine. >The AI agent will execute local CLI commands (such as `dotnet`, `npm`, etc.) - make sure you have the required tools installed on your machine.
Once the implementation is complete, test the application and resolve any runtime errors that may not be visible in CLI logs (e.g., browser console errors). You can copy and paste such errors back to your AI agent for resolution. Once the implementation is complete, test the application and resolve any runtime errors that may not be visible in CLI logs (e.g., browser console errors). You can copy and paste such errors back to your AI agent for resolution.
@@ -636,6 +615,11 @@ echo "Cleaning up..."
rm gcm-linux_amd64.2.6.1.deb rm gcm-linux_amd64.2.6.1.deb
``` ```
## 👥 Maintainers
- Den Delimarsky ([@localden](https://github.com/localden))
- John Lam ([@jflam](https://github.com/jflam))
## 💬 Support ## 💬 Support
For support, please open a [GitHub issue](https://github.com/github/spec-kit/issues/new). We welcome bug reports, feature requests, and questions about using Spec-Driven Development. For support, please open a [GitHub issue](https://github.com/github/spec-kit/issues/new). We welcome bug reports, feature requests, and questions about using Spec-Driven Development.

View File

@@ -12,7 +12,6 @@ Spec-Driven Development **flips the script** on traditional software development
- [Installation Guide](installation.md) - [Installation Guide](installation.md)
- [Quick Start Guide](quickstart.md) - [Quick Start Guide](quickstart.md)
- [Upgrade Guide](upgrade.md)
- [Local Development](local-development.md) - [Local Development](local-development.md)
## Core Philosophy ## Core Philosophy

View File

@@ -2,24 +2,16 @@
This guide will help you get started with Spec-Driven Development using Spec Kit. This guide will help you get started with Spec-Driven Development using Spec Kit.
> [!NOTE] > NEW: All automation scripts now provide both Bash (`.sh`) and PowerShell (`.ps1`) variants. The `specify` CLI auto-selects based on OS unless you pass `--script sh|ps`.
> All automation scripts now provide both Bash (`.sh`) and PowerShell (`.ps1`) variants. The `specify` CLI auto-selects based on OS unless you pass `--script sh|ps`.
## The 6-Step Process ## The 4-Step Process
> [!TIP] ### 1. Install Specify
> **Context Awareness**: Spec Kit commands automatically detect the active feature based on your current Git branch (e.g., `001-feature-name`). To switch between different specifications, simply switch Git branches.
### Step 1: Install Specify Initialize your project depending on the coding agent you're using:
**In your terminal**, run the `specify` CLI command to initialize your project:
```bash ```bash
# Create a new project directory
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
# OR initialize in the current directory
uvx --from git+https://github.com/github/spec-kit.git specify init .
``` ```
Pick script type explicitly (optional): Pick script type explicitly (optional):
@@ -29,71 +21,31 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script sh # Force POSIX shell uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script sh # Force POSIX shell
``` ```
### Step 2: Define Your Constitution ### 2. Create the Spec
**In your AI Agent's chat interface**, use the `/speckit.constitution` slash command to establish the core rules and principles for your project. You should provide your project's specific principles as arguments. Use the `/speckit.specify` command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
```markdown ```bash
/speckit.constitution This project follows a "Library-First" approach. All features must be implemented as standalone libraries first. We use TDD strictly. We prefer functional programming patterns.
```
### Step 3: Create the Spec
**In the chat**, use the `/speckit.specify` slash command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
```markdown
/speckit.specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface. /speckit.specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
``` ```
### Step 4: Refine the Spec ### 3. Create a Technical Implementation Plan
**In the chat**, use the `/speckit.clarify` slash command to identify and resolve ambiguities in your specification. You can provide specific focus areas as arguments. Use the `/speckit.plan` command to provide your tech stack and architecture choices.
```bash ```bash
/speckit.clarify Focus on security and performance requirements.
```
### Step 5: Create a Technical Implementation Plan
**In the chat**, use the `/speckit.plan` slash command to provide your tech stack and architecture choices.
```markdown
/speckit.plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database. /speckit.plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
``` ```
### Step 6: Break Down and Implement ### 4. Break Down and Implement
**In the chat**, use the `/speckit.tasks` slash command to create an actionable task list. Use `/speckit.tasks` to create an actionable task list, then ask your agent to implement the feature.
```markdown
/speckit.tasks
```
Optionally, validate the plan with `/speckit.analyze`:
```markdown
/speckit.analyze
```
Then, use the `/speckit.implement` slash command to execute the plan.
```markdown
/speckit.implement
```
## Detailed Example: Building Taskify ## Detailed Example: Building Taskify
Here's a complete example of building a team productivity platform: Here's a complete example of building a team productivity platform:
### Step 1: Define Constitution ### Step 1: Define Requirements with `/speckit.specify`
Initialize the project's constitution to set ground rules:
```markdown
/speckit.constitution Taskify is a "Security-First" application. All user inputs must be validated. We use a microservices architecture. Code must be fully documented.
```
### Step 2: Define Requirements with `/speckit.specify`
```text ```text
Develop Taskify, a team productivity platform. It should allow users to create projects, add team members, Develop Taskify, a team productivity platform. It should allow users to create projects, add team members,
@@ -102,51 +54,58 @@ let's call it "Create Taskify," let's have multiple users but the users will be
I want five users in two different categories, one product manager and four engineers. Let's create three I want five users in two different categories, one product manager and four engineers. Let's create three
different sample projects. Let's have the standard Kanban columns for the status of each task, such as "To Do," different sample projects. Let's have the standard Kanban columns for the status of each task, such as "To Do,"
"In Progress," "In Review," and "Done." There will be no login for this application as this is just the very "In Progress," "In Review," and "Done." There will be no login for this application as this is just the very
first testing thing to ensure that our basic features are set up. first testing thing to ensure that our basic features are set up. For each task in the UI for a task card,
you should be able to change the current status of the task between the different columns in the Kanban work board.
You should be able to leave an unlimited number of comments for a particular card. You should be able to, from that task
card, assign one of the valid users. When you first launch Taskify, it's going to give you a list of the five users to pick
from. There will be no password required. When you click on a user, you go into the main view, which displays the list of
projects. When you click on a project, you open the Kanban board for that project. You're going to see the columns.
You'll be able to drag and drop cards back and forth between different columns. You will see any cards that are
assigned to you, the currently logged in user, in a different color from all the other ones, so you can quickly
see yours. You can edit any comments that you make, but you can't edit comments that other people made. You can
delete any comments that you made, but you can't delete comments anybody else made.
``` ```
### Step 3: Refine the Specification ### Step 2: Refine the Specification
Use the `/speckit.clarify` command to interactively resolve any ambiguities in your specification. You can also provide specific details you want to ensure are included. After the initial specification is created, clarify any missing requirements:
```bash ```text
/speckit.clarify I want to clarify the task card details. For each task in the UI for a task card, you should be able to change the current status of the task between the different columns in the Kanban work board. You should be able to leave an unlimited number of comments for a particular card. You should be able to, from that task card, assign one of the valid users. For each sample project or project that you create there should be a variable number of tasks between 5 and 15
tasks for each one randomly distributed into different states of completion. Make sure that there's at least
one task in each stage of completion.
``` ```
You can continue to refine the spec with more details using `/speckit.clarify`: Also validate the specification checklist:
```bash ```text
/speckit.clarify When you first launch Taskify, it's going to give you a list of the five users to pick from. There will be no password required. When you click on a user, you go into the main view, which displays the list of projects. When you click on a project, you open the Kanban board for that project. You're going to see the columns. You'll be able to drag and drop cards back and forth between different columns. You will see any cards that are assigned to you, the currently logged in user, in a different color from all the other ones, so you can quickly see yours. You can edit any comments that you make, but you can't edit comments that other people made. You can delete any comments that you made, but you can't delete comments anybody else made. Read the review and acceptance checklist, and check off each item in the checklist if the feature spec meets the criteria. Leave it empty if it does not.
``` ```
### Step 4: Validate the Spec ### Step 3: Generate Technical Plan with `/speckit.plan`
Validate the specification checklist using the `/speckit.checklist` command:
```bash
/speckit.checklist
```
### Step 5: Generate Technical Plan with `/speckit.plan`
Be specific about your tech stack and technical requirements: Be specific about your tech stack and technical requirements:
```bash ```text
/speckit.plan We are going to generate this using .NET Aspire, using Postgres as the database. The frontend should use Blazor server with drag-and-drop task boards, real-time updates. There should be a REST API created with a projects API, tasks API, and a notifications API. We are going to generate this using .NET Aspire, using Postgres as the database. The frontend should use
Blazor server with drag-and-drop task boards, real-time updates. There should be a REST API created with a projects API,
tasks API, and a notifications API.
``` ```
### Step 6: Validate and Implement ### Step 4: Validate and Implement
Have your AI agent audit the implementation plan using `/speckit.analyze`: Have your AI agent audit the implementation plan:
```bash ```text
/speckit.analyze Now I want you to go and audit the implementation plan and the implementation detail files.
Read through it with an eye on determining whether or not there is a sequence of tasks that you need
to be doing that are obvious from reading this. Because I don't know if there's enough here.
``` ```
Finally, implement the solution: Finally, implement the solution:
```bash ```text
/speckit.implement implement specs/002-create-taskify/plan.md
``` ```
## Key Principles ## Key Principles
@@ -159,6 +118,6 @@ Finally, implement the solution:
## Next Steps ## Next Steps
- Read the [complete methodology](../spec-driven.md) for in-depth guidance - Read the complete methodology for in-depth guidance
- Check out [more examples](../templates) in the repository - Check out more examples in the repository
- Explore the [source code on GitHub](https://github.com/github/spec-kit) - Explore the source code on GitHub

View File

@@ -9,11 +9,10 @@
href: installation.md href: installation.md
- name: Quick Start - name: Quick Start
href: quickstart.md href: quickstart.md
- name: Upgrade
href: upgrade.md
# Development workflows # Development workflows
- name: Development - name: Development
items: items:
- name: Local Development - name: Local Development
href: local-development.md href: local-development.md

View File

@@ -1,444 +0,0 @@
# Upgrade Guide
> You have Spec Kit installed and want to upgrade to the latest version to get new features, bug fixes, or updated slash commands. This guide covers both upgrading the CLI tool and updating your project files.
---
## Quick Reference
| What to Upgrade | Command | When to Use |
|----------------|---------|-------------|
| **CLI Tool Only** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git` | Get latest CLI features without touching project files |
| **Project Files** | `specify init --here --force --ai <your-agent>` | Update slash commands, templates, and scripts in your project |
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
---
## Part 1: Upgrade the CLI Tool
The CLI tool (`specify`) is separate from your project files. Upgrade it to get the latest features and bug fixes.
### If you installed with `uv tool install`
```bash
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
```
### If you use one-shot `uvx` commands
No upgrade needed—`uvx` always fetches the latest version. Just run your commands as normal:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai copilot
```
### Verify the upgrade
```bash
specify check
```
This shows installed tools and confirms the CLI is working.
---
## Part 2: Updating Project Files
When Spec Kit releases new features (like new slash commands or updated templates), you need to refresh your project's Spec Kit files.
### What gets updated?
Running `specify init --here --force` will update:
-**Slash command files** (`.claude/commands/`, `.github/prompts/`, etc.)
-**Script files** (`.specify/scripts/`)
-**Template files** (`.specify/templates/`)
-**Shared memory files** (`.specify/memory/`) - **⚠️ See warnings below**
### What stays safe?
These files are **never touched** by the upgrade—the template packages don't even contain them:
-**Your specifications** (`specs/001-my-feature/spec.md`, etc.) - **CONFIRMED SAFE**
-**Your implementation plans** (`specs/001-my-feature/plan.md`, `tasks.md`, etc.) - **CONFIRMED SAFE**
-**Your source code** - **CONFIRMED SAFE**
-**Your git history** - **CONFIRMED SAFE**
The `specs/` directory is completely excluded from template packages and will never be modified during upgrades.
### Update command
Run this inside your project directory:
```bash
specify init --here --force --ai <your-agent>
```
Replace `<your-agent>` with your AI assistant. Refer to this list of [Supported AI Agents](../README.md#-supported-ai-agents)
**Example:**
```bash
specify init --here --force --ai copilot
```
### Understanding the `--force` flag
Without `--force`, the CLI warns you and asks for confirmation:
```text
Warning: Current directory is not empty (25 items)
Template files will be merged with existing content and may overwrite existing files
Proceed? [y/N]
```
With `--force`, it skips the confirmation and proceeds immediately.
**Important: Your `specs/` directory is always safe.** The `--force` flag only affects template files (commands, scripts, templates, memory). Your feature specifications, plans, and tasks in `specs/` are never included in upgrade packages and cannot be overwritten.
---
## ⚠️ Important Warnings
### 1. Constitution file will be overwritten
**Known issue:** `specify init --here --force` currently overwrites `.specify/memory/constitution.md` with the default template, erasing any customizations you made.
**Workaround:**
```bash
# 1. Back up your constitution before upgrading
cp .specify/memory/constitution.md .specify/memory/constitution-backup.md
# 2. Run the upgrade
specify init --here --force --ai copilot
# 3. Restore your customized constitution
mv .specify/memory/constitution-backup.md .specify/memory/constitution.md
```
Or use git to restore it:
```bash
# After upgrade, restore from git history
git restore .specify/memory/constitution.md
```
### 2. Custom template modifications
If you customized any templates in `.specify/templates/`, the upgrade will overwrite them. Back them up first:
```bash
# Back up custom templates
cp -r .specify/templates .specify/templates-backup
# After upgrade, merge your changes back manually
```
### 3. Duplicate slash commands (IDE-based agents)
Some IDE-based agents (like Kilo Code, Windsurf) may show **duplicate slash commands** after upgrading—both old and new versions appear.
**Solution:** Manually delete the old command files from your agent's folder.
**Example for Kilo Code:**
```bash
# Navigate to the agent's commands folder
cd .kilocode/rules/
# List files and identify duplicates
ls -la
# Delete old versions (example filenames - yours may differ)
rm speckit.specify-old.md
rm speckit.plan-v1.md
```
Restart your IDE to refresh the command list.
---
## Common Scenarios
### Scenario 1: "I just want new slash commands"
```bash
# Upgrade CLI (if using persistent install)
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
# Update project files to get new commands
specify init --here --force --ai copilot
# Restore your constitution if customized
git restore .specify/memory/constitution.md
```
### Scenario 2: "I customized templates and constitution"
```bash
# 1. Back up customizations
cp .specify/memory/constitution.md /tmp/constitution-backup.md
cp -r .specify/templates /tmp/templates-backup
# 2. Upgrade CLI
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
# 3. Update project
specify init --here --force --ai copilot
# 4. Restore customizations
mv /tmp/constitution-backup.md .specify/memory/constitution.md
# Manually merge template changes if needed
```
### Scenario 3: "I see duplicate slash commands in my IDE"
This happens with IDE-based agents (Kilo Code, Windsurf, Roo Code, etc.).
```bash
# Find the agent folder (example: .kilocode/rules/)
cd .kilocode/rules/
# List all files
ls -la
# Delete old command files
rm speckit.old-command-name.md
# Restart your IDE
```
### Scenario 4: "I'm working on a project without Git"
If you initialized your project with `--no-git`, you can still upgrade:
```bash
# Manually back up files you customized
cp .specify/memory/constitution.md /tmp/constitution-backup.md
# Run upgrade
specify init --here --force --ai copilot --no-git
# Restore customizations
mv /tmp/constitution-backup.md .specify/memory/constitution.md
```
The `--no-git` flag skips git initialization but doesn't affect file updates.
---
## Using `--no-git` Flag
The `--no-git` flag tells Spec Kit to **skip git repository initialization**. This is useful when:
- You manage version control differently (Mercurial, SVN, etc.)
- Your project is part of a larger monorepo with existing git setup
- You're experimenting and don't want version control yet
**During initial setup:**
```bash
specify init my-project --ai copilot --no-git
```
**During upgrade:**
```bash
specify init --here --force --ai copilot --no-git
```
### What `--no-git` does NOT do
❌ Does NOT prevent file updates
❌ Does NOT skip slash command installation
❌ Does NOT affect template merging
It **only** skips running `git init` and creating the initial commit.
### Working without Git
If you use `--no-git`, you'll need to manage feature directories manually:
**Set the `SPECIFY_FEATURE` environment variable** before using planning commands:
```bash
# Bash/Zsh
export SPECIFY_FEATURE="001-my-feature"
# PowerShell
$env:SPECIFY_FEATURE = "001-my-feature"
```
This tells Spec Kit which feature directory to use when creating specs, plans, and tasks.
**Why this matters:** Without git, Spec Kit can't detect your current branch name to determine the active feature. The environment variable provides that context manually.
---
## Troubleshooting
### "Slash commands not showing up after upgrade"
**Cause:** Agent didn't reload the command files.
**Fix:**
1. **Restart your IDE/editor** completely (not just reload window)
2. **For CLI-based agents**, verify files exist:
```bash
ls -la .claude/commands/ # Claude Code
ls -la .gemini/commands/ # Gemini
ls -la .cursor/commands/ # Cursor
```
3. **Check agent-specific setup:**
- Codex requires `CODEX_HOME` environment variable
- Some agents need workspace restart or cache clearing
### "I lost my constitution customizations"
**Fix:** Restore from git or backup:
```bash
# If you committed before upgrading
git restore .specify/memory/constitution.md
# If you backed up manually
cp /tmp/constitution-backup.md .specify/memory/constitution.md
```
**Prevention:** Always commit or back up `constitution.md` before upgrading.
### "Warning: Current directory is not empty"
**Full warning message:**
```text
Warning: Current directory is not empty (25 items)
Template files will be merged with existing content and may overwrite existing files
Do you want to continue? [y/N]
```
**What this means:**
This warning appears when you run `specify init --here` (or `specify init .`) in a directory that already has files. It's telling you:
1. **The directory has existing content** - In the example, 25 files/folders
2. **Files will be merged** - New template files will be added alongside your existing files
3. **Some files may be overwritten** - If you already have Spec Kit files (`.claude/`, `.specify/`, etc.), they'll be replaced with the new versions
**What gets overwritten:**
Only Spec Kit infrastructure files:
- Agent command files (`.claude/commands/`, `.github/prompts/`, etc.)
- Scripts in `.specify/scripts/`
- Templates in `.specify/templates/`
- Memory files in `.specify/memory/` (including constitution)
**What stays untouched:**
- Your `specs/` directory (specifications, plans, tasks)
- Your source code files
- Your `.git/` directory and git history
- Any other files not part of Spec Kit templates
**How to respond:**
- **Type `y` and press Enter** - Proceed with the merge (recommended if upgrading)
- **Type `n` and press Enter** - Cancel the operation
- **Use `--force` flag** - Skip this confirmation entirely:
```bash
specify init --here --force --ai copilot
```
**When you see this warning:**
- ✅ **Expected** when upgrading an existing Spec Kit project
- ✅ **Expected** when adding Spec Kit to an existing codebase
- ⚠️ **Unexpected** if you thought you were creating a new project in an empty directory
**Prevention tip:** Before upgrading, commit or back up your `.specify/memory/constitution.md` if you customized it.
### "CLI upgrade doesn't seem to work"
Verify the installation:
```bash
# Check installed tools
uv tool list
# Should show specify-cli
# Verify path
which specify
# Should point to the uv tool installation directory
```
If not found, reinstall:
```bash
uv tool uninstall specify-cli
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
```
### "Do I need to run specify every time I open my project?"
**Short answer:** No, you only run `specify init` once per project (or when upgrading).
**Explanation:**
The `specify` CLI tool is used for:
- **Initial setup:** `specify init` to bootstrap Spec Kit in your project
- **Upgrades:** `specify init --here --force` to update templates and commands
- **Diagnostics:** `specify check` to verify tool installation
Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/speckit.plan`, etc.) are **permanently installed** in your project's agent folder (`.claude/`, `.github/prompts/`, etc.). Your AI assistant reads these command files directly—no need to run `specify` again.
**If your agent isn't recognizing slash commands:**
1. **Verify command files exist:**
```bash
# For GitHub Copilot
ls -la .github/prompts/
# For Claude
ls -la .claude/commands/
```
2. **Restart your IDE/editor completely** (not just reload window)
3. **Check you're in the correct directory** where you ran `specify init`
4. **For some agents**, you may need to reload the workspace or clear cache
**Related issue:** If Copilot can't open local files or uses PowerShell commands unexpectedly, this is typically an IDE context issue, not related to `specify`. Try:
- Restarting VS Code
- Checking file permissions
- Ensuring the workspace folder is properly opened
---
## Version Compatibility
Spec Kit follows semantic versioning for major releases. The CLI and project files are designed to be compatible within the same major version.
**Best practice:** Keep both CLI and project files in sync by upgrading both together during major version changes.
---
## Next Steps
After upgrading:
- **Test new slash commands:** Run `/speckit.constitution` or another command to verify everything works
- **Review release notes:** Check [GitHub Releases](https://github.com/github/spec-kit/releases) for new features and breaking changes
- **Update workflows:** If new commands were added, update your team's development workflows
- **Check documentation:** Visit [github.io/spec-kit](https://github.github.io/spec-kit/) for updated guides

View File

@@ -1,714 +0,0 @@
# Extension API Reference
Technical reference for Spec Kit extension system APIs and manifest schema.
## Table of Contents
1. [Extension Manifest](#extension-manifest)
2. [Python API](#python-api)
3. [Command File Format](#command-file-format)
4. [Configuration Schema](#configuration-schema)
5. [Hook System](#hook-system)
6. [CLI Commands](#cli-commands)
---
## Extension Manifest
### Schema Version 1.0
File: `extension.yml`
```yaml
schema_version: "1.0" # Required
extension:
id: string # Required, pattern: ^[a-z0-9-]+$
name: string # Required, human-readable name
version: string # Required, semantic version (X.Y.Z)
description: string # Required, brief description (<200 chars)
author: string # Required
repository: string # Required, valid URL
license: string # Required (e.g., "MIT", "Apache-2.0")
homepage: string # Optional, valid URL
requires:
speckit_version: string # Required, version specifier (>=X.Y.Z)
tools: # Optional, array of tool requirements
- name: string # Tool name
version: string # Optional, version specifier
required: boolean # Optional, default: false
provides:
commands: # Required, at least one command
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
file: string # Required, relative path to command file
description: string # Required
aliases: [string] # Optional, array of alternate names
config: # Optional, array of config files
- name: string # Config file name
template: string # Template file path
description: string
required: boolean # Default: false
hooks: # Optional, event hooks
event_name: # e.g., "after_tasks", "after_implement"
command: string # Command to execute
optional: boolean # Default: true
prompt: string # Prompt text for optional hooks
description: string # Hook description
condition: string # Optional, condition expression
tags: # Optional, array of tags (2-10 recommended)
- string
defaults: # Optional, default configuration values
key: value # Any YAML structure
```
### Field Specifications
#### `extension.id`
- **Type**: string
- **Pattern**: `^[a-z0-9-]+$`
- **Description**: Unique extension identifier
- **Examples**: `jira`, `linear`, `azure-devops`
- **Invalid**: `Jira`, `my_extension`, `extension.id`
#### `extension.version`
- **Type**: string
- **Format**: Semantic versioning (X.Y.Z)
- **Description**: Extension version
- **Examples**: `1.0.0`, `0.9.5`, `2.1.3`
- **Invalid**: `v1.0`, `1.0`, `1.0.0-beta`
#### `requires.speckit_version`
- **Type**: string
- **Format**: Version specifier
- **Description**: Required spec-kit version range
- **Examples**:
- `>=0.1.0` - Any version 0.1.0 or higher
- `>=0.1.0,<2.0.0` - Version 0.1.x or 1.x
- `==0.1.0` - Exactly 0.1.0
- **Invalid**: `0.1.0`, `>= 0.1.0` (space), `latest`
#### `provides.commands[].name`
- **Type**: string
- **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$`
- **Description**: Namespaced command name
- **Format**: `speckit.{extension-id}.{command-name}`
- **Examples**: `speckit.jira.specstoissues`, `speckit.linear.sync`
- **Invalid**: `jira.specstoissues`, `speckit.command`, `speckit.jira.CreateIssues`
#### `hooks`
- **Type**: object
- **Keys**: Event names (e.g., `after_tasks`, `after_implement`, `before_commit`)
- **Description**: Hooks that execute at lifecycle events
- **Events**: Defined by core spec-kit commands
---
## Python API
### ExtensionManifest
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionManifest
manifest = ExtensionManifest(Path("extension.yml"))
```
**Properties**:
```python
manifest.id # str: Extension ID
manifest.name # str: Extension name
manifest.version # str: Version
manifest.description # str: Description
manifest.requires_speckit_version # str: Required spec-kit version
manifest.commands # List[Dict]: Command definitions
manifest.hooks # Dict: Hook definitions
```
**Methods**:
```python
manifest.get_hash() # str: SHA256 hash of manifest file
```
**Exceptions**:
```python
ValidationError # Invalid manifest structure
CompatibilityError # Incompatible with current spec-kit version
```
### ExtensionRegistry
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionRegistry
registry = ExtensionRegistry(extensions_dir)
```
**Methods**:
```python
# Add extension to registry
registry.add(extension_id: str, metadata: dict)
# Remove extension from registry
registry.remove(extension_id: str)
# Get extension metadata
metadata = registry.get(extension_id: str) # Optional[dict]
# List all extensions
extensions = registry.list() # Dict[str, dict]
# Check if installed
is_installed = registry.is_installed(extension_id: str) # bool
```
**Registry Format**:
```json
{
"schema_version": "1.0",
"extensions": {
"jira": {
"version": "1.0.0",
"source": "catalog",
"manifest_hash": "sha256...",
"enabled": true,
"registered_commands": ["speckit.jira.specstoissues", ...],
"installed_at": "2026-01-28T..."
}
}
}
```
### ExtensionManager
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionManager
manager = ExtensionManager(project_root)
```
**Methods**:
```python
# Install from directory
manifest = manager.install_from_directory(
source_dir: Path,
speckit_version: str,
register_commands: bool = True
) # Returns: ExtensionManifest
# Install from ZIP
manifest = manager.install_from_zip(
zip_path: Path,
speckit_version: str
) # Returns: ExtensionManifest
# Remove extension
success = manager.remove(
extension_id: str,
keep_config: bool = False
) # Returns: bool
# List installed extensions
extensions = manager.list_installed() # List[Dict]
# Get extension manifest
manifest = manager.get_extension(extension_id: str) # Optional[ExtensionManifest]
# Check compatibility
manager.check_compatibility(
manifest: ExtensionManifest,
speckit_version: str
) # Raises: CompatibilityError if incompatible
```
### ExtensionCatalog
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionCatalog
catalog = ExtensionCatalog(project_root)
```
**Methods**:
```python
# Fetch catalog
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
# Search extensions
results = catalog.search(
query: Optional[str] = None,
tag: Optional[str] = None,
author: Optional[str] = None,
verified_only: bool = False
) # Returns: List[Dict]
# Get extension info
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
# Check cache validity
is_valid = catalog.is_cache_valid() # bool
# Clear cache
catalog.clear_cache()
```
### HookExecutor
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import HookExecutor
hook_executor = HookExecutor(project_root)
```
**Methods**:
```python
# Get project config
config = hook_executor.get_project_config() # Dict
# Save project config
hook_executor.save_project_config(config: Dict)
# Register hooks
hook_executor.register_hooks(manifest: ExtensionManifest)
# Unregister hooks
hook_executor.unregister_hooks(extension_id: str)
# Get hooks for event
hooks = hook_executor.get_hooks_for_event(event_name: str) # List[Dict]
# Check if hook should execute
should_run = hook_executor.should_execute_hook(hook: Dict) # bool
# Format hook message
message = hook_executor.format_hook_message(
event_name: str,
hooks: List[Dict]
) # str
```
### CommandRegistrar
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import CommandRegistrar
registrar = CommandRegistrar()
```
**Methods**:
```python
# Register commands for Claude Code
registered = registrar.register_commands_for_claude(
manifest: ExtensionManifest,
extension_dir: Path,
project_root: Path
) # Returns: List[str] (command names)
# Parse frontmatter
frontmatter, body = registrar.parse_frontmatter(content: str)
# Render frontmatter
yaml_text = registrar.render_frontmatter(frontmatter: Dict) # str
```
---
## Command File Format
### Universal Command Format
**File**: `commands/{command-name}.md`
```markdown
---
description: "Command description"
tools:
- 'mcp-server/tool_name'
- 'other-mcp-server/other_tool'
---
# Command Title
Command documentation in Markdown.
## Prerequisites
1. Requirement 1
2. Requirement 2
## User Input
$ARGUMENTS
## Steps
### Step 1: Description
Instruction text...
\`\`\`bash
# Shell commands
\`\`\`
### Step 2: Another Step
More instructions...
## Configuration Reference
Information about configuration options.
## Notes
Additional notes and tips.
```
### Frontmatter Fields
```yaml
description: string # Required, brief command description
tools: [string] # Optional, MCP tools required
```
### Special Variables
- `$ARGUMENTS` - Placeholder for user-provided arguments
- Extension context automatically injected:
```markdown
<!-- Extension: {extension-id} -->
<!-- Config: .specify/extensions/{extension-id}/ -->
```
---
## Configuration Schema
### Extension Config File
**File**: `.specify/extensions/{extension-id}/{extension-id}-config.yml`
Extensions define their own config schema. Common patterns:
```yaml
# Connection settings
connection:
url: string
api_key: string
# Project settings
project:
key: string
workspace: string
# Feature flags
features:
enabled: boolean
auto_sync: boolean
# Defaults
defaults:
labels: [string]
assignee: string
# Custom fields
field_mappings:
internal_name: "external_field_id"
```
### Config Layers
1. **Extension Defaults** (from `extension.yml` `defaults` section)
2. **Project Config** (`{extension-id}-config.yml`)
3. **Local Override** (`{extension-id}-config.local.yml`, gitignored)
4. **Environment Variables** (`SPECKIT_{EXTENSION}_*`)
### Environment Variable Pattern
Format: `SPECKIT_{EXTENSION}_{KEY}`
Examples:
- `SPECKIT_JIRA_PROJECT_KEY`
- `SPECKIT_LINEAR_API_KEY`
- `SPECKIT_GITHUB_TOKEN`
---
## Hook System
### Hook Definition
**In extension.yml**:
```yaml
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy"
condition: null
```
### Hook Events
Standard events (defined by core):
- `after_tasks` - After task generation
- `after_implement` - After implementation
- `before_commit` - Before git commit
- `after_commit` - After git commit
### Hook Configuration
**In `.specify/extensions.yml`**:
```yaml
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
description: "..."
condition: null
```
### Hook Message Format
```markdown
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
Or for mandatory hooks:
```markdown
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
---
## CLI Commands
### extension list
**Usage**: `specify extension list [OPTIONS]`
**Options**:
- `--available` - Show available extensions from catalog
- `--all` - Show both installed and available
**Output**: List of installed extensions with metadata
### extension add
**Usage**: `specify extension add EXTENSION [OPTIONS]`
**Options**:
- `--from URL` - Install from custom URL
- `--dev PATH` - Install from local directory
- `--version VERSION` - Install specific version
- `--no-register` - Skip command registration
**Arguments**:
- `EXTENSION` - Extension name or URL
### extension remove
**Usage**: `specify extension remove EXTENSION [OPTIONS]`
**Options**:
- `--keep-config` - Preserve config files
- `--force` - Skip confirmation
**Arguments**:
- `EXTENSION` - Extension ID
### extension search
**Usage**: `specify extension search [QUERY] [OPTIONS]`
**Options**:
- `--tag TAG` - Filter by tag
- `--author AUTHOR` - Filter by author
- `--verified` - Show only verified extensions
**Arguments**:
- `QUERY` - Optional search query
### extension info
**Usage**: `specify extension info EXTENSION`
**Arguments**:
- `EXTENSION` - Extension ID
### extension update
**Usage**: `specify extension update [EXTENSION]`
**Arguments**:
- `EXTENSION` - Optional, extension ID (default: all)
### extension enable
**Usage**: `specify extension enable EXTENSION`
**Arguments**:
- `EXTENSION` - Extension ID
### extension disable
**Usage**: `specify extension disable EXTENSION`
**Arguments**:
- `EXTENSION` - Extension ID
---
## Exceptions
### ValidationError
Raised when extension manifest validation fails.
```python
from specify_cli.extensions import ValidationError
try:
manifest = ExtensionManifest(path)
except ValidationError as e:
print(f"Invalid manifest: {e}")
```
### CompatibilityError
Raised when extension is incompatible with current spec-kit version.
```python
from specify_cli.extensions import CompatibilityError
try:
manager.check_compatibility(manifest, "0.1.0")
except CompatibilityError as e:
print(f"Incompatible: {e}")
```
### ExtensionError
Base exception for all extension-related errors.
```python
from specify_cli.extensions import ExtensionError
try:
manager.install_from_directory(path, "0.1.0")
except ExtensionError as e:
print(f"Extension error: {e}")
```
---
## Version Functions
### version_satisfies
Check if a version satisfies a specifier.
```python
from specify_cli.extensions import version_satisfies
# True if 1.2.3 satisfies >=1.0.0,<2.0.0
satisfied = version_satisfies("1.2.3", ">=1.0.0,<2.0.0") # bool
```
---
## File System Layout
```text
.specify/
├── extensions/
│ ├── .registry # Extension registry (JSON)
│ ├── .cache/ # Catalog cache
│ │ ├── catalog.json
│ │ └── catalog-metadata.json
│ ├── .backup/ # Config backups
│ │ └── {ext}-{config}.yml
│ ├── {extension-id}/ # Extension directory
│ │ ├── extension.yml # Manifest
│ │ ├── {ext}-config.yml # User config
│ │ ├── {ext}-config.local.yml # Local overrides (gitignored)
│ │ ├── {ext}-config.template.yml # Template
│ │ ├── commands/ # Command files
│ │ │ └── *.md
│ │ ├── scripts/ # Helper scripts
│ │ │ └── *.sh
│ │ ├── docs/ # Documentation
│ │ └── README.md
│ └── extensions.yml # Project extension config
└── scripts/ # (existing spec-kit)
.claude/
└── commands/
└── speckit.{ext}.{cmd}.md # Registered commands
```
---
*Last Updated: 2026-01-28*
*API Version: 1.0*
*Spec Kit Version: 0.1.0*

View File

@@ -1,649 +0,0 @@
# 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"
```
---
## 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: Extension Catalog (Future)
Submit to official catalog:
1. **Fork** spec-kit repository
2. **Add entry** to `extensions/catalog.json`
3. **Create PR**
4. **After merge**, users can install with:
```bash
specify extension add my-ext # No URL needed!
```
---
## 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
<!-- commands/hello.md -->
---
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
<!-- commands/use-config.md -->
# 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! 🚀

View File

@@ -1,530 +0,0 @@
# Extension Publishing Guide
This guide explains how to publish your extension to the Spec Kit extension catalog, making it discoverable by `specify extension search`.
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Prepare Your Extension](#prepare-your-extension)
3. [Submit to Catalog](#submit-to-catalog)
4. [Verification Process](#verification-process)
5. [Release Workflow](#release-workflow)
6. [Best Practices](#best-practices)
---
## Prerequisites
Before publishing an extension, ensure you have:
1. **Valid Extension**: A working extension with a valid `extension.yml` manifest
2. **Git Repository**: Extension hosted on GitHub (or other public git hosting)
3. **Documentation**: README.md with installation and usage instructions
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
5. **Versioning**: Semantic versioning (e.g., 1.0.0)
6. **Testing**: Extension tested on real projects
---
## Prepare Your Extension
### 1. Extension Structure
Ensure your extension follows the standard structure:
```text
your-extension/
├── extension.yml # Required: Extension manifest
├── README.md # Required: Documentation
├── LICENSE # Required: License file
├── CHANGELOG.md # Recommended: Version history
├── .gitignore # Recommended: Git ignore rules
├── commands/ # Extension commands
│ ├── command1.md
│ └── command2.md
├── config-template.yml # Config template (if needed)
└── docs/ # Additional documentation
├── usage.md
└── examples/
```
### 2. extension.yml Validation
Verify your manifest is valid:
```yaml
schema_version: "1.0"
extension:
id: "your-extension" # Unique lowercase-hyphenated ID
name: "Your Extension Name" # Human-readable name
version: "1.0.0" # Semantic version
description: "Brief description (one sentence)"
author: "Your Name or Organization"
repository: "https://github.com/your-org/spec-kit-your-extension"
license: "MIT"
homepage: "https://github.com/your-org/spec-kit-your-extension"
requires:
speckit_version: ">=0.1.0" # Required spec-kit version
provides:
commands: # List all commands
- name: "speckit.your-extension.command"
file: "commands/command.md"
description: "Command description"
tags: # 2-5 relevant tags
- "category"
- "tool-name"
```
**Validation Checklist**:
-`id` is lowercase with hyphens only (no underscores, spaces, or special characters)
-`version` follows semantic versioning (X.Y.Z)
-`description` is concise (under 100 characters)
-`repository` URL is valid and public
- ✅ All command files exist in the extension directory
- ✅ Tags are lowercase and descriptive
### 3. Create GitHub Release
Create a GitHub release for your extension version:
```bash
# Tag the release
git tag v1.0.0
git push origin v1.0.0
# Create release on GitHub
# Go to: https://github.com/your-org/spec-kit-your-extension/releases/new
# - Tag: v1.0.0
# - Title: v1.0.0 - Release Name
# - Description: Changelog/release notes
```
The release archive URL will be:
```text
https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
```
### 4. Test Installation
Test that users can install from your release:
```bash
# Test dev installation
specify extension add --dev /path/to/your-extension
# Test from GitHub archive
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
```
---
## Submit to Catalog
### 1. Fork the spec-kit Repository
```bash
# Fork on GitHub
# https://github.com/statsperform/spec-kit/fork
# Clone your fork
git clone https://github.com/YOUR-USERNAME/spec-kit.git
cd spec-kit
```
### 2. Add Extension to Catalog
Edit `extensions/catalog.json` and add your extension:
```json
{
"schema_version": "1.0",
"updated_at": "2026-01-28T15:54:00Z",
"catalog_url": "https://raw.githubusercontent.com/statsperform/spec-kit/main/extensions/catalog.json",
"extensions": {
"your-extension": {
"name": "Your Extension Name",
"id": "your-extension",
"description": "Brief description of your extension",
"author": "Your Name",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/your-org/spec-kit-your-extension",
"homepage": "https://github.com/your-org/spec-kit-your-extension",
"documentation": "https://github.com/your-org/spec-kit-your-extension/blob/main/docs/",
"changelog": "https://github.com/your-org/spec-kit-your-extension/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "required-mcp-tool",
"version": ">=1.0.0",
"required": true
}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"category",
"tool-name",
"feature"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-01-28T00:00:00Z",
"updated_at": "2026-01-28T00:00:00Z"
}
}
}
```
**Important**:
- Set `verified: false` (maintainers will verify)
- Set `downloads: 0` and `stars: 0` (auto-updated later)
- Use current timestamp for `created_at` and `updated_at`
- Update the top-level `updated_at` to current time
### 3. Submit Pull Request
```bash
# Create a branch
git checkout -b add-your-extension
# Commit your changes
git add extensions/catalog.json
git commit -m "Add your-extension to catalog
- Extension ID: your-extension
- Version: 1.0.0
- Author: Your Name
- Description: Brief description
"
# Push to your fork
git push origin add-your-extension
# Create Pull Request on GitHub
# https://github.com/statsperform/spec-kit/compare
```
**Pull Request Template**:
```markdown
## Extension Submission
**Extension Name**: Your Extension Name
**Extension ID**: your-extension
**Version**: 1.0.0
**Author**: Your Name
**Repository**: https://github.com/your-org/spec-kit-your-extension
### Description
Brief description of what your extension does.
### Checklist
- [x] Valid extension.yml manifest
- [x] README.md with installation and usage docs
- [x] LICENSE file included
- [x] GitHub release created (v1.0.0)
- [x] Extension tested on real project
- [x] All commands working
- [x] No security vulnerabilities
### Testing
Tested on:
- macOS 13.0+ with spec-kit 0.1.0
- Project: [Your test project]
### Additional Notes
Any additional context or notes for reviewers.
```
---
## Verification Process
### What Happens After Submission
1. **Automated Checks** (if available):
- Manifest validation
- Download URL accessibility
- Repository existence
- License file presence
2. **Manual Review**:
- Code quality review
- Security audit
- Functionality testing
- Documentation review
3. **Verification**:
- If approved, `verified: true` is set
- Extension appears in `specify extension search --verified`
### Verification Criteria
To be verified, your extension must:
**Functionality**:
- Works as described in documentation
- All commands execute without errors
- No breaking changes to user workflows
**Security**:
- No known vulnerabilities
- No malicious code
- Safe handling of user data
- Proper validation of inputs
**Code Quality**:
- Clean, readable code
- Follows extension best practices
- Proper error handling
- Helpful error messages
**Documentation**:
- Clear installation instructions
- Usage examples
- Troubleshooting section
- Accurate description
**Maintenance**:
- Active repository
- Responsive to issues
- Regular updates
- Semantic versioning followed
### Typical Review Timeline
- **Automated checks**: Immediate (if implemented)
- **Manual review**: 3-7 business days
- **Verification**: After successful review
---
## Release Workflow
### Publishing New Versions
When releasing a new version:
1. **Update version** in `extension.yml`:
```yaml
extension:
version: "1.1.0" # Updated version
```
2. **Update CHANGELOG.md**:
```markdown
## [1.1.0] - 2026-02-15
### Added
- New feature X
### Fixed
- Bug fix Y
```
3. **Create GitHub release**:
```bash
git tag v1.1.0
git push origin v1.1.0
# Create release on GitHub
```
4. **Update catalog**:
```bash
# Fork spec-kit repo (or update existing fork)
cd spec-kit
# Update extensions/catalog.json
jq '.extensions["your-extension"].version = "1.1.0"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.extensions["your-extension"].download_url = "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.1.0.zip"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.extensions["your-extension"].updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
# Submit PR
git checkout -b update-your-extension-v1.1.0
git add extensions/catalog.json
git commit -m "Update your-extension to v1.1.0"
git push origin update-your-extension-v1.1.0
```
5. **Submit update PR** with changelog in description
---
## Best Practices
### Extension Design
1. **Single Responsibility**: Each extension should focus on one tool/integration
2. **Clear Naming**: Use descriptive, unambiguous names
3. **Minimal Dependencies**: Avoid unnecessary dependencies
4. **Backward Compatibility**: Follow semantic versioning strictly
### Documentation
1. **README.md Structure**:
- Overview and features
- Installation instructions
- Configuration guide
- Usage examples
- Troubleshooting
- Contributing guidelines
2. **Command Documentation**:
- Clear description
- Prerequisites listed
- Step-by-step instructions
- Error handling guidance
- Examples
3. **Configuration**:
- Provide template file
- Document all options
- Include examples
- Explain defaults
### Security
1. **Input Validation**: Validate all user inputs
2. **No Hardcoded Secrets**: Never include credentials
3. **Safe Dependencies**: Only use trusted dependencies
4. **Audit Regularly**: Check for vulnerabilities
### Maintenance
1. **Respond to Issues**: Address issues within 1-2 weeks
2. **Regular Updates**: Keep dependencies updated
3. **Changelog**: Maintain detailed changelog
4. **Deprecation**: Give advance notice for breaking changes
### Community
1. **License**: Use permissive open-source license (MIT, Apache 2.0)
2. **Contributing**: Welcome contributions
3. **Code of Conduct**: Be respectful and inclusive
4. **Support**: Provide ways to get help (issues, discussions, email)
---
## FAQ
### Q: Can I publish private/proprietary extensions?
A: The main catalog is for public extensions only. For private extensions:
- Host your own catalog.json file
- Users add your catalog: `specify extension add-catalog https://your-domain.com/catalog.json`
- Not yet implemented - coming in Phase 4
### Q: How long does verification take?
A: Typically 3-7 business days for initial review. Updates to verified extensions are usually faster.
### Q: What if my extension is rejected?
A: You'll receive feedback on what needs to be fixed. Make the changes and resubmit.
### Q: Can I update my extension anytime?
A: Yes, submit a PR to update the catalog with your new version. Verified status may be re-evaluated for major changes.
### Q: Do I need to be verified to be in the catalog?
A: No, unverified extensions are still searchable. Verification just adds trust and visibility.
### Q: Can extensions have paid features?
A: Extensions should be free and open-source. Commercial support/services are allowed, but core functionality must be free.
---
## Support
- **Catalog Issues**: <https://github.com/statsperform/spec-kit/issues>
- **Extension Template**: <https://github.com/statsperform/spec-kit-extension-template> (coming soon)
- **Development Guide**: See EXTENSION-DEVELOPMENT-GUIDE.md
- **Community**: Discussions and Q&A
---
## Appendix: Catalog Schema
### Complete Catalog Entry Schema
```json
{
"name": "string (required)",
"id": "string (required, unique)",
"description": "string (required, <200 chars)",
"author": "string (required)",
"version": "string (required, semver)",
"download_url": "string (required, valid URL)",
"repository": "string (required, valid URL)",
"homepage": "string (optional, valid URL)",
"documentation": "string (optional, valid URL)",
"changelog": "string (optional, valid URL)",
"license": "string (required)",
"requires": {
"speckit_version": "string (required, version specifier)",
"tools": [
{
"name": "string (required)",
"version": "string (optional, version specifier)",
"required": "boolean (default: false)"
}
]
},
"provides": {
"commands": "integer (optional)",
"hooks": "integer (optional)"
},
"tags": ["array of strings (2-10 tags)"],
"verified": "boolean (default: false)",
"downloads": "integer (auto-updated)",
"stars": "integer (auto-updated)",
"created_at": "string (ISO 8601 datetime)",
"updated_at": "string (ISO 8601 datetime)"
}
```
### Valid Tags
Recommended tag categories:
- **Integration**: jira, linear, github, gitlab, azure-devops
- **Category**: issue-tracking, vcs, ci-cd, documentation, testing
- **Platform**: atlassian, microsoft, google
- **Feature**: automation, reporting, deployment, monitoring
Use 2-5 tags that best describe your extension.
---
*Last Updated: 2026-01-28*
*Catalog Format Version: 1.0*

View File

@@ -1,885 +0,0 @@
# Extension User Guide
Complete guide for using Spec Kit extensions to enhance your workflow.
## Table of Contents
1. [Introduction](#introduction)
2. [Getting Started](#getting-started)
3. [Finding Extensions](#finding-extensions)
4. [Installing Extensions](#installing-extensions)
5. [Using Extensions](#using-extensions)
6. [Managing Extensions](#managing-extensions)
7. [Configuration](#configuration)
8. [Troubleshooting](#troubleshooting)
9. [Best Practices](#best-practices)
---
## Introduction
### What are Extensions?
Extensions are modular packages that add new commands and functionality to Spec Kit without bloating the core framework. They allow you to:
- **Integrate** with external tools (Jira, Linear, GitHub, etc.)
- **Automate** repetitive tasks with hooks
- **Customize** workflows for your team
- **Share** solutions across projects
### Why Use Extensions?
- **Clean Core**: Keeps spec-kit lightweight and focused
- **Optional Features**: Only install what you need
- **Community Driven**: Anyone can create and share extensions
- **Version Controlled**: Extensions are versioned independently
---
## Getting Started
### Prerequisites
- Spec Kit version 0.1.0 or higher
- A spec-kit project (directory with `.specify/` folder)
### Check Your Version
```bash
specify --version
# Should show 0.1.0 or higher
```
### First Extension
Let's install the Jira extension as an example:
```bash
# 1. Search for the extension
specify extension search jira
# 2. Get detailed information
specify extension info jira
# 3. Install it
specify extension add jira
# 4. Configure it
vim .specify/extensions/jira/jira-config.yml
# 5. Use it
# (Commands are now available in Claude Code)
/speckit.jira.specstoissues
```
---
## Finding Extensions
### Browse All Extensions
```bash
specify extension search
```
Shows all available extensions in the catalog.
### Search by Keyword
```bash
# Search for "jira"
specify extension search jira
# Search for "issue tracking"
specify extension search issue
```
### Filter by Tag
```bash
# Find all issue-tracking extensions
specify extension search --tag issue-tracking
# Find all Atlassian tools
specify extension search --tag atlassian
```
### Filter by Author
```bash
# Extensions by Stats Perform
specify extension search --author "Stats Perform"
```
### Show Verified Only
```bash
# Only show verified extensions
specify extension search --verified
```
### Get Extension Details
```bash
# Detailed information
specify extension info jira
```
Shows:
- Description
- Requirements
- Commands provided
- Hooks available
- Links (documentation, repository, changelog)
- Installation status
---
## Installing Extensions
### Install from Catalog
```bash
# By name (from catalog)
specify extension add jira
```
This will:
1. Download the extension from GitHub
2. Validate the manifest
3. Check compatibility with your spec-kit version
4. Install to `.specify/extensions/jira/`
5. Register commands with your AI agent
6. Create config template
### Install from URL
```bash
# From GitHub release
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
```
### Install from Local Directory (Development)
```bash
# For testing or development
specify extension add --dev /path/to/extension
```
### Installation Output
```text
✓ Extension installed successfully!
Jira Integration (v1.0.0)
Create Jira Epics, Stories, and Issues from spec-kit artifacts
Provided commands:
• speckit.jira.specstoissues - Create Jira hierarchy from spec and tasks
• speckit.jira.discover-fields - Discover Jira custom fields for configuration
• speckit.jira.sync-status - Sync task completion status to Jira
⚠ Configuration may be required
Check: .specify/extensions/jira/
```
---
## Using Extensions
### Using Extension Commands
Extensions add commands that appear in your AI agent (Claude Code):
```text
# In Claude Code
> /speckit.jira.specstoissues
# Or use short alias (if provided)
> /speckit.specstoissues
```
### Extension Configuration
Most extensions require configuration:
```bash
# 1. Find the config file
ls .specify/extensions/jira/
# 2. Copy template to config
cp .specify/extensions/jira/jira-config.template.yml \
.specify/extensions/jira/jira-config.yml
# 3. Edit configuration
vim .specify/extensions/jira/jira-config.yml
# 4. Use the extension
# (Commands will now work with your config)
```
### Extension Hooks
Some extensions provide hooks that execute after core commands:
**Example**: Jira extension hooks into `/speckit.tasks`
```text
# Run core command
> /speckit.tasks
# Output includes:
## Extension Hooks
**Optional Hook**: jira
Command: `/speckit.jira.specstoissues`
Description: Automatically create Jira hierarchy after task generation
Prompt: Create Jira issues from tasks?
To execute: `/speckit.jira.specstoissues`
```
You can then choose to run the hook or skip it.
---
## Managing Extensions
### List Installed Extensions
```bash
specify extension list
```
Output:
```text
Installed Extensions:
✓ Jira Integration (v1.0.0)
Create Jira Epics, Stories, and Issues from spec-kit artifacts
Commands: 3 | Hooks: 1 | Status: Enabled
```
### Update Extensions
```bash
# Check for updates (all extensions)
specify extension update
# Update specific extension
specify extension update jira
```
Output:
```text
🔄 Checking for updates...
Updates available:
• jira: 1.0.0 → 1.1.0
Update these extensions? [y/N]:
```
### Disable Extension Temporarily
```bash
# Disable without removing
specify extension disable jira
✓ Extension 'jira' disabled
Commands will no longer be available. Hooks will not execute.
To re-enable: specify extension enable jira
```
### Re-enable Extension
```bash
specify extension enable jira
✓ Extension 'jira' enabled
```
### Remove Extension
```bash
# Remove extension (with confirmation)
specify extension remove jira
# Keep configuration when removing
specify extension remove jira --keep-config
# Force removal (no confirmation)
specify extension remove jira --force
```
---
## Configuration
### Configuration Files
Extensions can have multiple configuration files:
```text
.specify/extensions/jira/
├── jira-config.yml # Main config (version controlled)
├── jira-config.local.yml # Local overrides (gitignored)
└── jira-config.template.yml # Template (reference)
```
### Configuration Layers
Configuration is merged in this order (highest priority last):
1. **Extension defaults** (from `extension.yml`)
2. **Project config** (`jira-config.yml`)
3. **Local overrides** (`jira-config.local.yml`)
4. **Environment variables** (`SPECKIT_JIRA_*`)
### Example: Jira Configuration
**Project config** (`.specify/extensions/jira/jira-config.yml`):
```yaml
project:
key: "MSATS"
defaults:
epic:
labels: ["spec-driven"]
```
**Local override** (`.specify/extensions/jira/jira-config.local.yml`):
```yaml
project:
key: "MYTEST" # Override for local development
```
**Environment variable**:
```bash
export SPECKIT_JIRA_PROJECT_KEY="DEVTEST"
```
Final resolved config uses `DEVTEST` from environment variable.
### Project-Wide Extension Settings
File: `.specify/extensions.yml`
```yaml
# Extensions installed in this project
installed:
- jira
- linear
# Global settings
settings:
auto_execute_hooks: true
# Hook configuration
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
```
### Core Environment Variables
In addition to extension-specific environment variables (`SPECKIT_{EXT_ID}_*`), spec-kit supports core environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| `SPECKIT_CATALOG_URL` | Override the extension catalog URL | GitHub-hosted catalog |
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None |
#### Example: Using a custom catalog for testing
```bash
# Point to a local or alternative catalog
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
# Or use a staging catalog
export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
```
---
## Organization Catalog Customization
### Why the Default Catalog is Empty
The default spec-kit catalog ships empty by design. This allows organizations to:
- **Control available extensions** - Curate which extensions your team can install
- **Host private extensions** - Internal tools that shouldn't be public
- **Customize for compliance** - Meet security/audit requirements
- **Support air-gapped environments** - Work without internet access
### Setting Up a Custom Catalog
#### 1. Create Your Catalog File
Create a `catalog.json` file with your extensions:
```json
{
"schema_version": "1.0",
"updated_at": "2026-02-03T00:00:00Z",
"catalog_url": "https://your-org.com/spec-kit/catalog.json",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"description": "Create Jira issues from spec-kit artifacts",
"author": "Your Organization",
"version": "2.1.0",
"download_url": "https://github.com/your-org/spec-kit-jira/archive/refs/tags/v2.1.0.zip",
"repository": "https://github.com/your-org/spec-kit-jira",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{"name": "atlassian-mcp-server", "required": true}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": ["jira", "atlassian", "issue-tracking"],
"verified": true
},
"internal-tool": {
"name": "Internal Tool Integration",
"id": "internal-tool",
"description": "Connect to internal company systems",
"author": "Your Organization",
"version": "1.0.0",
"download_url": "https://internal.your-org.com/extensions/internal-tool-1.0.0.zip",
"repository": "https://github.internal.your-org.com/spec-kit-internal",
"license": "Proprietary",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2
},
"tags": ["internal", "proprietary"],
"verified": true
}
}
}
```
#### 2. Host the Catalog
Options for hosting your catalog:
| Method | URL Example | Use Case |
| ------ | ----------- | -------- |
| GitHub Pages | `https://your-org.github.io/spec-kit-catalog/catalog.json` | Public or org-visible |
| Internal web server | `https://internal.company.com/spec-kit/catalog.json` | Corporate network |
| S3/Cloud storage | `https://s3.amazonaws.com/your-bucket/catalog.json` | Cloud-hosted teams |
| Local file server | `http://localhost:8000/catalog.json` | Development/testing |
**Security requirement**: URLs must use HTTPS (except `localhost` for testing).
#### 3. Configure Your Environment
##### Option A: Environment variable (recommended for CI/CD)
```bash
# In ~/.bashrc, ~/.zshrc, or CI pipeline
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
```
##### Option B: Per-project configuration
Create `.env` or set in your shell before running spec-kit commands:
```bash
SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json" specify extension search
```
#### 4. Verify Configuration
```bash
# Search should now show your catalog's extensions
specify extension search
# Install from your catalog
specify extension add jira
```
### Catalog JSON Schema
Required fields for each extension entry:
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `name` | string | Yes | Human-readable name |
| `id` | string | Yes | Unique identifier (lowercase, hyphens) |
| `version` | string | Yes | Semantic version (X.Y.Z) |
| `download_url` | string | Yes | URL to ZIP archive |
| `repository` | string | Yes | Source code URL |
| `description` | string | No | Brief description |
| `author` | string | No | Author/organization |
| `license` | string | No | SPDX license identifier |
| `requires.speckit_version` | string | No | Version constraint |
| `requires.tools` | array | No | Required external tools |
| `provides.commands` | number | No | Number of commands |
| `provides.hooks` | number | No | Number of hooks |
| `tags` | array | No | Search tags |
| `verified` | boolean | No | Verification status |
### Use Cases
#### Private/Internal Extensions
Host proprietary extensions that integrate with internal systems:
```json
{
"internal-auth": {
"name": "Internal SSO Integration",
"download_url": "https://artifactory.company.com/spec-kit/internal-auth-1.0.0.zip",
"verified": true
}
}
```
#### Curated Team Catalog
Limit which extensions your team can install:
```json
{
"extensions": {
"jira": { "..." },
"github": { "..." }
}
}
```
Only `jira` and `github` will appear in `specify extension search`.
#### Air-Gapped Environments
For networks without internet access:
1. Download extension ZIPs to internal file server
2. Create catalog pointing to internal URLs
3. Host catalog on internal web server
```json
{
"jira": {
"download_url": "https://files.internal/spec-kit/jira-2.1.0.zip"
}
}
```
#### Development/Testing
Test new extensions before publishing:
```bash
# Start local server
python -m http.server 8000 --directory ./my-catalog/
# Point spec-kit to local catalog
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
# Test installation
specify extension add my-new-extension
```
### Combining with Direct Installation
You can still install extensions not in your catalog using `--from`:
```bash
# From catalog
specify extension add jira
# Direct URL (bypasses catalog)
specify extension add --from https://github.com/someone/spec-kit-ext/archive/v1.0.0.zip
# Local development
specify extension add --dev /path/to/extension
```
**Note**: Direct URL installation shows a security warning since the extension isn't from your configured catalog.
---
## Troubleshooting
### Extension Not Found
**Error**: `Extension 'jira' not found in catalog
**Solutions**:
1. Check spelling: `specify extension search jira`
2. Refresh catalog: `specify extension search --help`
3. Check internet connection
4. Extension may not be published yet
### Configuration Not Found
**Error**: `Jira configuration not found`
**Solutions**:
1. Check if extension is installed: `specify extension list`
2. Create config from template:
```bash
cp .specify/extensions/jira/jira-config.template.yml \
.specify/extensions/jira/jira-config.yml
```
3. Reinstall extension: `specify extension remove jira && specify extension add jira`
### Command Not Available
**Issue**: Extension command not appearing in AI agent
**Solutions**:
1. Check extension is enabled: `specify extension list`
2. Restart AI agent (Claude Code)
3. Check command file exists:
```bash
ls .claude/commands/speckit.jira.*.md
```
4. Reinstall extension
### Incompatible Version
**Error**: `Extension requires spec-kit >=0.2.0, but you have 0.1.0`
**Solutions**:
1. Upgrade spec-kit:
```bash
uv tool upgrade specify-cli
```
2. Install older version of extension:
```bash
specify extension add --from https://github.com/org/ext/archive/v1.0.0.zip
```
### MCP Tool Not Available
**Error**: `Tool 'jira-mcp-server/epic_create' not found`
**Solutions**:
1. Check MCP server is installed
2. Check AI agent MCP configuration
3. Restart AI agent
4. Check extension requirements: `specify extension info jira`
### Permission Denied
**Error**: `Permission denied` when accessing Jira
**Solutions**:
1. Check Jira credentials in MCP server config
2. Verify project permissions in Jira
3. Test MCP server connection independently
---
## Best Practices
### 1. Version Control
**Do commit**:
- `.specify/extensions.yml` (project extension config)
- `.specify/extensions/*/jira-config.yml` (project config)
**Don't commit**:
- `.specify/extensions/.cache/` (catalog cache)
- `.specify/extensions/.backup/` (config backups)
- `.specify/extensions/*/*.local.yml` (local overrides)
- `.specify/extensions/.registry` (installation state)
Add to `.gitignore`:
```gitignore
.specify/extensions/.cache/
.specify/extensions/.backup/
.specify/extensions/*/*.local.yml
.specify/extensions/.registry
```
### 2. Team Workflows
**For teams**:
1. Agree on which extensions to use
2. Commit extension configuration
3. Document extension usage in README
4. Keep extensions updated together
**Example README section**:
```markdown
## Extensions
This project uses:
- **jira** (v1.0.0) - Jira integration
- Config: `.specify/extensions/jira/jira-config.yml`
- Requires: jira-mcp-server
To install: `specify extension add jira`
```
### 3. Local Development
Use local config for development:
```yaml
# .specify/extensions/jira/jira-config.local.yml
project:
key: "DEVTEST" # Your test project
defaults:
task:
custom_fields:
customfield_10002: 1 # Lower story points for testing
```
### 4. Environment-Specific Config
Use environment variables for CI/CD:
```bash
# .github/workflows/deploy.yml
env:
SPECKIT_JIRA_PROJECT_KEY: ${{ secrets.JIRA_PROJECT }}
- name: Create Jira Issues
run: specify extension add jira && ...
```
### 5. Extension Updates
**Check for updates regularly**:
```bash
# Weekly or before major releases
specify extension update
```
**Pin versions for stability**:
```yaml
# .specify/extensions.yml
installed:
- id: jira
version: "1.0.0" # Pin to specific version
```
### 6. Minimal Extensions
Only install extensions you actively use:
- Reduces complexity
- Faster command loading
- Less configuration
### 7. Documentation
Document extension usage in your project:
```markdown
# PROJECT.md
## Working with Jira
After creating tasks, sync to Jira:
1. Run `/speckit.tasks` to generate tasks
2. Run `/speckit.jira.specstoissues` to create Jira issues
3. Run `/speckit.jira.sync-status` to update status
```
---
## FAQ
### Q: Can I use multiple extensions at once?
**A**: Yes! Extensions are designed to work together. Install as many as you need.
### Q: Do extensions slow down spec-kit?
**A**: No. Extensions are loaded on-demand and only when their commands are used.
### Q: Can I create private extensions?
**A**: Yes. Install with `--dev` or `--from` and keep private. Public catalog submission is optional.
### Q: How do I know if an extension is safe?
**A**: Look for the ✓ Verified badge. Verified extensions are reviewed by maintainers. Always review extension code before installing.
### Q: Can extensions modify spec-kit core?
**A**: No. Extensions can only add commands and hooks. They cannot modify core functionality.
### Q: What happens if two extensions have the same command name?
**A**: Extensions use namespaced commands (`speckit.{extension}.{command}`), so conflicts are very rare. The extension system will warn you if conflicts occur.
### Q: Can I contribute to existing extensions?
**A**: Yes! Most extensions are open source. Check the repository link in `specify extension info {extension}`.
### Q: How do I report extension bugs?
**A**: Go to the extension's repository (shown in `specify extension info`) and create an issue.
### Q: Can extensions work offline?
**A**: Once installed, extensions work offline. However, some extensions may require internet for their functionality (e.g., Jira requires Jira API access).
### Q: How do I backup my extension configuration?
**A**: Extension configs are in `.specify/extensions/{extension}/`. Back up this directory or commit configs to git.
---
## Support
- **Extension Issues**: Report to extension repository (see `specify extension info`)
- **Spec Kit Issues**: <https://github.com/statsperform/spec-kit/issues>
- **Extension Catalog**: <https://github.com/statsperform/spec-kit/tree/main/extensions>
- **Documentation**: See EXTENSION-DEVELOPMENT-GUIDE.md and EXTENSION-PUBLISHING-GUIDE.md
---
*Last Updated: 2026-01-28*
*Spec Kit Version: 0.1.0*

File diff suppressed because it is too large Load Diff

View File

@@ -1,60 +0,0 @@
{
"schema_version": "1.0",
"updated_at": "2026-02-03T00:00:00Z",
"catalog_url": "https://your-org.example.com/speckit/catalog.json",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"description": "Create Jira Epics, Stories, and Issues from spec-kit artifacts",
"author": "Your Organization",
"version": "2.1.0",
"download_url": "https://github.com/your-org/spec-kit-jira/archive/refs/tags/v2.1.0.zip",
"repository": "https://github.com/your-org/spec-kit-jira",
"homepage": "https://github.com/your-org/spec-kit-jira",
"documentation": "https://github.com/your-org/spec-kit-jira/blob/main/README.md",
"changelog": "https://github.com/your-org/spec-kit-jira/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "atlassian",
"version": ">=1.0.0",
"required": true
}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": ["jira", "atlassian", "issue-tracking"],
"verified": true,
"downloads": 0,
"stars": 0,
"created_at": "2026-01-28T00:00:00Z",
"updated_at": "2026-02-03T00:00:00Z"
},
"linear": {
"name": "Linear Integration",
"id": "linear",
"description": "Sync specs and tasks with Linear issues",
"author": "Your Organization",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-linear/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/your-org/spec-kit-linear",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2
},
"tags": ["linear", "issue-tracking"],
"verified": false,
"created_at": "2026-01-30T00:00:00Z",
"updated_at": "2026-01-30T00:00:00Z"
}
}
}

View File

@@ -1,6 +0,0 @@
{
"schema_version": "1.0",
"updated_at": "2026-02-03T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
"extensions": {}
}

View File

@@ -1,39 +0,0 @@
# Local configuration overrides
*-config.local.yml
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Build artifacts
dist/
build/
*.egg-info/
# Temporary files
*.tmp
.cache/

View File

@@ -1,39 +0,0 @@
# Changelog
All notable changes to this extension will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Planned
- Feature ideas for future versions
- Enhancements
- Bug fixes
## [1.0.0] - YYYY-MM-DD
### Added
- Initial release of extension
- Command: `/speckit.my-extension.example` - Example command functionality
- Configuration system with template
- Documentation and examples
### Features
- Feature 1 description
- Feature 2 description
- Feature 3 description
### Requirements
- Spec Kit: >=0.1.0
- External dependencies (if any)
---
[Unreleased]: https://github.com/your-org/spec-kit-my-extension/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/your-org/spec-kit-my-extension/releases/tag/v1.0.0

View File

@@ -1,158 +0,0 @@
# EXAMPLE: Extension README
This is an example of what your extension README should look like after customization.
**Delete this file and replace README.md with content similar to this.**
---
# My Extension
<!-- CUSTOMIZE: Replace with your extension description -->
Brief description of what your extension does and why it's useful.
## Features
<!-- CUSTOMIZE: List key features -->
- Feature 1: Description
- Feature 2: Description
- Feature 3: Description
## Installation
```bash
# Install from catalog
specify extension add my-extension
# Or install from local development directory
specify extension add --dev /path/to/my-extension
```
## Configuration
1. Create configuration file:
```bash
cp .specify/extensions/my-extension/config-template.yml \
.specify/extensions/my-extension/my-extension-config.yml
```
2. Edit configuration:
```bash
vim .specify/extensions/my-extension/my-extension-config.yml
```
3. Set required values:
<!-- CUSTOMIZE: List required configuration -->
```yaml
connection:
url: "https://api.example.com"
api_key: "your-api-key"
project:
id: "your-project-id"
```
## Usage
<!-- CUSTOMIZE: Add usage examples -->
### Command: example
Description of what this command does.
```bash
# In Claude Code
> /speckit.my-extension.example
```
**Prerequisites**:
- Prerequisite 1
- Prerequisite 2
**Output**:
- What this command produces
- Where results are saved
## Configuration Reference
<!-- CUSTOMIZE: Document all configuration options -->
### Connection Settings
| Setting | Type | Required | Description |
|---------|------|----------|-------------|
| `connection.url` | string | Yes | API endpoint URL |
| `connection.api_key` | string | Yes | API authentication key |
### Project Settings
| Setting | Type | Required | Description |
|---------|------|----------|-------------|
| `project.id` | string | Yes | Project identifier |
| `project.workspace` | string | No | Workspace or organization |
## Environment Variables
Override configuration with environment variables:
```bash
# Override connection settings
export SPECKIT_MY_EXTENSION_CONNECTION_URL="https://custom-api.com"
export SPECKIT_MY_EXTENSION_CONNECTION_API_KEY="custom-key"
```
## Examples
<!-- CUSTOMIZE: Add real-world examples -->
### Example 1: Basic Workflow
```bash
# Step 1: Create specification
> /speckit.spec
# Step 2: Generate tasks
> /speckit.tasks
# Step 3: Use extension
> /speckit.my-extension.example
```
## Troubleshooting
<!-- CUSTOMIZE: Add common issues -->
### Issue: Configuration not found
**Solution**: Create config from template (see Configuration section)
### Issue: Command not available
**Solutions**:
1. Check extension is installed: `specify extension list`
2. Restart AI agent
3. Reinstall extension
## License
MIT License - see LICENSE file
## Support
- **Issues**: <https://github.com/your-org/spec-kit-my-extension/issues>
- **Spec Kit Docs**: <https://github.com/statsperform/spec-kit>
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history.
---
*Extension Version: 1.0.0*
*Spec Kit: >=0.1.0*

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 [Your Name or Organization]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,79 +0,0 @@
# Extension Template
Starter template for creating a Spec Kit extension.
## Quick Start
1. **Copy this template**:
```bash
cp -r extensions/template my-extension
cd my-extension
```
2. **Customize `extension.yml`**:
- Change extension ID, name, description
- Update author and repository
- Define your commands
3. **Create commands**:
- Add command files in `commands/` directory
- Use Markdown format with YAML frontmatter
4. **Create config template**:
- Define configuration options
- Document all settings
5. **Write documentation**:
- Update README.md with usage instructions
- Add examples
6. **Test locally**:
```bash
cd /path/to/spec-kit-project
specify extension add --dev /path/to/my-extension
```
7. **Publish** (optional):
- Create GitHub repository
- Create release
- Submit to catalog (see EXTENSION-PUBLISHING-GUIDE.md)
## Files in This Template
- `extension.yml` - Extension manifest (CUSTOMIZE THIS)
- `config-template.yml` - Configuration template (CUSTOMIZE THIS)
- `commands/example.md` - Example command (REPLACE THIS)
- `README.md` - Extension documentation (REPLACE THIS)
- `LICENSE` - MIT License (REVIEW THIS)
- `CHANGELOG.md` - Version history (UPDATE THIS)
- `.gitignore` - Git ignore rules
## Customization Checklist
- [ ] Update `extension.yml` with your extension details
- [ ] Change extension ID to your extension name
- [ ] Update author information
- [ ] Define your commands
- [ ] Create command files in `commands/`
- [ ] Update config template
- [ ] Write README with usage instructions
- [ ] Add examples
- [ ] Update LICENSE if needed
- [ ] Test extension locally
- [ ] Create git repository
- [ ] Create first release
## Need Help?
- **Development Guide**: See EXTENSION-DEVELOPMENT-GUIDE.md
- **API Reference**: See EXTENSION-API-REFERENCE.md
- **Publishing Guide**: See EXTENSION-PUBLISHING-GUIDE.md
- **User Guide**: See EXTENSION-USER-GUIDE.md
## Template Version
- Version: 1.0.0
- Last Updated: 2026-01-28
- Compatible with Spec Kit: >=0.1.0

View File

@@ -1,210 +0,0 @@
---
description: "Example command that demonstrates extension functionality"
# CUSTOMIZE: List MCP tools this command uses
tools:
- 'example-mcp-server/example_tool'
---
# Example Command
<!-- CUSTOMIZE: Replace this entire file with your command documentation -->
This is an example command that demonstrates how to create commands for Spec Kit extensions.
## Purpose
Describe what this command does and when to use it.
## Prerequisites
List requirements before using this command:
1. Prerequisite 1 (e.g., "MCP server configured")
2. Prerequisite 2 (e.g., "Configuration file exists")
3. Prerequisite 3 (e.g., "Valid API credentials")
## User Input
$ARGUMENTS
## Steps
### Step 1: Load Configuration
<!-- CUSTOMIZE: Replace with your actual steps -->
Load extension configuration from the project:
``bash
config_file=".specify/extensions/my-extension/my-extension-config.yml"
if [ ! -f "$config_file" ]; then
echo "❌ Error: Configuration not found at $config_file"
echo "Run 'specify extension add my-extension' to install and configure"
exit 1
fi
# Read configuration values
setting_value=$(yq eval '.settings.key' "$config_file")
# Apply environment variable overrides
setting_value="${SPECKIT_MY_EXTENSION_KEY:-$setting_value}"
# Validate configuration
if [ -z "$setting_value" ]; then
echo "❌ Error: Configuration value not set"
echo "Edit $config_file and set 'settings.key'"
exit 1
fi
echo "📋 Configuration loaded: $setting_value"
``
### Step 2: Perform Main Action
<!-- CUSTOMIZE: Replace with your command logic -->
Describe what this step does:
``markdown
Use MCP tools to perform the main action:
- Tool: example-mcp-server example_tool
- Parameters: { "key": "$setting_value" }
This calls the MCP server tool to execute the operation.
``
### Step 3: Process Results
<!-- CUSTOMIZE: Add more steps as needed -->
Process the results and provide output:
`` bash
echo ""
echo "✅ Command completed successfully!"
echo ""
echo "Results:"
echo " • Item 1: Value"
echo " • Item 2: Value"
echo ""
``
### Step 4: Save Output (Optional)
Save results to a file if needed:
``bash
output_file=".specify/my-extension-output.json"
cat > "$output_file" <<EOF
{
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"setting": "$setting_value",
"results": []
}
EOF
echo "💾 Output saved to $output_file"
``
## Configuration Reference
<!-- CUSTOMIZE: Document configuration options -->
This command uses the following configuration from `my-extension-config.yml`:
- **settings.key**: Description of what this setting does
- Type: string
- Required: Yes
- Example: `"example-value"`
- **settings.another_key**: Description of another setting
- Type: boolean
- Required: No
- Default: `false`
- Example: `true`
## Environment Variables
<!-- CUSTOMIZE: Document environment variable overrides -->
Configuration can be overridden with environment variables:
- `SPECKIT_MY_EXTENSION_KEY` - Overrides `settings.key`
- `SPECKIT_MY_EXTENSION_ANOTHER_KEY` - Overrides `settings.another_key`
Example:
``bash
export SPECKIT_MY_EXTENSION_KEY="override-value"
``
## Troubleshooting
<!-- CUSTOMIZE: Add common issues and solutions -->
### "Configuration not found"
**Solution**: Install the extension and create configuration:
``bash
specify extension add my-extension
cp .specify/extensions/my-extension/config-template.yml \
.specify/extensions/my-extension/my-extension-config.yml
``
### "MCP tool not available"
**Solution**: Ensure MCP server is configured in your AI agent settings.
### "Permission denied"
**Solution**: Check credentials and permissions in the external service.
## Notes
<!-- CUSTOMIZE: Add helpful notes and tips -->
- This command requires an active connection to the external service
- Results are cached for performance
- Re-run the command to refresh data
## Examples
<!-- CUSTOMIZE: Add usage examples -->
### Example 1: Basic Usage
``bash
# Run with default configuration
>
> /speckit.my-extension.example
``
### Example 2: With Environment Override
``bash
# Override configuration with environment variable
export SPECKIT_MY_EXTENSION_KEY="custom-value"
> /speckit.my-extension.example
``
### Example 3: After Core Command
``bash
# Use as part of a workflow
>
> /speckit.tasks
> /speckit.my-extension.example
``
---
*For more information, see the extension README or run `specify extension info my-extension`*

View File

@@ -1,75 +0,0 @@
# Extension Configuration Template
# Copy this to my-extension-config.yml and customize for your project
# CUSTOMIZE: Add your configuration sections below
# Example: Connection settings
connection:
# URL to external service
url: "" # REQUIRED: e.g., "https://api.example.com"
# API key or token
api_key: "" # REQUIRED: Your API key
# Example: Project settings
project:
# Project identifier
id: "" # REQUIRED: e.g., "my-project"
# Workspace or organization
workspace: "" # OPTIONAL: e.g., "my-org"
# Example: Feature flags
features:
# Enable/disable main functionality
enabled: true
# Automatic synchronization
auto_sync: false
# Verbose logging
verbose: false
# Example: Default values
defaults:
# Labels to apply
labels: [] # e.g., ["automated", "spec-kit"]
# Priority level
priority: "medium" # Options: "low", "medium", "high"
# Assignee
assignee: "" # OPTIONAL: Default assignee
# Example: Field mappings
# Map internal names to external field IDs
field_mappings:
# Example mappings
# internal_field: "external_field_id"
# status: "customfield_10001"
# Example: Advanced settings
advanced:
# Timeout in seconds
timeout: 30
# Retry attempts
retry_count: 3
# Cache duration in seconds
cache_duration: 3600
# Environment Variable Overrides:
# You can override any setting with environment variables using this pattern:
# SPECKIT_MY_EXTENSION_{SECTION}_{KEY}
#
# Examples:
# - SPECKIT_MY_EXTENSION_CONNECTION_API_KEY: Override connection.api_key
# - SPECKIT_MY_EXTENSION_PROJECT_ID: Override project.id
# - SPECKIT_MY_EXTENSION_FEATURES_ENABLED: Override features.enabled
#
# Note: Use uppercase and replace dots with underscores
# Local Overrides:
# For local development, create my-extension-config.local.yml (gitignored)
# to override settings without affecting the team configuration

View File

@@ -1,97 +0,0 @@
schema_version: "1.0"
extension:
# CUSTOMIZE: Change 'my-extension' to your extension ID (lowercase, hyphen-separated)
id: "my-extension"
# CUSTOMIZE: Human-readable name for your extension
name: "My Extension"
# CUSTOMIZE: Update version when releasing (semantic versioning: X.Y.Z)
version: "1.0.0"
# CUSTOMIZE: Brief description (under 200 characters)
description: "Brief description of what your extension does"
# CUSTOMIZE: Your name or organization name
author: "Your Name"
# CUSTOMIZE: GitHub repository URL (create before publishing)
repository: "https://github.com/your-org/spec-kit-my-extension"
# REVIEW: License (MIT is recommended for open source)
license: "MIT"
# CUSTOMIZE: Extension homepage (can be same as repository)
homepage: "https://github.com/your-org/spec-kit-my-extension"
# Requirements for this extension
requires:
# CUSTOMIZE: Minimum spec-kit version required
# Use >=X.Y.Z for minimum version
# Use >=X.Y.Z,<Y.0.0 for version range
speckit_version: ">=0.1.0"
# CUSTOMIZE: Add MCP tools or other dependencies
# Remove if no external tools required
tools:
- name: "example-mcp-server"
version: ">=1.0.0"
required: true
# Commands provided by this extension
provides:
commands:
# CUSTOMIZE: Define your commands
# Pattern: speckit.{extension-id}.{command-name}
- name: "speckit.my-extension.example"
file: "commands/example.md"
description: "Example command that demonstrates functionality"
# Optional: Add aliases for shorter command names
aliases: ["speckit.example"]
# ADD MORE COMMANDS: Copy this block for each command
# - name: "speckit.my-extension.another-command"
# file: "commands/another-command.md"
# description: "Another command"
# CUSTOMIZE: Define configuration files
config:
- name: "my-extension-config.yml"
template: "config-template.yml"
description: "Extension configuration"
required: true # Set to false if config is optional
# CUSTOMIZE: Define hooks (optional)
# Remove if no hooks needed
hooks:
# Hook that runs after /speckit.tasks
after_tasks:
command: "speckit.my-extension.example"
optional: true # User will be prompted
prompt: "Run example command?"
description: "Demonstrates hook functionality"
condition: null # Future: conditional execution
# ADD MORE HOOKS: Copy this block for other events
# after_implement:
# command: "speckit.my-extension.another"
# optional: false # Auto-execute without prompting
# description: "Runs automatically after implementation"
# CUSTOMIZE: Add relevant tags (2-5 recommended)
# Used for discovery in catalog
tags:
- "example"
- "template"
# ADD MORE: "category", "tool-name", etc.
# CUSTOMIZE: Default configuration values (optional)
# These are merged with user config
defaults:
# Example default values
feature:
enabled: true
auto_sync: false
# ADD MORE: Any default settings for your extension

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "specify-cli" name = "specify-cli"
version = "0.1.0" version = "0.0.21"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)." description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -10,8 +10,6 @@ dependencies = [
"platformdirs", "platformdirs",
"readchar", "readchar",
"truststore>=0.10.4", "truststore>=0.10.4",
"pyyaml>=6.0",
"packaging>=23.0",
] ]
[project.scripts] [project.scripts]
@@ -24,30 +22,3 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/specify_cli"] packages = ["src/specify_cli"]
[project.optional-dependencies]
test = [
"pytest>=7.0",
"pytest-cov>=4.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--tb=short",
]
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/__pycache__/*"]
[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false

View File

@@ -75,7 +75,7 @@ EOF
done done
# Source common functions # Source common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh" source "$SCRIPT_DIR/common.sh"
# Get feature paths and validate branch # Get feature paths and validate branch

View File

@@ -7,7 +7,7 @@ get_repo_root() {
git rev-parse --show-toplevel git rev-parse --show-toplevel
else else
# Fall back to script location for non-git repos # Fall back to script location for non-git repos
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
(cd "$script_dir/../../.." && pwd) (cd "$script_dir/../../.." && pwd)
fi fi
} }

View File

@@ -4,7 +4,6 @@ set -e
JSON_MODE=false JSON_MODE=false
SHORT_NAME="" SHORT_NAME=""
BRANCH_NUMBER=""
ARGS=() ARGS=()
i=1 i=1
while [ $i -le $# ]; do while [ $i -le $# ]; do
@@ -27,31 +26,17 @@ while [ $i -le $# ]; do
fi fi
SHORT_NAME="$next_arg" SHORT_NAME="$next_arg"
;; ;;
--number)
if [ $((i + 1)) -gt $# ]; then
echo 'Error: --number requires a value' >&2
exit 1
fi
i=$((i + 1))
next_arg="${!i}"
if [[ "$next_arg" == --* ]]; then
echo 'Error: --number requires a value' >&2
exit 1
fi
BRANCH_NUMBER="$next_arg"
;;
--help|-h) --help|-h)
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" echo "Usage: $0 [--json] [--short-name <name>] <feature_description>"
echo "" echo ""
echo "Options:" echo "Options:"
echo " --json Output in JSON format" echo " --json Output in JSON format"
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch" echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
echo " --number N Specify branch number manually (overrides auto-detection)"
echo " --help, -h Show this help message" echo " --help, -h Show this help message"
echo "" echo ""
echo "Examples:" echo "Examples:"
echo " $0 'Add user authentication system' --short-name 'user-auth'" echo " $0 'Add user authentication system' --short-name 'user-auth'"
echo " $0 'Implement OAuth2 integration for API' --number 5" echo " $0 'Implement OAuth2 integration for API'"
exit 0 exit 0
;; ;;
*) *)
@@ -63,7 +48,7 @@ done
FEATURE_DESCRIPTION="${ARGS[*]}" FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2 echo "Usage: $0 [--json] [--short-name <name>] <feature_description>" >&2
exit 1 exit 1
fi fi
@@ -80,85 +65,10 @@ find_repo_root() {
return 1 return 1
} }
# Function to get highest number from specs directory
get_highest_from_specs() {
local specs_dir="$1"
local highest=0
if [ -d "$specs_dir" ]; then
for dir in "$specs_dir"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
done
fi
echo "$highest"
}
# Function to get highest number from git branches
get_highest_from_branches() {
local highest=0
# Get all branches (local and remote)
branches=$(git branch -a 2>/dev/null || echo "")
if [ -n "$branches" ]; then
while IFS= read -r branch; do
# Clean branch name: remove leading markers and remote prefixes
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
# Extract feature number if branch matches pattern ###-*
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
fi
done <<< "$branches"
fi
echo "$highest"
}
# Function to check existing branches (local and remote) and return next available number
check_existing_branches() {
local specs_dir="$1"
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
git fetch --all --prune 2>/dev/null || true
# Get highest number from ALL branches (not just matching short name)
local highest_branch=$(get_highest_from_branches)
# Get highest number from ALL specs (not just matching short name)
local highest_spec=$(get_highest_from_specs "$specs_dir")
# Take the maximum of both
local max_num=$highest_branch
if [ "$highest_spec" -gt "$max_num" ]; then
max_num=$highest_spec
fi
# Return next number
echo $((max_num + 1))
}
# Function to clean and format a branch name
clean_branch_name() {
local name="$1"
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
}
# Resolve repository root. Prefer git information when available, but fall back # Resolve repository root. Prefer git information when available, but fall back
# to searching for repository markers so the workflow still functions in repositories that # to searching for repository markers so the workflow still functions in repositories that
# were initialised with --no-git. # were initialised with --no-git.
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if git rev-parse --show-toplevel >/dev/null 2>&1; then if git rev-parse --show-toplevel >/dev/null 2>&1; then
REPO_ROOT=$(git rev-parse --show-toplevel) REPO_ROOT=$(git rev-parse --show-toplevel)
@@ -177,6 +87,48 @@ cd "$REPO_ROOT"
SPECS_DIR="$REPO_ROOT/specs" SPECS_DIR="$REPO_ROOT/specs"
mkdir -p "$SPECS_DIR" mkdir -p "$SPECS_DIR"
# Get highest number from specs directory
HIGHEST_FROM_SPECS=0
if [ -d "$SPECS_DIR" ]; then
for dir in "$SPECS_DIR"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$HIGHEST_FROM_SPECS" ]; then HIGHEST_FROM_SPECS=$number; fi
done
fi
# Get highest number from branch names (both local and remote)
HIGHEST_FROM_BRANCHES=0
if [ "$HAS_GIT" = true ]; then
# Get all branches (local and remote)
branches=$(git branch -a 2>/dev/null || echo "")
if [ -n "$branches" ]; then
while IFS= read -r branch; do
# Clean branch name: remove leading markers and remote prefixes
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
# Extract feature number if branch matches pattern ###-*
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$HIGHEST_FROM_BRANCHES" ]; then HIGHEST_FROM_BRANCHES=$number; fi
fi
done <<< "$branches"
fi
fi
# Use the highest number from either source
HIGHEST=$HIGHEST_FROM_SPECS
if [ "$HIGHEST_FROM_BRANCHES" -gt "$HIGHEST" ]; then
HIGHEST=$HIGHEST_FROM_BRANCHES
fi
NEXT=$((HIGHEST + 1))
FEATURE_NUM=$(printf "%03d" "$NEXT")
# Function to generate branch name with stop word filtering and length filtering # Function to generate branch name with stop word filtering and length filtering
generate_branch_name() { generate_branch_name() {
local description="$1" local description="$1"
@@ -220,34 +172,19 @@ generate_branch_name() {
echo "$result" echo "$result"
else else
# Fallback to original logic if no meaningful words found # Fallback to original logic if no meaningful words found
local cleaned=$(clean_branch_name "$description") echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
fi fi
} }
# Generate branch name # Generate branch name
if [ -n "$SHORT_NAME" ]; then if [ -n "$SHORT_NAME" ]; then
# Use provided short name, just clean it up # Use provided short name, just clean it up
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME") BRANCH_SUFFIX=$(echo "$SHORT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//')
else else
# Generate from description with smart filtering # Generate from description with smart filtering
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
fi fi
# Determine branch number
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$HAS_GIT" = true ]; then
# Check existing branches on remotes
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
else
# Fall back to local directory check
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
BRANCH_NUMBER=$((HIGHEST + 1))
fi
fi
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
# GitHub enforces a 244-byte limit on branch names # GitHub enforces a 244-byte limit on branch names

View File

@@ -24,7 +24,7 @@ for arg in "$@"; do
done done
# Get script directory and load common functions # Get script directory and load common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh" source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions # Get all paths and variables from common functions

View File

@@ -30,12 +30,12 @@
# #
# 5. Multi-Agent Support # 5. Multi-Agent Support
# - Handles agent-specific file paths and naming conventions # - Handles agent-specific file paths and naming conventions
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Amazon Q Developer CLI, or Antigravity # - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Amp, or Amazon Q Developer CLI
# - Can update single agents or all existing agent files # - Can update single agents or all existing agent files
# - Creates default Claude file if no agent files exist # - Creates default Claude file if no agent files exist
# #
# Usage: ./update-agent-context.sh [agent_type] # Usage: ./update-agent-context.sh [agent_type]
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qoder # Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|q
# Leave empty to update all existing agent files # Leave empty to update all existing agent files
set -e set -e
@@ -49,7 +49,7 @@ set -o pipefail
#============================================================================== #==============================================================================
# Get script directory and load common functions # Get script directory and load common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh" source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions # Get all paths and variables from common functions
@@ -61,7 +61,7 @@ AGENT_TYPE="${1:-}"
# Agent-specific file paths # Agent-specific file paths
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
GEMINI_FILE="$REPO_ROOT/GEMINI.md" GEMINI_FILE="$REPO_ROOT/GEMINI.md"
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md" COPILOT_FILE="$REPO_ROOT/.github/chatmodes/copilot-instructions.md"
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
QWEN_FILE="$REPO_ROOT/QWEN.md" QWEN_FILE="$REPO_ROOT/QWEN.md"
AGENTS_FILE="$REPO_ROOT/AGENTS.md" AGENTS_FILE="$REPO_ROOT/AGENTS.md"
@@ -70,12 +70,8 @@ KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
QODER_FILE="$REPO_ROOT/QODER.md"
AMP_FILE="$REPO_ROOT/AGENTS.md" AMP_FILE="$REPO_ROOT/AGENTS.md"
SHAI_FILE="$REPO_ROOT/SHAI.md"
Q_FILE="$REPO_ROOT/AGENTS.md" Q_FILE="$REPO_ROOT/AGENTS.md"
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
BOB_FILE="$REPO_ROOT/AGENTS.md"
# Template file # Template file
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
@@ -619,27 +615,15 @@ update_specific_agent() {
codebuddy) codebuddy)
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
;; ;;
qoder)
update_agent_file "$QODER_FILE" "Qoder CLI"
;;
amp) amp)
update_agent_file "$AMP_FILE" "Amp" update_agent_file "$AMP_FILE" "Amp"
;; ;;
shai)
update_agent_file "$SHAI_FILE" "SHAI"
;;
q) q)
update_agent_file "$Q_FILE" "Amazon Q Developer CLI" update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
;; ;;
agy)
update_agent_file "$AGY_FILE" "Antigravity"
;;
bob)
update_agent_file "$BOB_FILE" "IBM Bob"
;;
*) *)
log_error "Unknown agent type '$agent_type'" log_error "Unknown agent type '$agent_type'"
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|amp|shai|q|agy|bob|qoder" log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|amp|q"
exit 1 exit 1
;; ;;
esac esac
@@ -704,30 +688,11 @@ update_all_existing_agents() {
found_agent=true found_agent=true
fi fi
if [[ -f "$SHAI_FILE" ]]; then
update_agent_file "$SHAI_FILE" "SHAI"
found_agent=true
fi
if [[ -f "$QODER_FILE" ]]; then
update_agent_file "$QODER_FILE" "Qoder CLI"
found_agent=true
fi
if [[ -f "$Q_FILE" ]]; then if [[ -f "$Q_FILE" ]]; then
update_agent_file "$Q_FILE" "Amazon Q Developer CLI" update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
found_agent=true found_agent=true
fi fi
if [[ -f "$AGY_FILE" ]]; then
update_agent_file "$AGY_FILE" "Antigravity"
found_agent=true
fi
if [[ -f "$BOB_FILE" ]]; then
update_agent_file "$BOB_FILE" "IBM Bob"
found_agent=true
fi
# If no agent files exist, create a default Claude file # If no agent files exist, create a default Claude file
if [[ "$found_agent" == false ]]; then if [[ "$found_agent" == false ]]; then
log_info "No existing agent files found, creating default Claude file..." log_info "No existing agent files found, creating default Claude file..."
@@ -752,7 +717,7 @@ print_summary() {
echo echo
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qoder]" log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|codebuddy|q]"
} }
#============================================================================== #==============================================================================

View File

@@ -4,7 +4,6 @@
param( param(
[switch]$Json, [switch]$Json,
[string]$ShortName, [string]$ShortName,
[int]$Number = 0,
[switch]$Help, [switch]$Help,
[Parameter(ValueFromRemainingArguments = $true)] [Parameter(ValueFromRemainingArguments = $true)]
[string[]]$FeatureDescription [string[]]$FeatureDescription
@@ -13,12 +12,11 @@ $ErrorActionPreference = 'Stop'
# Show help if requested # Show help if requested
if ($Help) { if ($Help) {
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>" Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] <feature description>"
Write-Host "" Write-Host ""
Write-Host "Options:" Write-Host "Options:"
Write-Host " -Json Output in JSON format" Write-Host " -Json Output in JSON format"
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch" Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
Write-Host " -Help Show this help message" Write-Host " -Help Show this help message"
Write-Host "" Write-Host ""
Write-Host "Examples:" Write-Host "Examples:"
@@ -58,77 +56,6 @@ function Find-RepositoryRoot {
$current = $parent $current = $parent
} }
} }
function Get-HighestNumberFromSpecs {
param([string]$SpecsDir)
$highest = 0
if (Test-Path $SpecsDir) {
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d+)') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
}
return $highest
}
function Get-HighestNumberFromBranches {
param()
$highest = 0
try {
$branches = git branch -a 2>$null
if ($LASTEXITCODE -eq 0) {
foreach ($branch in $branches) {
# Clean branch name: remove leading markers and remote prefixes
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
# Extract feature number if branch matches pattern ###-*
if ($cleanBranch -match '^(\d+)-') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
}
} catch {
# If git command fails, return 0
Write-Verbose "Could not check Git branches: $_"
}
return $highest
}
function Get-NextBranchNumber {
param(
[string]$SpecsDir
)
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
try {
git fetch --all --prune 2>$null | Out-Null
} catch {
# Ignore fetch errors
}
# Get highest number from ALL branches (not just matching short name)
$highestBranch = Get-HighestNumberFromBranches
# Get highest number from ALL specs (not just matching short name)
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir
# Take the maximum of both
$maxNum = [Math]::Max($highestBranch, $highestSpec)
# Return next number
return $maxNum + 1
}
function ConvertTo-CleanBranchName {
param([string]$Name)
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
}
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot) $fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
if (-not $fallbackRoot) { if (-not $fallbackRoot) {
Write-Error "Error: Could not determine repository root. Please run this script from within the repository." Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
@@ -152,6 +79,43 @@ Set-Location $repoRoot
$specsDir = Join-Path $repoRoot 'specs' $specsDir = Join-Path $repoRoot 'specs'
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
# Get highest number from specs directory
$highestFromSpecs = 0
if (Test-Path $specsDir) {
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d{3})') {
$num = [int]$matches[1]
if ($num -gt $highestFromSpecs) { $highestFromSpecs = $num }
}
}
}
# Get highest number from branch names (both local and remote)
$highestFromBranches = 0
try {
$branches = git branch -a 2>$null
if ($LASTEXITCODE -eq 0) {
foreach ($branch in $branches) {
# Clean branch name: remove leading markers and remote prefixes
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
# Extract feature number if branch matches pattern ###-*
if ($cleanBranch -match '^(\d{3})-') {
$num = [int]$matches[1]
if ($num -gt $highestFromBranches) { $highestFromBranches = $num }
}
}
}
} catch {
# If git command fails, just continue with specs-only check
Write-Verbose "Could not check Git branches: $_"
}
# Use the highest number from either source
$highest = [Math]::Max($highestFromSpecs, $highestFromBranches)
$next = $highest + 1
$featureNum = ('{0:000}' -f $next)
# Function to generate branch name with stop word filtering and length filtering # Function to generate branch name with stop word filtering and length filtering
function Get-BranchName { function Get-BranchName {
param([string]$Description) param([string]$Description)
@@ -191,7 +155,7 @@ function Get-BranchName {
return $result return $result
} else { } else {
# Fallback to original logic if no meaningful words found # Fallback to original logic if no meaningful words found
$result = ConvertTo-CleanBranchName -Name $Description $result = $Description.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
$fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3 $fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3
return [string]::Join('-', $fallbackWords) return [string]::Join('-', $fallbackWords)
} }
@@ -200,24 +164,12 @@ function Get-BranchName {
# Generate branch name # Generate branch name
if ($ShortName) { if ($ShortName) {
# Use provided short name, just clean it up # Use provided short name, just clean it up
$branchSuffix = ConvertTo-CleanBranchName -Name $ShortName $branchSuffix = $ShortName.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
} else { } else {
# Generate from description with smart filtering # Generate from description with smart filtering
$branchSuffix = Get-BranchName -Description $featureDesc $branchSuffix = Get-BranchName -Description $featureDesc
} }
# Determine branch number
if ($Number -eq 0) {
if ($hasGit) {
# Check existing branches on remotes
$Number = Get-NextBranchNumber -SpecsDir $specsDir
} else {
# Fall back to local directory check
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
}
}
$featureNum = ('{0:000}' -f $Number)
$branchName = "$featureNum-$branchSuffix" $branchName = "$featureNum-$branchSuffix"
# GitHub enforces a 244-byte limit on branch names # GitHub enforces a 244-byte limit on branch names

View File

@@ -59,3 +59,4 @@ if ($Json) {
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)" Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
Write-Output "HAS_GIT: $($paths.HAS_GIT)" Write-Output "HAS_GIT: $($paths.HAS_GIT)"
} }

View File

@@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh:
2. Plan Data Extraction 2. Plan Data Extraction
3. Agent File Management (create from template or update existing) 3. Agent File Management (create from template or update existing)
4. Content Generation (technology stack, recent changes, timestamp) 4. Content Generation (technology stack, recent changes, timestamp)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, q, agy, bob, qoder) 5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, amp, q)
.PARAMETER AgentType .PARAMETER AgentType
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist). Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
@@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1
#> #>
param( param(
[Parameter(Position=0)] [Parameter(Position=0)]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','q','agy','bob','qoder')] [ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','q')]
[string]$AgentType [string]$AgentType
) )
@@ -46,7 +46,7 @@ $NEW_PLAN = $IMPL_PLAN
# Agent file paths # Agent file paths
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md' $CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md' $GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/copilot-instructions.md' $COPILOT_FILE = Join-Path $REPO_ROOT '.github/chatmodes/copilot-instructions.md'
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc' $CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md' $QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
@@ -55,12 +55,8 @@ $KILOCODE_FILE = Join-Path $REPO_ROOT '.kilocode/rules/specify-rules.md'
$AUGGIE_FILE = Join-Path $REPO_ROOT '.augment/rules/specify-rules.md' $AUGGIE_FILE = Join-Path $REPO_ROOT '.augment/rules/specify-rules.md'
$ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md' $ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md'
$CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md' $CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md'
$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
$Q_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $Q_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md' $TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
@@ -384,13 +380,9 @@ function Update-SpecificAgent {
'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' } 'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' }
'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' } 'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' }
'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI' } 'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI' }
'qoder' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' } 'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
'q' { Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI' } 'q' { Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI' }
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' } default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|q'; return $false }
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qoder'; return $false }
} }
} }
@@ -408,11 +400,7 @@ function Update-AllExistingAgents {
if (Test-Path $AUGGIE_FILE) { if (-not (Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }; $found = $true } if (Test-Path $AUGGIE_FILE) { if (-not (Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }; $found = $true }
if (Test-Path $ROO_FILE) { if (-not (Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }; $found = $true } if (Test-Path $ROO_FILE) { if (-not (Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }; $found = $true }
if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true } if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true }
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
if (Test-Path $Q_FILE) { if (-not (Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI')) { $ok = $false }; $found = $true } if (Test-Path $Q_FILE) { if (-not (Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI')) { $ok = $false }; $found = $true }
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true }
if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
if (-not $found) { if (-not $found) {
Write-Info 'No existing agent files found, creating default Claude file...' Write-Info 'No existing agent files found, creating default Claude file...'
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false } if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
@@ -427,7 +415,7 @@ function Print-Summary {
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" } if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" } if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
Write-Host '' Write-Host ''
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qoder]' Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|q]'
} }
function Main { function Main {

View File

@@ -195,7 +195,6 @@ The templates include comprehensive checklists that act as "unit tests" for the
```markdown ```markdown
### Requirement Completeness ### Requirement Completeness
- [ ] No [NEEDS CLARIFICATION] markers remain - [ ] No [NEEDS CLARIFICATION] markers remain
- [ ] Requirements are testable and unambiguous - [ ] Requirements are testable and unambiguous
- [ ] Success criteria are measurable - [ ] Success criteria are measurable
@@ -209,14 +208,10 @@ The implementation plan template enforces architectural principles through phase
```markdown ```markdown
### Phase -1: Pre-Implementation Gates ### Phase -1: Pre-Implementation Gates
#### Simplicity Gate (Article VII) #### Simplicity Gate (Article VII)
- [ ] Using ≤3 projects? - [ ] Using ≤3 projects?
- [ ] No future-proofing? - [ ] No future-proofing?
#### Anti-Abstraction Gate (Article VIII) #### Anti-Abstraction Gate (Article VIII)
- [ ] Using framework directly? - [ ] Using framework directly?
- [ ] Single model representation? - [ ] Single model representation?
``` ```
@@ -352,19 +347,15 @@ The implementation plan template operationalizes these articles through concrete
```markdown ```markdown
### Phase -1: Pre-Implementation Gates ### Phase -1: Pre-Implementation Gates
#### Simplicity Gate (Article VII) #### Simplicity Gate (Article VII)
- [ ] Using ≤3 projects? - [ ] Using ≤3 projects?
- [ ] No future-proofing? - [ ] No future-proofing?
#### Anti-Abstraction Gate (Article VIII) #### Anti-Abstraction Gate (Article VIII)
- [ ] Using framework directly? - [ ] Using framework directly?
- [ ] Single model representation? - [ ] Single model representation?
#### Integration-First Gate (Article IX) #### Integration-First Gate (Article IX)
- [ ] Contracts defined? - [ ] Contracts defined?
- [ ] Contract tests written? - [ ] Contract tests written?
``` ```

View File

@@ -1,8 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

@@ -190,12 +190,6 @@ AGENT_CONFIG = {
"install_url": "https://www.codebuddy.ai/cli", "install_url": "https://www.codebuddy.ai/cli",
"requires_cli": True, "requires_cli": True,
}, },
"qoder": {
"name": "Qoder CLI",
"folder": ".qoder/",
"install_url": "https://qoder.com/cli",
"requires_cli": True,
},
"roo": { "roo": {
"name": "Roo Code", "name": "Roo Code",
"folder": ".roo/", "folder": ".roo/",
@@ -214,24 +208,6 @@ AGENT_CONFIG = {
"install_url": "https://ampcode.com/manual#install", "install_url": "https://ampcode.com/manual#install",
"requires_cli": True, "requires_cli": True,
}, },
"shai": {
"name": "SHAI",
"folder": ".shai/",
"install_url": "https://github.com/ovh/shai",
"requires_cli": True,
},
"agy": {
"name": "Antigravity",
"folder": ".agent/",
"install_url": None, # IDE-based
"requires_cli": False,
},
"bob": {
"name": "IBM Bob",
"folder": ".bob/",
"install_url": None, # IDE-based
"requires_cli": False,
},
} }
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
@@ -948,45 +924,10 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
for f in failures: for f in failures:
console.print(f" - {f}") console.print(f" - {f}")
def ensure_constitution_from_template(project_path: Path, tracker: StepTracker | None = None) -> None:
"""Copy constitution template to memory if it doesn't exist (preserves existing constitution on reinitialization)."""
memory_constitution = project_path / ".specify" / "memory" / "constitution.md"
template_constitution = project_path / ".specify" / "templates" / "constitution-template.md"
# If constitution already exists in memory, preserve it
if memory_constitution.exists():
if tracker:
tracker.add("constitution", "Constitution setup")
tracker.skip("constitution", "existing file preserved")
return
# If template doesn't exist, something went wrong with extraction
if not template_constitution.exists():
if tracker:
tracker.add("constitution", "Constitution setup")
tracker.error("constitution", "template not found")
return
# Copy template to memory directory
try:
memory_constitution.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(template_constitution, memory_constitution)
if tracker:
tracker.add("constitution", "Constitution setup")
tracker.complete("constitution", "copied from template")
else:
console.print(f"[cyan]Initialized constitution from template[/cyan]")
except Exception as e:
if tracker:
tracker.add("constitution", "Constitution setup")
tracker.error("constitution", str(e))
else:
console.print(f"[yellow]Warning: Could not initialize constitution: {e}[/yellow]")
@app.command() @app.command()
def init( def init(
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"), project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, q, agy, bob, or qoder "), ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, or q"),
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"), script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"), ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
@@ -1149,7 +1090,6 @@ def init(
("zip-list", "Archive contents"), ("zip-list", "Archive contents"),
("extracted-summary", "Extraction summary"), ("extracted-summary", "Extraction summary"),
("chmod", "Ensure scripts executable"), ("chmod", "Ensure scripts executable"),
("constitution", "Constitution setup"),
("cleanup", "Cleanup"), ("cleanup", "Cleanup"),
("git", "Initialize git repository"), ("git", "Initialize git repository"),
("final", "Finalize") ("final", "Finalize")
@@ -1170,8 +1110,6 @@ def init(
ensure_executable_scripts(project_path, tracker=tracker) ensure_executable_scripts(project_path, tracker=tracker)
ensure_constitution_from_template(project_path, tracker=tracker)
if not no_git: if not no_git:
tracker.start("git") tracker.start("git")
if is_git_repo(project_path): if is_git_repo(project_path):
@@ -1390,7 +1328,7 @@ def version():
info_table.add_row("Template Version", template_version) info_table.add_row("Template Version", template_version)
info_table.add_row("Released", release_date) info_table.add_row("Released", release_date)
info_table.add_row("", "") info_table.add_row("", "")
info_table.add_row("Python", platform.python_version()) info_table.add_row("Python", f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")
info_table.add_row("Platform", platform.system()) info_table.add_row("Platform", platform.system())
info_table.add_row("Architecture", platform.machine()) info_table.add_row("Architecture", platform.machine())
info_table.add_row("OS Version", platform.version()) info_table.add_row("OS Version", platform.version())
@@ -1405,641 +1343,6 @@ def version():
console.print(panel) console.print(panel)
console.print() console.print()
# ===== Extension Commands =====
extension_app = typer.Typer(
name="extension",
help="Manage spec-kit extensions",
add_completion=False,
)
app.add_typer(extension_app, name="extension")
def get_speckit_version() -> str:
"""Get current spec-kit version."""
import importlib.metadata
try:
return importlib.metadata.version("specify-cli")
except Exception:
# Fallback: try reading from pyproject.toml
try:
import tomllib
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
if pyproject_path.exists():
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
return data.get("project", {}).get("version", "unknown")
except Exception:
# Intentionally ignore any errors while reading/parsing pyproject.toml.
# If this lookup fails for any reason, we fall back to returning "unknown" below.
pass
return "unknown"
@extension_app.command("list")
def extension_list(
available: bool = typer.Option(False, "--available", help="Show available extensions from catalog"),
all_extensions: bool = typer.Option(False, "--all", help="Show both installed and available"),
):
"""List installed extensions."""
from .extensions import ExtensionManager
project_root = Path.cwd()
# Check if we're in a spec-kit project
specify_dir = project_root / ".specify"
if not specify_dir.exists():
console.print("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)")
console.print("Run this command from a spec-kit project root")
raise typer.Exit(1)
manager = ExtensionManager(project_root)
installed = manager.list_installed()
if not installed and not (available or all_extensions):
console.print("[yellow]No extensions installed.[/yellow]")
console.print("\nInstall an extension with:")
console.print(" specify extension add <extension-name>")
return
if installed:
console.print("\n[bold cyan]Installed Extensions:[/bold cyan]\n")
for ext in installed:
status_icon = "" if ext["enabled"] else ""
status_color = "green" if ext["enabled"] else "red"
console.print(f" [{status_color}]{status_icon}[/{status_color}] [bold]{ext['name']}[/bold] (v{ext['version']})")
console.print(f" {ext['description']}")
console.print(f" Commands: {ext['command_count']} | Hooks: {ext['hook_count']} | Status: {'Enabled' if ext['enabled'] else 'Disabled'}")
console.print()
if available or all_extensions:
console.print("\nInstall an extension:")
console.print(" [cyan]specify extension add <name>[/cyan]")
@extension_app.command("add")
def extension_add(
extension: str = typer.Argument(help="Extension name or path"),
dev: bool = typer.Option(False, "--dev", help="Install from local directory"),
from_url: Optional[str] = typer.Option(None, "--from", help="Install from custom URL"),
):
"""Install an extension."""
from .extensions import ExtensionManager, ExtensionCatalog, ExtensionError, ValidationError, CompatibilityError
project_root = Path.cwd()
# Check if we're in a spec-kit project
specify_dir = project_root / ".specify"
if not specify_dir.exists():
console.print("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)")
console.print("Run this command from a spec-kit project root")
raise typer.Exit(1)
manager = ExtensionManager(project_root)
speckit_version = get_speckit_version()
try:
with console.status(f"[cyan]Installing extension: {extension}[/cyan]"):
if dev:
# Install from local directory
source_path = Path(extension).expanduser().resolve()
if not source_path.exists():
console.print(f"[red]Error:[/red] Directory not found: {source_path}")
raise typer.Exit(1)
if not (source_path / "extension.yml").exists():
console.print(f"[red]Error:[/red] No extension.yml found in {source_path}")
raise typer.Exit(1)
manifest = manager.install_from_directory(source_path, speckit_version)
elif from_url:
# Install from URL (ZIP file)
import urllib.request
import urllib.error
from urllib.parse import urlparse
# Validate URL
parsed = urlparse(from_url)
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
console.print("[red]Error:[/red] URL must use HTTPS for security.")
console.print("HTTP is only allowed for localhost URLs.")
raise typer.Exit(1)
# Warn about untrusted sources
console.print("[yellow]Warning:[/yellow] Installing from external URL.")
console.print("Only install extensions from sources you trust.\n")
console.print(f"Downloading from {from_url}...")
# Download ZIP to temp location
download_dir = project_root / ".specify" / "extensions" / ".cache" / "downloads"
download_dir.mkdir(parents=True, exist_ok=True)
zip_path = download_dir / f"{extension}-url-download.zip"
try:
with urllib.request.urlopen(from_url, timeout=60) as response:
zip_data = response.read()
zip_path.write_bytes(zip_data)
# Install from downloaded ZIP
manifest = manager.install_from_zip(zip_path, speckit_version)
except urllib.error.URLError as e:
console.print(f"[red]Error:[/red] Failed to download from {from_url}: {e}")
raise typer.Exit(1)
finally:
# Clean up downloaded ZIP
if zip_path.exists():
zip_path.unlink()
else:
# Install from catalog
catalog = ExtensionCatalog(project_root)
# Check if extension exists in catalog
ext_info = catalog.get_extension_info(extension)
if not ext_info:
console.print(f"[red]Error:[/red] Extension '{extension}' not found in catalog")
console.print("\nSearch available extensions:")
console.print(" specify extension search")
raise typer.Exit(1)
# Download extension ZIP
console.print(f"Downloading {ext_info['name']} v{ext_info.get('version', 'unknown')}...")
zip_path = catalog.download_extension(extension)
try:
# Install from downloaded ZIP
manifest = manager.install_from_zip(zip_path, speckit_version)
finally:
# Clean up downloaded ZIP
if zip_path.exists():
zip_path.unlink()
console.print(f"\n[green]✓[/green] Extension installed successfully!")
console.print(f"\n[bold]{manifest.name}[/bold] (v{manifest.version})")
console.print(f" {manifest.description}")
console.print(f"\n[bold cyan]Provided commands:[/bold cyan]")
for cmd in manifest.commands:
console.print(f"{cmd['name']} - {cmd.get('description', '')}")
console.print(f"\n[yellow]⚠[/yellow] Configuration may be required")
console.print(f" Check: .specify/extensions/{manifest.id}/")
except ValidationError as e:
console.print(f"\n[red]Validation Error:[/red] {e}")
raise typer.Exit(1)
except CompatibilityError as e:
console.print(f"\n[red]Compatibility Error:[/red] {e}")
raise typer.Exit(1)
except ExtensionError as e:
console.print(f"\n[red]Error:[/red] {e}")
raise typer.Exit(1)
@extension_app.command("remove")
def extension_remove(
extension: str = typer.Argument(help="Extension ID to remove"),
keep_config: bool = typer.Option(False, "--keep-config", help="Don't remove config files"),
force: bool = typer.Option(False, "--force", help="Skip confirmation"),
):
"""Uninstall an extension."""
from .extensions import ExtensionManager
project_root = Path.cwd()
# Check if we're in a spec-kit project
specify_dir = project_root / ".specify"
if not specify_dir.exists():
console.print("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)")
console.print("Run this command from a spec-kit project root")
raise typer.Exit(1)
manager = ExtensionManager(project_root)
# Check if extension is installed
if not manager.registry.is_installed(extension):
console.print(f"[red]Error:[/red] Extension '{extension}' is not installed")
raise typer.Exit(1)
# Get extension info
ext_manifest = manager.get_extension(extension)
if ext_manifest:
ext_name = ext_manifest.name
cmd_count = len(ext_manifest.commands)
else:
ext_name = extension
cmd_count = 0
# Confirm removal
if not force:
console.print(f"\n[yellow]⚠ This will remove:[/yellow]")
console.print(f"{cmd_count} commands from AI agent")
console.print(f" • Extension directory: .specify/extensions/{extension}/")
if not keep_config:
console.print(f" • Config files (will be backed up)")
console.print()
confirm = typer.confirm("Continue?")
if not confirm:
console.print("Cancelled")
raise typer.Exit(0)
# Remove extension
success = manager.remove(extension, keep_config=keep_config)
if success:
console.print(f"\n[green]✓[/green] Extension '{ext_name}' removed successfully")
if keep_config:
console.print(f"\nConfig files preserved in .specify/extensions/{extension}/")
else:
console.print(f"\nConfig files backed up to .specify/extensions/.backup/{extension}/")
console.print(f"\nTo reinstall: specify extension add {extension}")
else:
console.print(f"[red]Error:[/red] Failed to remove extension")
raise typer.Exit(1)
@extension_app.command("search")
def extension_search(
query: str = typer.Argument(None, help="Search query (optional)"),
tag: Optional[str] = typer.Option(None, "--tag", help="Filter by tag"),
author: Optional[str] = typer.Option(None, "--author", help="Filter by author"),
verified: bool = typer.Option(False, "--verified", help="Show only verified extensions"),
):
"""Search for available extensions in catalog."""
from .extensions import ExtensionCatalog, ExtensionError
project_root = Path.cwd()
# Check if we're in a spec-kit project
specify_dir = project_root / ".specify"
if not specify_dir.exists():
console.print("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)")
console.print("Run this command from a spec-kit project root")
raise typer.Exit(1)
catalog = ExtensionCatalog(project_root)
try:
console.print("🔍 Searching extension catalog...")
results = catalog.search(query=query, tag=tag, author=author, verified_only=verified)
if not results:
console.print("\n[yellow]No extensions found matching criteria[/yellow]")
if query or tag or author or verified:
console.print("\nTry:")
console.print(" • Broader search terms")
console.print(" • Remove filters")
console.print(" • specify extension search (show all)")
raise typer.Exit(0)
console.print(f"\n[green]Found {len(results)} extension(s):[/green]\n")
for ext in results:
# Extension header
verified_badge = " [green]✓ Verified[/green]" if ext.get("verified") else ""
console.print(f"[bold]{ext['name']}[/bold] (v{ext['version']}){verified_badge}")
console.print(f" {ext['description']}")
# Metadata
console.print(f"\n [dim]Author:[/dim] {ext.get('author', 'Unknown')}")
if ext.get('tags'):
tags_str = ", ".join(ext['tags'])
console.print(f" [dim]Tags:[/dim] {tags_str}")
# Stats
stats = []
if ext.get('downloads') is not None:
stats.append(f"Downloads: {ext['downloads']:,}")
if ext.get('stars') is not None:
stats.append(f"Stars: {ext['stars']}")
if stats:
console.print(f" [dim]{' | '.join(stats)}[/dim]")
# Links
if ext.get('repository'):
console.print(f" [dim]Repository:[/dim] {ext['repository']}")
# Install command
console.print(f"\n [cyan]Install:[/cyan] specify extension add {ext['id']}")
console.print()
except ExtensionError as e:
console.print(f"\n[red]Error:[/red] {e}")
console.print("\nTip: The catalog may be temporarily unavailable. Try again later.")
raise typer.Exit(1)
@extension_app.command("info")
def extension_info(
extension: str = typer.Argument(help="Extension ID or name"),
):
"""Show detailed information about an extension."""
from .extensions import ExtensionCatalog, ExtensionManager, ExtensionError
project_root = Path.cwd()
# Check if we're in a spec-kit project
specify_dir = project_root / ".specify"
if not specify_dir.exists():
console.print("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)")
console.print("Run this command from a spec-kit project root")
raise typer.Exit(1)
catalog = ExtensionCatalog(project_root)
manager = ExtensionManager(project_root)
try:
ext_info = catalog.get_extension_info(extension)
if not ext_info:
console.print(f"[red]Error:[/red] Extension '{extension}' not found in catalog")
console.print("\nTry: specify extension search")
raise typer.Exit(1)
# Header
verified_badge = " [green]✓ Verified[/green]" if ext_info.get("verified") else ""
console.print(f"\n[bold]{ext_info['name']}[/bold] (v{ext_info['version']}){verified_badge}")
console.print(f"ID: {ext_info['id']}")
console.print()
# Description
console.print(f"{ext_info['description']}")
console.print()
# Author and License
console.print(f"[dim]Author:[/dim] {ext_info.get('author', 'Unknown')}")
console.print(f"[dim]License:[/dim] {ext_info.get('license', 'Unknown')}")
console.print()
# Requirements
if ext_info.get('requires'):
console.print("[bold]Requirements:[/bold]")
reqs = ext_info['requires']
if reqs.get('speckit_version'):
console.print(f" • Spec Kit: {reqs['speckit_version']}")
if reqs.get('tools'):
for tool in reqs['tools']:
tool_name = tool['name']
tool_version = tool.get('version', 'any')
required = " (required)" if tool.get('required') else " (optional)"
console.print(f"{tool_name}: {tool_version}{required}")
console.print()
# Provides
if ext_info.get('provides'):
console.print("[bold]Provides:[/bold]")
provides = ext_info['provides']
if provides.get('commands'):
console.print(f" • Commands: {provides['commands']}")
if provides.get('hooks'):
console.print(f" • Hooks: {provides['hooks']}")
console.print()
# Tags
if ext_info.get('tags'):
tags_str = ", ".join(ext_info['tags'])
console.print(f"[bold]Tags:[/bold] {tags_str}")
console.print()
# Statistics
stats = []
if ext_info.get('downloads') is not None:
stats.append(f"Downloads: {ext_info['downloads']:,}")
if ext_info.get('stars') is not None:
stats.append(f"Stars: {ext_info['stars']}")
if stats:
console.print(f"[bold]Statistics:[/bold] {' | '.join(stats)}")
console.print()
# Links
console.print("[bold]Links:[/bold]")
if ext_info.get('repository'):
console.print(f" • Repository: {ext_info['repository']}")
if ext_info.get('homepage'):
console.print(f" • Homepage: {ext_info['homepage']}")
if ext_info.get('documentation'):
console.print(f" • Documentation: {ext_info['documentation']}")
if ext_info.get('changelog'):
console.print(f" • Changelog: {ext_info['changelog']}")
console.print()
# Installation status and command
is_installed = manager.registry.is_installed(ext_info['id'])
if is_installed:
console.print("[green]✓ Installed[/green]")
console.print(f"\nTo remove: specify extension remove {ext_info['id']}")
else:
console.print("[yellow]Not installed[/yellow]")
console.print(f"\n[cyan]Install:[/cyan] specify extension add {ext_info['id']}")
except ExtensionError as e:
console.print(f"\n[red]Error:[/red] {e}")
raise typer.Exit(1)
@extension_app.command("update")
def extension_update(
extension: str = typer.Argument(None, help="Extension ID to update (or all)"),
):
"""Update extension(s) to latest version."""
from .extensions import ExtensionManager, ExtensionCatalog, ExtensionError
from packaging import version as pkg_version
project_root = Path.cwd()
# Check if we're in a spec-kit project
specify_dir = project_root / ".specify"
if not specify_dir.exists():
console.print("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)")
console.print("Run this command from a spec-kit project root")
raise typer.Exit(1)
manager = ExtensionManager(project_root)
catalog = ExtensionCatalog(project_root)
try:
# Get list of extensions to update
if extension:
# Update specific extension
if not manager.registry.is_installed(extension):
console.print(f"[red]Error:[/red] Extension '{extension}' is not installed")
raise typer.Exit(1)
extensions_to_update = [extension]
else:
# Update all extensions
installed = manager.list_installed()
extensions_to_update = [ext["id"] for ext in installed]
if not extensions_to_update:
console.print("[yellow]No extensions installed[/yellow]")
raise typer.Exit(0)
console.print("🔄 Checking for updates...\n")
updates_available = []
for ext_id in extensions_to_update:
# Get installed version
metadata = manager.registry.get(ext_id)
installed_version = pkg_version.Version(metadata["version"])
# Get catalog info
ext_info = catalog.get_extension_info(ext_id)
if not ext_info:
console.print(f"{ext_id}: Not found in catalog (skipping)")
continue
catalog_version = pkg_version.Version(ext_info["version"])
if catalog_version > installed_version:
updates_available.append(
{
"id": ext_id,
"installed": str(installed_version),
"available": str(catalog_version),
"download_url": ext_info.get("download_url"),
}
)
else:
console.print(f"{ext_id}: Up to date (v{installed_version})")
if not updates_available:
console.print("\n[green]All extensions are up to date![/green]")
raise typer.Exit(0)
# Show available updates
console.print("\n[bold]Updates available:[/bold]\n")
for update in updates_available:
console.print(
f"{update['id']}: {update['installed']}{update['available']}"
)
console.print()
confirm = typer.confirm("Update these extensions?")
if not confirm:
console.print("Cancelled")
raise typer.Exit(0)
# Perform updates
console.print()
for update in updates_available:
ext_id = update["id"]
console.print(f"📦 Updating {ext_id}...")
# TODO: Implement download and reinstall from URL
# For now, just show message
console.print(
f"[yellow]Note:[/yellow] Automatic update not yet implemented. "
f"Please update manually:"
)
console.print(f" specify extension remove {ext_id} --keep-config")
console.print(f" specify extension add {ext_id}")
console.print(
"\n[cyan]Tip:[/cyan] Automatic updates will be available in a future version"
)
except ExtensionError as e:
console.print(f"\n[red]Error:[/red] {e}")
raise typer.Exit(1)
@extension_app.command("enable")
def extension_enable(
extension: str = typer.Argument(help="Extension ID to enable"),
):
"""Enable a disabled extension."""
from .extensions import ExtensionManager, HookExecutor
project_root = Path.cwd()
# Check if we're in a spec-kit project
specify_dir = project_root / ".specify"
if not specify_dir.exists():
console.print("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)")
console.print("Run this command from a spec-kit project root")
raise typer.Exit(1)
manager = ExtensionManager(project_root)
hook_executor = HookExecutor(project_root)
if not manager.registry.is_installed(extension):
console.print(f"[red]Error:[/red] Extension '{extension}' is not installed")
raise typer.Exit(1)
# Update registry
metadata = manager.registry.get(extension)
if metadata.get("enabled", True):
console.print(f"[yellow]Extension '{extension}' is already enabled[/yellow]")
raise typer.Exit(0)
metadata["enabled"] = True
manager.registry.add(extension, metadata)
# Enable hooks in extensions.yml
config = hook_executor.get_project_config()
if "hooks" in config:
for hook_name in config["hooks"]:
for hook in config["hooks"][hook_name]:
if hook.get("extension") == extension:
hook["enabled"] = True
hook_executor.save_project_config(config)
console.print(f"[green]✓[/green] Extension '{extension}' enabled")
@extension_app.command("disable")
def extension_disable(
extension: str = typer.Argument(help="Extension ID to disable"),
):
"""Disable an extension without removing it."""
from .extensions import ExtensionManager, HookExecutor
project_root = Path.cwd()
# Check if we're in a spec-kit project
specify_dir = project_root / ".specify"
if not specify_dir.exists():
console.print("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)")
console.print("Run this command from a spec-kit project root")
raise typer.Exit(1)
manager = ExtensionManager(project_root)
hook_executor = HookExecutor(project_root)
if not manager.registry.is_installed(extension):
console.print(f"[red]Error:[/red] Extension '{extension}' is not installed")
raise typer.Exit(1)
# Update registry
metadata = manager.registry.get(extension)
if not metadata.get("enabled", True):
console.print(f"[yellow]Extension '{extension}' is already disabled[/yellow]")
raise typer.Exit(0)
metadata["enabled"] = False
manager.registry.add(extension, metadata)
# Disable hooks in extensions.yml
config = hook_executor.get_project_config()
if "hooks" in config:
for hook_name in config["hooks"]:
for hook in config["hooks"][hook_name]:
if hook.get("extension") == extension:
hook["enabled"] = False
hook_executor.save_project_config(config)
console.print(f"[green]✓[/green] Extension '{extension}' disabled")
console.print(f"\nCommands will no longer be available. Hooks will not execute.")
console.print(f"To re-enable: specify extension enable {extension}")
def main(): def main():
app() app()

File diff suppressed because it is too large Load Diff

View File

@@ -16,13 +16,11 @@ You **MUST** consider the user input before proceeding (if not empty).
## Outline ## Outline
You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts. You are updating the project constitution at `/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
**Note**: If `.specify/memory/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
Follow this execution flow: Follow this execution flow:
1. Load the existing constitution at `.specify/memory/constitution.md`. 1. Load the existing constitution template at `/memory/constitution.md`.
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`. - Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly. **IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
@@ -43,10 +41,10 @@ Follow this execution flow:
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations. - Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
4. Consistency propagation checklist (convert prior checklist into active validations): 4. Consistency propagation checklist (convert prior checklist into active validations):
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles. - Read `/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints. - Read `/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline). - Read `/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required. - Read each command file in `/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed. - Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update): 5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
@@ -63,7 +61,7 @@ Follow this execution flow:
- Dates ISO format YYYY-MM-DD. - Dates ISO format YYYY-MM-DD.
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate). - Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite). 7. Write the completed constitution back to `/memory/constitution.md` (overwrite).
8. Output a final summary to the user with: 8. Output a final summary to the user with:
- New version and bump rationale. - New version and bump rationale.
@@ -81,4 +79,4 @@ If the user supplies partial updates (e.g., only one principle revision), still
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items. If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file. Do not create a new template; always operate on the existing `/memory/constitution.md` file.

View File

@@ -67,8 +67,7 @@ You **MUST** consider the user input before proceeding (if not empty).
``` ```
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore - Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore
- Check if .eslintrc* exists → create/verify .eslintignore - Check if .eslintrc*or eslint.config.* exists → create/verify .eslintignore
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns
- Check if .prettierrc* exists → create/verify .prettierignore - Check if .prettierrc* exists → create/verify .prettierignore
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing) - Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
- Check if terraform files (*.tf) exist → create/verify .terraformignore - Check if terraform files (*.tf) exist → create/verify .terraformignore

View File

@@ -39,37 +39,16 @@ Given that feature description, do this:
- "Create a dashboard for analytics" → "analytics-dashboard" - "Create a dashboard for analytics" → "analytics-dashboard"
- "Fix payment processing timeout bug" → "fix-payment-timeout" - "Fix payment processing timeout bug" → "fix-payment-timeout"
2. **Check for existing branches before creating new one**: 2. Run the script `{SCRIPT}` from repo root **with the short-name argument** and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute.
a. First, fetch all remote branches to ensure we have the latest information:
```bash
git fetch --all --prune
```
b. Find the highest feature number across all sources for the short-name:
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
c. Determine the next available number:
- Extract all numbers from all three sources
- Find the highest number N
- Use N+1 for the new branch number
d. Run the script `{SCRIPT}` with the calculated number and short-name:
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
- Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"`
- PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
**IMPORTANT**: **IMPORTANT**:
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
- Only match branches/directories with the exact short-name pattern - Append the short-name argument to the `{SCRIPT}` command with the 2-4 word short name you created in step 1. Keep the feature description as the final argument.
- If no existing branches/directories found with this short-name, start with number 1 - Bash example: `--short-name "your-generated-short-name" "Feature description here"`
- You must only ever run this script once per feature - PowerShell example: `-ShortName "your-generated-short-name" "Feature description here"`
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot") - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
- You must only ever run this script once
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
3. Load `templates/spec-template.md` to understand required sections. 3. Load `templates/spec-template.md` to understand required sections.

View File

@@ -42,7 +42,7 @@ You **MUST** consider the user input before proceeding (if not empty).
- Create parallel execution examples per user story - Create parallel execution examples per user story
- Validate task completeness (each user story has all needed tasks, independently testable) - Validate task completeness (each user story has all needed tasks, independently testable)
4. **Generate tasks.md**: Use `templates/tasks-template.md` as structure, fill with: 4. **Generate tasks.md**: Use `.specify/templates/tasks-template.md` as structure, fill with:
- Correct feature name from plan.md - Correct feature name from plan.md
- Phase 1: Setup tasks (project initialization) - Phase 1: Setup tasks (project initialization)
- Phase 2: Foundational tasks (blocking prerequisites for all user stories) - Phase 2: Foundational tasks (blocking prerequisites for all user stories)

View File

@@ -1,6 +1,6 @@
--- ---
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts. description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
tools: ['github/github-mcp-server/issue_write'] tools: ['github/github-mcp-server/create_issue']
scripts: scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
@@ -24,10 +24,8 @@ You **MUST** consider the user input before proceeding (if not empty).
git config --get remote.origin.url git config --get remote.origin.url
``` ```
> [!CAUTION] **ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL**
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote. 1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
> [!CAUTION] **UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL**
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL

View File

@@ -3,7 +3,7 @@
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] **Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` **Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow. **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
## Summary ## Summary

View File

@@ -1 +0,0 @@
"""Unit tests for Spec Kit."""

View File

@@ -1,989 +0,0 @@
"""
Unit tests for the extension system.
Tests cover:
- Extension manifest validation
- Extension registry operations
- Extension manager installation/removal
- Command registration
"""
import pytest
import json
import tempfile
import shutil
from pathlib import Path
from datetime import datetime, timezone
from specify_cli.extensions import (
ExtensionManifest,
ExtensionRegistry,
ExtensionManager,
CommandRegistrar,
ExtensionCatalog,
ExtensionError,
ValidationError,
CompatibilityError,
version_satisfies,
)
# ===== Fixtures =====
@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests."""
tmpdir = tempfile.mkdtemp()
yield Path(tmpdir)
shutil.rmtree(tmpdir)
@pytest.fixture
def valid_manifest_data():
"""Valid extension manifest data."""
return {
"schema_version": "1.0",
"extension": {
"id": "test-ext",
"name": "Test Extension",
"version": "1.0.0",
"description": "A test extension",
"author": "Test Author",
"repository": "https://github.com/test/test-ext",
"license": "MIT",
},
"requires": {
"speckit_version": ">=0.1.0",
"commands": ["speckit.tasks"],
},
"provides": {
"commands": [
{
"name": "speckit.test.hello",
"file": "commands/hello.md",
"description": "Test command",
}
]
},
"hooks": {
"after_tasks": {
"command": "speckit.test.hello",
"optional": True,
"prompt": "Run test?",
}
},
"tags": ["testing", "example"],
}
@pytest.fixture
def extension_dir(temp_dir, valid_manifest_data):
"""Create a complete extension directory structure."""
ext_dir = temp_dir / "test-ext"
ext_dir.mkdir()
# Write manifest
import yaml
manifest_path = ext_dir / "extension.yml"
with open(manifest_path, 'w') as f:
yaml.dump(valid_manifest_data, f)
# Create commands directory
commands_dir = ext_dir / "commands"
commands_dir.mkdir()
# Write command file
cmd_file = commands_dir / "hello.md"
cmd_file.write_text("""---
description: "Test hello command"
---
# Test Hello Command
$ARGUMENTS
""")
return ext_dir
@pytest.fixture
def project_dir(temp_dir):
"""Create a mock spec-kit project directory."""
proj_dir = temp_dir / "project"
proj_dir.mkdir()
# Create .specify directory
specify_dir = proj_dir / ".specify"
specify_dir.mkdir()
return proj_dir
# ===== ExtensionManifest Tests =====
class TestExtensionManifest:
"""Test ExtensionManifest validation and parsing."""
def test_valid_manifest(self, extension_dir):
"""Test loading a valid manifest."""
manifest_path = extension_dir / "extension.yml"
manifest = ExtensionManifest(manifest_path)
assert manifest.id == "test-ext"
assert manifest.name == "Test Extension"
assert manifest.version == "1.0.0"
assert manifest.description == "A test extension"
assert len(manifest.commands) == 1
assert manifest.commands[0]["name"] == "speckit.test.hello"
def test_missing_required_field(self, temp_dir):
"""Test manifest missing required field."""
import yaml
manifest_path = temp_dir / "extension.yml"
with open(manifest_path, 'w') as f:
yaml.dump({"schema_version": "1.0"}, f) # Missing 'extension'
with pytest.raises(ValidationError, match="Missing required field"):
ExtensionManifest(manifest_path)
def test_invalid_extension_id(self, temp_dir, valid_manifest_data):
"""Test manifest with invalid extension ID format."""
import yaml
valid_manifest_data["extension"]["id"] = "Invalid_ID" # Uppercase not allowed
manifest_path = temp_dir / "extension.yml"
with open(manifest_path, 'w') as f:
yaml.dump(valid_manifest_data, f)
with pytest.raises(ValidationError, match="Invalid extension ID"):
ExtensionManifest(manifest_path)
def test_invalid_version(self, temp_dir, valid_manifest_data):
"""Test manifest with invalid semantic version."""
import yaml
valid_manifest_data["extension"]["version"] = "invalid"
manifest_path = temp_dir / "extension.yml"
with open(manifest_path, 'w') as f:
yaml.dump(valid_manifest_data, f)
with pytest.raises(ValidationError, match="Invalid version"):
ExtensionManifest(manifest_path)
def test_invalid_command_name(self, temp_dir, valid_manifest_data):
"""Test manifest with invalid command name format."""
import yaml
valid_manifest_data["provides"]["commands"][0]["name"] = "invalid-name"
manifest_path = temp_dir / "extension.yml"
with open(manifest_path, 'w') as f:
yaml.dump(valid_manifest_data, f)
with pytest.raises(ValidationError, match="Invalid command name"):
ExtensionManifest(manifest_path)
def test_no_commands(self, temp_dir, valid_manifest_data):
"""Test manifest with no commands provided."""
import yaml
valid_manifest_data["provides"]["commands"] = []
manifest_path = temp_dir / "extension.yml"
with open(manifest_path, 'w') as f:
yaml.dump(valid_manifest_data, f)
with pytest.raises(ValidationError, match="must provide at least one command"):
ExtensionManifest(manifest_path)
def test_manifest_hash(self, extension_dir):
"""Test manifest hash calculation."""
manifest_path = extension_dir / "extension.yml"
manifest = ExtensionManifest(manifest_path)
hash_value = manifest.get_hash()
assert hash_value.startswith("sha256:")
assert len(hash_value) > 10
# ===== ExtensionRegistry Tests =====
class TestExtensionRegistry:
"""Test ExtensionRegistry operations."""
def test_empty_registry(self, temp_dir):
"""Test creating a new empty registry."""
extensions_dir = temp_dir / "extensions"
extensions_dir.mkdir()
registry = ExtensionRegistry(extensions_dir)
assert registry.data["schema_version"] == "1.0"
assert registry.data["extensions"] == {}
assert len(registry.list()) == 0
def test_add_extension(self, temp_dir):
"""Test adding an extension to registry."""
extensions_dir = temp_dir / "extensions"
extensions_dir.mkdir()
registry = ExtensionRegistry(extensions_dir)
metadata = {
"version": "1.0.0",
"source": "local",
"enabled": True,
}
registry.add("test-ext", metadata)
assert registry.is_installed("test-ext")
ext_data = registry.get("test-ext")
assert ext_data["version"] == "1.0.0"
assert "installed_at" in ext_data
def test_remove_extension(self, temp_dir):
"""Test removing an extension from registry."""
extensions_dir = temp_dir / "extensions"
extensions_dir.mkdir()
registry = ExtensionRegistry(extensions_dir)
registry.add("test-ext", {"version": "1.0.0"})
assert registry.is_installed("test-ext")
registry.remove("test-ext")
assert not registry.is_installed("test-ext")
assert registry.get("test-ext") is None
def test_registry_persistence(self, temp_dir):
"""Test that registry persists to disk."""
extensions_dir = temp_dir / "extensions"
extensions_dir.mkdir()
# Create registry and add extension
registry1 = ExtensionRegistry(extensions_dir)
registry1.add("test-ext", {"version": "1.0.0"})
# Load new registry instance
registry2 = ExtensionRegistry(extensions_dir)
# Should still have the extension
assert registry2.is_installed("test-ext")
assert registry2.get("test-ext")["version"] == "1.0.0"
# ===== ExtensionManager Tests =====
class TestExtensionManager:
"""Test ExtensionManager installation and removal."""
def test_check_compatibility_valid(self, extension_dir, project_dir):
"""Test compatibility check with valid version."""
manager = ExtensionManager(project_dir)
manifest = ExtensionManifest(extension_dir / "extension.yml")
# Should not raise
result = manager.check_compatibility(manifest, "0.1.0")
assert result is True
def test_check_compatibility_invalid(self, extension_dir, project_dir):
"""Test compatibility check with invalid version."""
manager = ExtensionManager(project_dir)
manifest = ExtensionManifest(extension_dir / "extension.yml")
# Requires >=0.1.0, but we have 0.0.1
with pytest.raises(CompatibilityError, match="Extension requires spec-kit"):
manager.check_compatibility(manifest, "0.0.1")
def test_install_from_directory(self, extension_dir, project_dir):
"""Test installing extension from directory."""
manager = ExtensionManager(project_dir)
manifest = manager.install_from_directory(
extension_dir,
"0.1.0",
register_commands=False # Skip command registration for now
)
assert manifest.id == "test-ext"
assert manager.registry.is_installed("test-ext")
# Check extension directory was copied
ext_dir = project_dir / ".specify" / "extensions" / "test-ext"
assert ext_dir.exists()
assert (ext_dir / "extension.yml").exists()
assert (ext_dir / "commands" / "hello.md").exists()
def test_install_duplicate(self, extension_dir, project_dir):
"""Test installing already installed extension."""
manager = ExtensionManager(project_dir)
# Install once
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
# Try to install again
with pytest.raises(ExtensionError, match="already installed"):
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
def test_remove_extension(self, extension_dir, project_dir):
"""Test removing an installed extension."""
manager = ExtensionManager(project_dir)
# Install extension
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
ext_dir = project_dir / ".specify" / "extensions" / "test-ext"
assert ext_dir.exists()
# Remove extension
result = manager.remove("test-ext", keep_config=False)
assert result is True
assert not manager.registry.is_installed("test-ext")
assert not ext_dir.exists()
def test_remove_nonexistent(self, project_dir):
"""Test removing non-existent extension."""
manager = ExtensionManager(project_dir)
result = manager.remove("nonexistent")
assert result is False
def test_list_installed(self, extension_dir, project_dir):
"""Test listing installed extensions."""
manager = ExtensionManager(project_dir)
# Initially empty
assert len(manager.list_installed()) == 0
# Install extension
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
# Should have one extension
installed = manager.list_installed()
assert len(installed) == 1
assert installed[0]["id"] == "test-ext"
assert installed[0]["name"] == "Test Extension"
assert installed[0]["version"] == "1.0.0"
assert installed[0]["command_count"] == 1
assert installed[0]["hook_count"] == 1
def test_config_backup_on_remove(self, extension_dir, project_dir):
"""Test that config files are backed up on removal."""
manager = ExtensionManager(project_dir)
# Install extension
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
# Create a config file
ext_dir = project_dir / ".specify" / "extensions" / "test-ext"
config_file = ext_dir / "test-ext-config.yml"
config_file.write_text("test: config")
# Remove extension (without keep_config)
manager.remove("test-ext", keep_config=False)
# Check backup was created (now in subdirectory per extension)
backup_dir = project_dir / ".specify" / "extensions" / ".backup" / "test-ext"
backup_file = backup_dir / "test-ext-config.yml"
assert backup_file.exists()
assert backup_file.read_text() == "test: config"
# ===== CommandRegistrar Tests =====
class TestCommandRegistrar:
"""Test CommandRegistrar command registration."""
def test_parse_frontmatter_valid(self):
"""Test parsing valid YAML frontmatter."""
content = """---
description: "Test command"
tools:
- tool1
- tool2
---
# Command body
$ARGUMENTS
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
assert frontmatter["description"] == "Test command"
assert frontmatter["tools"] == ["tool1", "tool2"]
assert "Command body" in body
assert "$ARGUMENTS" in body
def test_parse_frontmatter_no_frontmatter(self):
"""Test parsing content without frontmatter."""
content = "# Just a command\n$ARGUMENTS"
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
assert frontmatter == {}
assert body == content
def test_render_frontmatter(self):
"""Test rendering frontmatter to YAML."""
frontmatter = {
"description": "Test command",
"tools": ["tool1", "tool2"]
}
registrar = CommandRegistrar()
output = registrar.render_frontmatter(frontmatter)
assert output.startswith("---\n")
assert output.endswith("---\n")
assert "description: Test command" in output
def test_register_commands_for_claude(self, extension_dir, project_dir):
"""Test registering commands for Claude agent."""
# Create .claude directory
claude_dir = project_dir / ".claude" / "commands"
claude_dir.mkdir(parents=True)
ExtensionManager(project_dir) # Initialize manager (side effects only)
manifest = ExtensionManifest(extension_dir / "extension.yml")
registrar = CommandRegistrar()
registered = registrar.register_commands_for_claude(
manifest,
extension_dir,
project_dir
)
assert len(registered) == 1
assert "speckit.test.hello" in registered
# Check command file was created
cmd_file = claude_dir / "speckit.test.hello.md"
assert cmd_file.exists()
content = cmd_file.read_text()
assert "description: Test hello command" in content
assert "<!-- Extension: test-ext -->" in content
assert "<!-- Config: .specify/extensions/test-ext/ -->" in content
def test_command_with_aliases(self, project_dir, temp_dir):
"""Test registering a command with aliases."""
import yaml
# Create extension with command alias
ext_dir = temp_dir / "ext-alias"
ext_dir.mkdir()
manifest_data = {
"schema_version": "1.0",
"extension": {
"id": "ext-alias",
"name": "Extension with Alias",
"version": "1.0.0",
"description": "Test",
},
"requires": {
"speckit_version": ">=0.1.0",
},
"provides": {
"commands": [
{
"name": "speckit.alias.cmd",
"file": "commands/cmd.md",
"aliases": ["speckit.shortcut"],
}
]
},
}
with open(ext_dir / "extension.yml", 'w') as f:
yaml.dump(manifest_data, f)
(ext_dir / "commands").mkdir()
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nTest")
claude_dir = project_dir / ".claude" / "commands"
claude_dir.mkdir(parents=True)
manifest = ExtensionManifest(ext_dir / "extension.yml")
registrar = CommandRegistrar()
registered = registrar.register_commands_for_claude(manifest, ext_dir, project_dir)
assert len(registered) == 2
assert "speckit.alias.cmd" in registered
assert "speckit.shortcut" in registered
assert (claude_dir / "speckit.alias.cmd.md").exists()
assert (claude_dir / "speckit.shortcut.md").exists()
# ===== Utility Function Tests =====
class TestVersionSatisfies:
"""Test version_satisfies utility function."""
def test_version_satisfies_simple(self):
"""Test simple version comparison."""
assert version_satisfies("1.0.0", ">=1.0.0")
assert version_satisfies("1.0.1", ">=1.0.0")
assert not version_satisfies("0.9.9", ">=1.0.0")
def test_version_satisfies_range(self):
"""Test version range."""
assert version_satisfies("1.5.0", ">=1.0.0,<2.0.0")
assert not version_satisfies("2.0.0", ">=1.0.0,<2.0.0")
assert not version_satisfies("0.9.0", ">=1.0.0,<2.0.0")
def test_version_satisfies_complex(self):
"""Test complex version specifier."""
assert version_satisfies("1.0.5", ">=1.0.0,!=1.0.3")
assert not version_satisfies("1.0.3", ">=1.0.0,!=1.0.3")
def test_version_satisfies_invalid(self):
"""Test invalid version strings."""
assert not version_satisfies("invalid", ">=1.0.0")
assert not version_satisfies("1.0.0", "invalid specifier")
# ===== Integration Tests =====
class TestIntegration:
"""Integration tests for complete workflows."""
def test_full_install_and_remove_workflow(self, extension_dir, project_dir):
"""Test complete installation and removal workflow."""
# Create Claude directory
(project_dir / ".claude" / "commands").mkdir(parents=True)
manager = ExtensionManager(project_dir)
# Install
manager.install_from_directory(
extension_dir,
"0.1.0",
register_commands=True
)
# Verify installation
assert manager.registry.is_installed("test-ext")
installed = manager.list_installed()
assert len(installed) == 1
assert installed[0]["id"] == "test-ext"
# Verify command registered
cmd_file = project_dir / ".claude" / "commands" / "speckit.test.hello.md"
assert cmd_file.exists()
# Verify registry has registered commands (now a dict keyed by agent)
metadata = manager.registry.get("test-ext")
registered_commands = metadata["registered_commands"]
# Check that the command is registered for at least one agent
assert any(
"speckit.test.hello" in cmds
for cmds in registered_commands.values()
)
# Remove
result = manager.remove("test-ext")
assert result is True
# Verify removal
assert not manager.registry.is_installed("test-ext")
assert not cmd_file.exists()
assert len(manager.list_installed()) == 0
def test_multiple_extensions(self, temp_dir, project_dir):
"""Test installing multiple extensions."""
import yaml
# Create two extensions
for i in range(1, 3):
ext_dir = temp_dir / f"ext{i}"
ext_dir.mkdir()
manifest_data = {
"schema_version": "1.0",
"extension": {
"id": f"ext{i}",
"name": f"Extension {i}",
"version": "1.0.0",
"description": f"Extension {i}",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"commands": [
{
"name": f"speckit.ext{i}.cmd",
"file": "commands/cmd.md",
}
]
},
}
with open(ext_dir / "extension.yml", 'w') as f:
yaml.dump(manifest_data, f)
(ext_dir / "commands").mkdir()
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\nTest")
manager = ExtensionManager(project_dir)
# Install both
manager.install_from_directory(temp_dir / "ext1", "0.1.0", register_commands=False)
manager.install_from_directory(temp_dir / "ext2", "0.1.0", register_commands=False)
# Verify both installed
installed = manager.list_installed()
assert len(installed) == 2
assert {ext["id"] for ext in installed} == {"ext1", "ext2"}
# Remove first
manager.remove("ext1")
# Verify only second remains
installed = manager.list_installed()
assert len(installed) == 1
assert installed[0]["id"] == "ext2"
# ===== Extension Catalog Tests =====
class TestExtensionCatalog:
"""Test extension catalog functionality."""
def test_catalog_initialization(self, temp_dir):
"""Test catalog initialization."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
assert catalog.project_root == project_dir
assert catalog.cache_dir == project_dir / ".specify" / "extensions" / ".cache"
def test_cache_directory_creation(self, temp_dir):
"""Test catalog cache directory is created when fetching."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
# Create mock catalog data
catalog_data = {
"schema_version": "1.0",
"extensions": {
"test-ext": {
"name": "Test Extension",
"id": "test-ext",
"version": "1.0.0",
"description": "Test",
}
},
}
# Manually save to cache to test cache reading
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
catalog.cache_file.write_text(json.dumps(catalog_data))
catalog.cache_metadata_file.write_text(
json.dumps(
{
"cached_at": datetime.now(timezone.utc).isoformat(),
"catalog_url": "http://test.com/catalog.json",
}
)
)
# Should use cache
result = catalog.fetch_catalog()
assert result == catalog_data
def test_cache_expiration(self, temp_dir):
"""Test that expired cache is not used."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
# Create expired cache
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
catalog_data = {"schema_version": "1.0", "extensions": {}}
catalog.cache_file.write_text(json.dumps(catalog_data))
# Set cache time to 2 hours ago (expired)
expired_time = datetime.now(timezone.utc).timestamp() - 7200
expired_datetime = datetime.fromtimestamp(expired_time, tz=timezone.utc)
catalog.cache_metadata_file.write_text(
json.dumps(
{
"cached_at": expired_datetime.isoformat(),
"catalog_url": "http://test.com/catalog.json",
}
)
)
# Cache should be invalid
assert not catalog.is_cache_valid()
def test_search_all_extensions(self, temp_dir):
"""Test searching all extensions without filters."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
# Create mock catalog
catalog_data = {
"schema_version": "1.0",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"version": "1.0.0",
"description": "Jira integration",
"author": "Stats Perform",
"tags": ["issue-tracking", "jira"],
"verified": True,
},
"linear": {
"name": "Linear Integration",
"id": "linear",
"version": "0.9.0",
"description": "Linear integration",
"author": "Community",
"tags": ["issue-tracking"],
"verified": False,
},
},
}
# Save to cache
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
catalog.cache_file.write_text(json.dumps(catalog_data))
catalog.cache_metadata_file.write_text(
json.dumps(
{
"cached_at": datetime.now(timezone.utc).isoformat(),
"catalog_url": "http://test.com",
}
)
)
# Search without filters
results = catalog.search()
assert len(results) == 2
def test_search_by_query(self, temp_dir):
"""Test searching by query text."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
# Create mock catalog
catalog_data = {
"schema_version": "1.0",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"version": "1.0.0",
"description": "Jira issue tracking",
"tags": ["jira"],
},
"linear": {
"name": "Linear Integration",
"id": "linear",
"version": "1.0.0",
"description": "Linear project management",
"tags": ["linear"],
},
},
}
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
catalog.cache_file.write_text(json.dumps(catalog_data))
catalog.cache_metadata_file.write_text(
json.dumps(
{
"cached_at": datetime.now(timezone.utc).isoformat(),
"catalog_url": "http://test.com",
}
)
)
# Search for "jira"
results = catalog.search(query="jira")
assert len(results) == 1
assert results[0]["id"] == "jira"
def test_search_by_tag(self, temp_dir):
"""Test searching by tag."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
# Create mock catalog
catalog_data = {
"schema_version": "1.0",
"extensions": {
"jira": {
"name": "Jira",
"id": "jira",
"version": "1.0.0",
"description": "Jira",
"tags": ["issue-tracking", "jira"],
},
"linear": {
"name": "Linear",
"id": "linear",
"version": "1.0.0",
"description": "Linear",
"tags": ["issue-tracking", "linear"],
},
"github": {
"name": "GitHub",
"id": "github",
"version": "1.0.0",
"description": "GitHub",
"tags": ["vcs", "github"],
},
},
}
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
catalog.cache_file.write_text(json.dumps(catalog_data))
catalog.cache_metadata_file.write_text(
json.dumps(
{
"cached_at": datetime.now(timezone.utc).isoformat(),
"catalog_url": "http://test.com",
}
)
)
# Search by tag "issue-tracking"
results = catalog.search(tag="issue-tracking")
assert len(results) == 2
assert {r["id"] for r in results} == {"jira", "linear"}
def test_search_verified_only(self, temp_dir):
"""Test searching verified extensions only."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
# Create mock catalog
catalog_data = {
"schema_version": "1.0",
"extensions": {
"jira": {
"name": "Jira",
"id": "jira",
"version": "1.0.0",
"description": "Jira",
"verified": True,
},
"linear": {
"name": "Linear",
"id": "linear",
"version": "1.0.0",
"description": "Linear",
"verified": False,
},
},
}
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
catalog.cache_file.write_text(json.dumps(catalog_data))
catalog.cache_metadata_file.write_text(
json.dumps(
{
"cached_at": datetime.now(timezone.utc).isoformat(),
"catalog_url": "http://test.com",
}
)
)
# Search verified only
results = catalog.search(verified_only=True)
assert len(results) == 1
assert results[0]["id"] == "jira"
def test_get_extension_info(self, temp_dir):
"""Test getting specific extension info."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
# Create mock catalog
catalog_data = {
"schema_version": "1.0",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"version": "1.0.0",
"description": "Jira integration",
"author": "Stats Perform",
},
},
}
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
catalog.cache_file.write_text(json.dumps(catalog_data))
catalog.cache_metadata_file.write_text(
json.dumps(
{
"cached_at": datetime.now(timezone.utc).isoformat(),
"catalog_url": "http://test.com",
}
)
)
# Get extension info
info = catalog.get_extension_info("jira")
assert info is not None
assert info["id"] == "jira"
assert info["name"] == "Jira Integration"
# Non-existent extension
info = catalog.get_extension_info("nonexistent")
assert info is None
def test_clear_cache(self, temp_dir):
"""Test clearing catalog cache."""
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
# Create cache
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
catalog.cache_file.write_text("{}")
catalog.cache_metadata_file.write_text("{}")
assert catalog.cache_file.exists()
assert catalog.cache_metadata_file.exists()
# Clear cache
catalog.clear_cache()
assert not catalog.cache_file.exists()
assert not catalog.cache_metadata_file.exists()