mirror of
https://github.com/github/spec-kit.git
synced 2026-03-20 20:33:08 +00:00
Compare commits
20 Commits
copilot/ad
...
c883952b43
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c883952b43 | ||
|
|
b9c1a1c7bb | ||
|
|
46bc65b1ce | ||
|
|
017e1c4c2f | ||
|
|
7562664fd1 | ||
|
|
976c9981a4 | ||
|
|
d3fc056743 | ||
|
|
58ce653908 | ||
|
|
82f8a13f83 | ||
|
|
0f1cbd74fe | ||
|
|
ec60c5b2fe | ||
|
|
e56d37db8c | ||
|
|
33e853e9c9 | ||
|
|
929fab5d98 | ||
|
|
56095f06d2 | ||
|
|
2632a0f52d | ||
|
|
4ab91fbadf | ||
|
|
5c0bedb410 | ||
|
|
d92798d5b0 | ||
|
|
ee922cbde9 |
@@ -53,7 +53,7 @@ echo "✅ Done"
|
|||||||
|
|
||||||
echo -e "\n🤖 Installing Kiro CLI..."
|
echo -e "\n🤖 Installing Kiro CLI..."
|
||||||
# https://kiro.dev/docs/cli/
|
# https://kiro.dev/docs/cli/
|
||||||
KIRO_INSTALLER_URL="https://cli.kiro.dev/install"
|
KIRO_INSTALLER_URL="https://kiro.dev/install.sh"
|
||||||
KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
|
KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
|
||||||
KIRO_INSTALLER_PATH="$(mktemp)"
|
KIRO_INSTALLER_PATH="$(mktemp)"
|
||||||
|
|
||||||
@@ -80,6 +80,11 @@ fi
|
|||||||
run_command "$kiro_binary --help > /dev/null"
|
run_command "$kiro_binary --help > /dev/null"
|
||||||
echo "✅ Done"
|
echo "✅ Done"
|
||||||
|
|
||||||
|
echo -e "\n🤖 Installing Kimi CLI..."
|
||||||
|
# https://code.kimi.com
|
||||||
|
run_command "pipx install kimi-cli"
|
||||||
|
echo "✅ Done"
|
||||||
|
|
||||||
echo -e "\n🤖 Installing CodeBuddy CLI..."
|
echo -e "\n🤖 Installing CodeBuddy CLI..."
|
||||||
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
|
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
|
||||||
echo "✅ Done"
|
echo "✅ Done"
|
||||||
|
|||||||
2
.github/workflows/scripts/create-github-release.sh
vendored
Normal file → Executable file
2
.github/workflows/scripts/create-github-release.sh
vendored
Normal file → Executable file
@@ -56,6 +56,8 @@ gh release create "$VERSION" \
|
|||||||
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-vibe-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-vibe-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-vibe-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-vibe-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-kimi-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-kimi-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-generic-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-generic-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
|
||||||
--title "Spec Kit Templates - $VERSION_NO_V" \
|
--title "Spec Kit Templates - $VERSION_NO_V" \
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
.PARAMETER Agents
|
.PARAMETER Agents
|
||||||
Comma or space separated subset of agents to build (default: all)
|
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, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, generic
|
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, kimi, generic
|
||||||
|
|
||||||
.PARAMETER Scripts
|
.PARAMETER Scripts
|
||||||
Comma or space separated subset of script types to build (default: both)
|
Comma or space separated subset of script types to build (default: both)
|
||||||
@@ -201,6 +201,93 @@ agent: $basename
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Create Kimi Code skills in .kimi/skills/<name>/SKILL.md format.
|
||||||
|
# Kimi CLI discovers skills as directories containing a SKILL.md file,
|
||||||
|
# invoked with /skill:<name> (e.g. /skill:speckit.specify).
|
||||||
|
function New-KimiSkills {
|
||||||
|
param(
|
||||||
|
[string]$SkillsDir,
|
||||||
|
[string]$ScriptVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($template in $templates) {
|
||||||
|
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
|
||||||
|
$skillName = "speckit.$name"
|
||||||
|
$skillDir = Join-Path $SkillsDir $skillName
|
||||||
|
New-Item -ItemType Directory -Force -Path $skillDir | Out-Null
|
||||||
|
|
||||||
|
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
|
||||||
|
|
||||||
|
# Extract description
|
||||||
|
$description = "Spec Kit: $name workflow"
|
||||||
|
if ($fileContent -match '(?m)^description:\s*(.+)$') {
|
||||||
|
$description = $matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract script command
|
||||||
|
$scriptCommand = "(Missing script command for $ScriptVariant)"
|
||||||
|
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
|
||||||
|
$scriptCommand = $matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract agent_script command from frontmatter if present
|
||||||
|
$agentScriptCommand = ""
|
||||||
|
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
|
||||||
|
$agentScriptCommand = $matches[1].Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Replace {SCRIPT}, strip scripts sections, rewrite paths
|
||||||
|
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
|
||||||
|
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
|
||||||
|
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = $body -split "`n"
|
||||||
|
$outputLines = @()
|
||||||
|
$inFrontmatter = $false
|
||||||
|
$skipScripts = $false
|
||||||
|
$dashCount = 0
|
||||||
|
|
||||||
|
foreach ($line in $lines) {
|
||||||
|
if ($line -match '^---$') {
|
||||||
|
$outputLines += $line
|
||||||
|
$dashCount++
|
||||||
|
$inFrontmatter = ($dashCount -eq 1)
|
||||||
|
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"
|
||||||
|
$body = $body -replace '\{ARGS\}', '$ARGUMENTS'
|
||||||
|
$body = $body -replace '__AGENT__', 'kimi'
|
||||||
|
$body = Rewrite-Paths -Content $body
|
||||||
|
|
||||||
|
# Strip existing frontmatter, keep only body
|
||||||
|
$templateBody = ""
|
||||||
|
$fmCount = 0
|
||||||
|
$inBody = $false
|
||||||
|
foreach ($line in ($body -split "`n")) {
|
||||||
|
if ($line -match '^---$') {
|
||||||
|
$fmCount++
|
||||||
|
if ($fmCount -eq 2) { $inBody = $true }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ($inBody) { $templateBody += "$line`n" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$skillContent = "---`nname: `"$skillName`"`ndescription: `"$description`"`n---`n`n$templateBody"
|
||||||
|
Set-Content -Path (Join-Path $skillDir "SKILL.md") -Value $skillContent -NoNewline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Build-Variant {
|
function Build-Variant {
|
||||||
param(
|
param(
|
||||||
[string]$Agent,
|
[string]$Agent,
|
||||||
@@ -241,7 +328,6 @@ function Build-Variant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Copy any script files that aren't in variant-specific directories
|
|
||||||
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
|
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
|
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
|
||||||
}
|
}
|
||||||
@@ -281,11 +367,9 @@ function Build-Variant {
|
|||||||
$agentsDir = Join-Path $baseDir ".github/agents"
|
$agentsDir = Join-Path $baseDir ".github/agents"
|
||||||
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
|
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
|
||||||
|
|
||||||
# Generate companion prompt files
|
|
||||||
$promptsDir = Join-Path $baseDir ".github/prompts"
|
$promptsDir = Join-Path $baseDir ".github/prompts"
|
||||||
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
|
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
|
||||||
|
|
||||||
# Create VS Code workspace settings
|
|
||||||
$vscodeDir = Join-Path $baseDir ".vscode"
|
$vscodeDir = Join-Path $baseDir ".vscode"
|
||||||
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
|
||||||
if (Test-Path "templates/vscode-settings.json") {
|
if (Test-Path "templates/vscode-settings.json") {
|
||||||
@@ -298,7 +382,7 @@ function Build-Variant {
|
|||||||
}
|
}
|
||||||
'qwen' {
|
'qwen' {
|
||||||
$cmdDir = Join-Path $baseDir ".qwen/commands"
|
$cmdDir = Join-Path $baseDir ".qwen/commands"
|
||||||
Generate-Commands -Agent 'qwen' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'qwen' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
if (Test-Path "agent_templates/qwen/QWEN.md") {
|
if (Test-Path "agent_templates/qwen/QWEN.md") {
|
||||||
Copy-Item -Path "agent_templates/qwen/QWEN.md" -Destination (Join-Path $baseDir "QWEN.md")
|
Copy-Item -Path "agent_templates/qwen/QWEN.md" -Destination (Join-Path $baseDir "QWEN.md")
|
||||||
}
|
}
|
||||||
@@ -358,17 +442,22 @@ function Build-Variant {
|
|||||||
if (Test-Path $tabnineTemplate) { Copy-Item $tabnineTemplate (Join-Path $baseDir 'TABNINE.md') }
|
if (Test-Path $tabnineTemplate) { Copy-Item $tabnineTemplate (Join-Path $baseDir 'TABNINE.md') }
|
||||||
}
|
}
|
||||||
'agy' {
|
'agy' {
|
||||||
$cmdDir = Join-Path $baseDir ".agent/workflows"
|
$cmdDir = Join-Path $baseDir ".agent/commands"
|
||||||
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
}
|
}
|
||||||
'generic' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".speckit/commands"
|
|
||||||
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'vibe' {
|
'vibe' {
|
||||||
$cmdDir = Join-Path $baseDir ".vibe/prompts"
|
$cmdDir = Join-Path $baseDir ".vibe/prompts"
|
||||||
Generate-Commands -Agent 'vibe' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'vibe' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
}
|
}
|
||||||
|
'kimi' {
|
||||||
|
$skillsDir = Join-Path $baseDir ".kimi/skills"
|
||||||
|
New-Item -ItemType Directory -Force -Path $skillsDir | Out-Null
|
||||||
|
New-KimiSkills -SkillsDir $skillsDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'generic' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".speckit/commands"
|
||||||
|
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
default {
|
default {
|
||||||
throw "Unsupported agent '$Agent'."
|
throw "Unsupported agent '$Agent'."
|
||||||
}
|
}
|
||||||
@@ -381,7 +470,7 @@ function Build-Variant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Define all agents and scripts
|
# Define all agents and scripts
|
||||||
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'generic')
|
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'kimi', 'generic')
|
||||||
$AllScripts = @('sh', 'ps')
|
$AllScripts = @('sh', 'ps')
|
||||||
|
|
||||||
function Normalize-List {
|
function Normalize-List {
|
||||||
@@ -391,7 +480,6 @@ function Normalize-List {
|
|||||||
return @()
|
return @()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Split by comma or space and remove duplicates while preserving order
|
|
||||||
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
|
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
|
||||||
return $items
|
return $items
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli generic (default: all)
|
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi generic (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
|
||||||
@@ -113,7 +113,6 @@ generate_copilot_prompts() {
|
|||||||
local basename=$(basename "$agent_file" .agent.md)
|
local basename=$(basename "$agent_file" .agent.md)
|
||||||
local prompt_file="$prompts_dir/${basename}.prompt.md"
|
local prompt_file="$prompts_dir/${basename}.prompt.md"
|
||||||
|
|
||||||
# Create prompt file with agent frontmatter
|
|
||||||
cat > "$prompt_file" <<EOF
|
cat > "$prompt_file" <<EOF
|
||||||
---
|
---
|
||||||
agent: ${basename}
|
agent: ${basename}
|
||||||
@@ -122,6 +121,76 @@ EOF
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Create Kimi Code skills in .kimi/skills/<name>/SKILL.md format.
|
||||||
|
# Kimi CLI discovers skills as directories containing a SKILL.md file,
|
||||||
|
# invoked with /skill:<name> (e.g. /skill:speckit.specify).
|
||||||
|
create_kimi_skills() {
|
||||||
|
local skills_dir="$1"
|
||||||
|
local script_variant="$2"
|
||||||
|
|
||||||
|
for template in templates/commands/*.md; do
|
||||||
|
[[ -f "$template" ]] || continue
|
||||||
|
local name
|
||||||
|
name=$(basename "$template" .md)
|
||||||
|
local skill_name="speckit.${name}"
|
||||||
|
local skill_dir="${skills_dir}/${skill_name}"
|
||||||
|
mkdir -p "$skill_dir"
|
||||||
|
|
||||||
|
local file_content
|
||||||
|
file_content=$(tr -d '\r' < "$template")
|
||||||
|
|
||||||
|
# Extract description from frontmatter
|
||||||
|
local description
|
||||||
|
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
||||||
|
[[ -z "$description" ]] && description="Spec Kit: ${name} workflow"
|
||||||
|
|
||||||
|
# Extract script command
|
||||||
|
local script_command
|
||||||
|
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
|
||||||
|
[[ -z "$script_command" ]] && script_command="(Missing script command for $script_variant)"
|
||||||
|
|
||||||
|
# Extract agent_script command from frontmatter if present
|
||||||
|
local agent_script_command
|
||||||
|
agent_script_command=$(printf '%s\n' "$file_content" | awk '
|
||||||
|
/^agent_scripts:$/ { in_agent_scripts=1; next }
|
||||||
|
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
|
||||||
|
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
|
||||||
|
print
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
|
||||||
|
')
|
||||||
|
|
||||||
|
# Build body: replace placeholders, strip scripts sections, rewrite paths
|
||||||
|
local body
|
||||||
|
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
||||||
|
if [[ -n $agent_script_command ]]; then
|
||||||
|
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
|
||||||
|
fi
|
||||||
|
body=$(printf '%s\n' "$body" | awk '
|
||||||
|
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
||||||
|
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
|
||||||
|
in_frontmatter && /^agent_scripts:$/ { skip_scripts=1; next }
|
||||||
|
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
|
||||||
|
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
|
||||||
|
{ print }
|
||||||
|
')
|
||||||
|
body=$(printf '%s\n' "$body" | sed 's/{ARGS}/\$ARGUMENTS/g' | sed 's/__AGENT__/kimi/g' | rewrite_paths)
|
||||||
|
|
||||||
|
# Strip existing frontmatter and prepend Kimi frontmatter
|
||||||
|
local template_body
|
||||||
|
template_body=$(printf '%s\n' "$body" | awk '/^---/{p++; if(p==2){found=1; next}} found')
|
||||||
|
|
||||||
|
{
|
||||||
|
printf -- '---\n'
|
||||||
|
printf 'name: "%s"\n' "$skill_name"
|
||||||
|
printf 'description: "%s"\n' "$description"
|
||||||
|
printf -- '---\n\n'
|
||||||
|
printf '%s\n' "$template_body"
|
||||||
|
} > "$skill_dir/SKILL.md"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
build_variant() {
|
build_variant() {
|
||||||
local agent=$1 script=$2
|
local agent=$1 script=$2
|
||||||
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
|
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
|
||||||
@@ -140,12 +209,10 @@ build_variant() {
|
|||||||
case $script in
|
case $script in
|
||||||
sh)
|
sh)
|
||||||
[[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; }
|
[[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; }
|
||||||
# Copy any script files that aren't in variant-specific directories
|
|
||||||
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
||||||
;;
|
;;
|
||||||
ps)
|
ps)
|
||||||
[[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; }
|
[[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; }
|
||||||
# Copy any script files that aren't in variant-specific directories
|
|
||||||
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -153,11 +220,6 @@ build_variant() {
|
|||||||
|
|
||||||
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
|
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
|
||||||
|
|
||||||
# NOTE: We substitute {ARGS} internally. Outward tokens differ intentionally:
|
|
||||||
# * Markdown/prompt (claude, copilot, cursor-agent, opencode): $ARGUMENTS
|
|
||||||
# * TOML (gemini, qwen, tabnine): {{args}}
|
|
||||||
# This keeps formats readable without extra abstraction.
|
|
||||||
|
|
||||||
case $agent in
|
case $agent in
|
||||||
claude)
|
claude)
|
||||||
mkdir -p "$base_dir/.claude/commands"
|
mkdir -p "$base_dir/.claude/commands"
|
||||||
@@ -169,9 +231,7 @@ build_variant() {
|
|||||||
copilot)
|
copilot)
|
||||||
mkdir -p "$base_dir/.github/agents"
|
mkdir -p "$base_dir/.github/agents"
|
||||||
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$script"
|
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$script"
|
||||||
# Generate companion prompt files
|
|
||||||
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts"
|
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts"
|
||||||
# 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"
|
||||||
;;
|
;;
|
||||||
@@ -180,7 +240,7 @@ build_variant() {
|
|||||||
generate_commands cursor-agent md "\$ARGUMENTS" "$base_dir/.cursor/commands" "$script" ;;
|
generate_commands cursor-agent md "\$ARGUMENTS" "$base_dir/.cursor/commands" "$script" ;;
|
||||||
qwen)
|
qwen)
|
||||||
mkdir -p "$base_dir/.qwen/commands"
|
mkdir -p "$base_dir/.qwen/commands"
|
||||||
generate_commands qwen toml "{{args}}" "$base_dir/.qwen/commands" "$script"
|
generate_commands qwen md "\$ARGUMENTS" "$base_dir/.qwen/commands" "$script"
|
||||||
[[ -f agent_templates/qwen/QWEN.md ]] && cp agent_templates/qwen/QWEN.md "$base_dir/QWEN.md" ;;
|
[[ -f agent_templates/qwen/QWEN.md ]] && cp agent_templates/qwen/QWEN.md "$base_dir/QWEN.md" ;;
|
||||||
opencode)
|
opencode)
|
||||||
mkdir -p "$base_dir/.opencode/command"
|
mkdir -p "$base_dir/.opencode/command"
|
||||||
@@ -220,14 +280,17 @@ build_variant() {
|
|||||||
mkdir -p "$base_dir/.kiro/prompts"
|
mkdir -p "$base_dir/.kiro/prompts"
|
||||||
generate_commands kiro-cli md "\$ARGUMENTS" "$base_dir/.kiro/prompts" "$script" ;;
|
generate_commands kiro-cli md "\$ARGUMENTS" "$base_dir/.kiro/prompts" "$script" ;;
|
||||||
agy)
|
agy)
|
||||||
mkdir -p "$base_dir/.agent/workflows"
|
mkdir -p "$base_dir/.agent/commands"
|
||||||
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/workflows" "$script" ;;
|
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/commands" "$script" ;;
|
||||||
bob)
|
bob)
|
||||||
mkdir -p "$base_dir/.bob/commands"
|
mkdir -p "$base_dir/.bob/commands"
|
||||||
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
|
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
|
||||||
vibe)
|
vibe)
|
||||||
mkdir -p "$base_dir/.vibe/prompts"
|
mkdir -p "$base_dir/.vibe/prompts"
|
||||||
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
|
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
|
||||||
|
kimi)
|
||||||
|
mkdir -p "$base_dir/.kimi/skills"
|
||||||
|
create_kimi_skills "$base_dir/.kimi/skills" "$script" ;;
|
||||||
generic)
|
generic)
|
||||||
mkdir -p "$base_dir/.speckit/commands"
|
mkdir -p "$base_dir/.speckit/commands"
|
||||||
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
|
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
|
||||||
@@ -237,11 +300,10 @@ build_variant() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Determine agent list
|
# Determine agent list
|
||||||
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli generic)
|
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi generic)
|
||||||
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
|
|
||||||
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?"\n":"") $i);out=1}}}END{printf("\n")}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
AGENTS.md
10
AGENTS.md
@@ -35,7 +35,7 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
| **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/agents/` | 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/` | Markdown | `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 |
|
||||||
@@ -48,6 +48,7 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
|
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
|
||||||
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
|
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
|
||||||
| **Tabnine CLI** | `.tabnine/agent/commands/` | TOML | `tabnine` | Tabnine CLI |
|
| **Tabnine CLI** | `.tabnine/agent/commands/` | TOML | `tabnine` | Tabnine CLI |
|
||||||
|
| **Kimi Code** | `.kimi/skills/` | Markdown | `kimi` | Kimi Code CLI (Moonshot AI) |
|
||||||
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
||||||
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
|
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ This eliminates the need for special-case mappings throughout the codebase.
|
|||||||
- `folder`: Directory where agent-specific files are stored (relative to project root)
|
- `folder`: Directory where agent-specific files are stored (relative to project root)
|
||||||
- `commands_subdir`: Subdirectory name within the agent folder where command/prompt files are stored (default: `"commands"`)
|
- `commands_subdir`: Subdirectory name within the agent folder where command/prompt files are stored (default: `"commands"`)
|
||||||
- Most agents use `"commands"` (e.g., `.claude/commands/`)
|
- Most agents use `"commands"` (e.g., `.claude/commands/`)
|
||||||
- Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode, agy), `"prompts"` (codex, kiro-cli), `"command"` (opencode - singular)
|
- Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode), `"prompts"` (codex, kiro-cli), `"command"` (opencode - singular)
|
||||||
- This field enables `--ai-skills` to locate command templates correctly for skill generation
|
- This field enables `--ai-skills` to locate command templates correctly for skill generation
|
||||||
- `install_url`: Installation documentation URL (set to `None` for IDE-based agents)
|
- `install_url`: Installation documentation URL (set to `None` for IDE-based agents)
|
||||||
- `requires_cli`: Whether the agent requires a CLI tool check during initialization
|
- `requires_cli`: Whether the agent requires a CLI tool check during initialization
|
||||||
@@ -324,6 +325,7 @@ Require a command-line tool to be installed:
|
|||||||
- **Amp**: `amp` CLI
|
- **Amp**: `amp` CLI
|
||||||
- **SHAI**: `shai` CLI
|
- **SHAI**: `shai` CLI
|
||||||
- **Tabnine CLI**: `tabnine` CLI
|
- **Tabnine CLI**: `tabnine` CLI
|
||||||
|
- **Kimi Code**: `kimi` CLI
|
||||||
|
|
||||||
### IDE-Based Agents
|
### IDE-Based Agents
|
||||||
|
|
||||||
@@ -337,7 +339,7 @@ Work within integrated development environments:
|
|||||||
|
|
||||||
### Markdown Format
|
### Markdown Format
|
||||||
|
|
||||||
Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob
|
Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code, Qwen
|
||||||
|
|
||||||
**Standard format:**
|
**Standard format:**
|
||||||
|
|
||||||
@@ -362,7 +364,7 @@ Command content with {SCRIPT} and $ARGUMENTS placeholders.
|
|||||||
|
|
||||||
### TOML Format
|
### TOML Format
|
||||||
|
|
||||||
Used by: Gemini, Qwen, Tabnine
|
Used by: Gemini, Tabnine
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
description = "Command description"
|
description = "Command description"
|
||||||
|
|||||||
102
CHANGELOG.md
102
CHANGELOG.md
@@ -7,11 +7,113 @@ Recent 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.2.1] - 2026-03-11
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added February 2026 newsletter (#1812)
|
||||||
|
- feat: add Kimi Code CLI agent support (#1790)
|
||||||
|
- docs: fix broken links in quickstart guide (#1759) (#1797)
|
||||||
|
- docs: add catalog cli help documentation (#1793) (#1794)
|
||||||
|
- fix: use quiet checkout to avoid exception on git checkout (#1792)
|
||||||
|
- feat(extensions): support .extensionignore to exclude files during install (#1781)
|
||||||
|
- feat: add Codex support for extension command registration (#1767)
|
||||||
|
- chore: bump version to 0.2.0 (#1786)
|
||||||
|
- fix: sync agent list comments with actual supported agents (#1785)
|
||||||
|
- feat(extensions): support multiple active catalogs simultaneously (#1720)
|
||||||
|
- Pavel/add tabnine cli support (#1503)
|
||||||
|
- Add Understanding extension to community catalog (#1778)
|
||||||
|
- Add ralph extension to community catalog (#1780)
|
||||||
|
- Update README with project initialization instructions (#1772)
|
||||||
|
- feat: add review extension to community catalog (#1775)
|
||||||
|
- Add fleet extension to community catalog (#1771)
|
||||||
|
- Integration of Mistral vibe support into speckit (#1725)
|
||||||
|
- fix: Remove duplicate options in specify.md (#1765)
|
||||||
|
- fix: use global branch numbering instead of per-short-name detection (#1757)
|
||||||
|
- Add Community Walkthroughs section to README (#1766)
|
||||||
|
- feat(extensions): add Jira Integration to community catalog (#1764)
|
||||||
|
- Add Azure DevOps Integration extension to community catalog (#1734)
|
||||||
|
- Fix docs: update Antigravity link and add initialization example (#1748)
|
||||||
|
- fix: wire after_tasks and after_implement hook events into command templates (#1702)
|
||||||
|
- make c ignores consistent with c++ (#1747)
|
||||||
|
- chore: bump version to 0.1.13 (#1746)
|
||||||
|
- feat: add kiro-cli and AGENT_CONFIG consistency coverage (#1690)
|
||||||
|
- feat: add verify extension to community catalog (#1726)
|
||||||
|
- Add Retrospective Extension to community catalog README table (#1741)
|
||||||
|
- fix(scripts): add empty description validation and branch checkout error handling (#1559)
|
||||||
|
- fix: correct Copilot extension command registration (#1724)
|
||||||
|
- fix(implement): remove Makefile from C ignore patterns (#1558)
|
||||||
|
- Add sync extension to community catalog (#1728)
|
||||||
|
- fix(checklist): clarify file handling behavior for append vs create (#1556)
|
||||||
|
- fix(clarify): correct conflicting question limit from 10 to 5 (#1557)
|
||||||
|
- chore: bump version to 0.1.12 (#1737)
|
||||||
|
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
|
||||||
|
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
|
||||||
|
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
|
||||||
|
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- feat(extensions): support `.extensionignore` to exclude files/folders during `specify extension add` (#1781)
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-03-09
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- feat: add Kimi Code CLI agent support
|
||||||
|
- fix: sync agent list comments with actual supported agents (#1785)
|
||||||
|
- feat(extensions): support multiple active catalogs simultaneously (#1720)
|
||||||
|
- Pavel/add tabnine cli support (#1503)
|
||||||
|
- Add Understanding extension to community catalog (#1778)
|
||||||
|
- Add ralph extension to community catalog (#1780)
|
||||||
|
- Update README with project initialization instructions (#1772)
|
||||||
|
- feat: add review extension to community catalog (#1775)
|
||||||
|
- Add fleet extension to community catalog (#1771)
|
||||||
|
- Integration of Mistral vibe support into speckit (#1725)
|
||||||
|
- fix: Remove duplicate options in specify.md (#1765)
|
||||||
|
- fix: use global branch numbering instead of per-short-name detection (#1757)
|
||||||
|
- Add Community Walkthroughs section to README (#1766)
|
||||||
|
- feat(extensions): add Jira Integration to community catalog (#1764)
|
||||||
|
- Add Azure DevOps Integration extension to community catalog (#1734)
|
||||||
|
- Fix docs: update Antigravity link and add initialization example (#1748)
|
||||||
|
- fix: wire after_tasks and after_implement hook events into command templates (#1702)
|
||||||
|
- make c ignores consistent with c++ (#1747)
|
||||||
|
- chore: bump version to 0.1.13 (#1746)
|
||||||
|
- feat: add kiro-cli and AGENT_CONFIG consistency coverage (#1690)
|
||||||
|
- feat: add verify extension to community catalog (#1726)
|
||||||
|
- Add Retrospective Extension to community catalog README table (#1741)
|
||||||
|
- fix(scripts): add empty description validation and branch checkout error handling (#1559)
|
||||||
|
- fix: correct Copilot extension command registration (#1724)
|
||||||
|
- fix(implement): remove Makefile from C ignore patterns (#1558)
|
||||||
|
- Add sync extension to community catalog (#1728)
|
||||||
|
- fix(checklist): clarify file handling behavior for append vs create (#1556)
|
||||||
|
- fix(clarify): correct conflicting question limit from 10 to 5 (#1557)
|
||||||
|
- chore: bump version to 0.1.12 (#1737)
|
||||||
|
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
|
||||||
|
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
|
||||||
|
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
|
||||||
|
|
||||||
## [0.1.14] - 2026-03-09
|
## [0.1.14] - 2026-03-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- feat: add Tabnine CLI agent support
|
- feat: add Tabnine CLI agent support
|
||||||
|
- **Multi-Catalog Support (#1707)**: Extension catalog system now supports multiple active catalogs simultaneously via a catalog stack
|
||||||
|
- New `specify extension catalog list` command lists all active catalogs with name, URL, priority, and `install_allowed` status
|
||||||
|
- New `specify extension catalog add` and `specify extension catalog remove` commands for project-scoped catalog management
|
||||||
|
- Default built-in stack includes `catalog.json` (default, installable) and `catalog.community.json` (community, discovery only) — community extensions are now surfaced in search results out of the box
|
||||||
|
- `specify extension search` aggregates results across all active catalogs, annotating each result with source catalog
|
||||||
|
- `specify extension add` enforces `install_allowed` policy — extensions from discovery-only catalogs cannot be installed directly
|
||||||
|
- Project-level `.specify/extension-catalogs.yml` and user-level `~/.specify/extension-catalogs.yml` config files supported, with project-level taking precedence
|
||||||
|
- `SPECKIT_CATALOG_URL` environment variable still works for backward compatibility (replaces full stack with single catalog)
|
||||||
|
- All catalog URLs require HTTPS (HTTP allowed for localhost development)
|
||||||
|
- New `CatalogEntry` dataclass in `extensions.py` for catalog stack representation
|
||||||
|
- Per-URL hash-based caching for non-default catalogs; legacy cache preserved for default catalog
|
||||||
|
- Higher-priority catalogs win on merge conflicts (same extension id in multiple catalogs)
|
||||||
|
- 13 new tests covering catalog stack resolution, merge conflicts, URL validation, and `install_allowed` enforcement
|
||||||
|
- Updated RFC, Extension User Guide, and Extension API Reference documentation
|
||||||
|
|
||||||
## [0.1.13] - 2026-03-03
|
## [0.1.13] - 2026-03-03
|
||||||
|
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -154,7 +154,9 @@ See Spec-Driven Development in action across different scenarios with these comm
|
|||||||
|
|
||||||
- **[Greenfield Spring Boot + React platform](https://github.com/mnriem/spec-kit-spring-react-demo)** — Builds an LLM performance analytics platform (REST API, graphs, iteration tracking) from scratch using Spring Boot, embedded React, PostgreSQL, and Docker Compose, with a clarify step and a cross-artifact consistency analysis pass included.
|
- **[Greenfield Spring Boot + React platform](https://github.com/mnriem/spec-kit-spring-react-demo)** — Builds an LLM performance analytics platform (REST API, graphs, iteration tracking) from scratch using Spring Boot, embedded React, PostgreSQL, and Docker Compose, with a clarify step and a cross-artifact consistency analysis pass included.
|
||||||
|
|
||||||
- **[Brownfield ASP.NET CMS extension](https://github.com/mnriem/spec-kit-aspnet-brownfield-demo)** — Extends an existing open-source .NET CMS (CarrotCakeCMS-Core) with two new features — cross-platform Docker Compose infrastructure and a token-authenticated headless REST API — demonstrating how spec-kit fits into existing codebases without prior specs or a constitution.
|
- **[Brownfield ASP.NET CMS extension](https://github.com/mnriem/spec-kit-aspnet-brownfield-demo)** — Extends an existing open-source .NET CMS (CarrotCakeCMS-Core, ~307,000 lines of C#, Razor, SQL, JavaScript, and config files) with two new features — cross-platform Docker Compose infrastructure and a token-authenticated headless REST API — demonstrating how spec-kit fits into existing codebases without prior specs or a constitution.
|
||||||
|
|
||||||
|
- **[Brownfield Java runtime extension](https://github.com/mnriem/spec-kit-java-brownfield-demo)** — Extends an existing open-source Jakarta EE runtime (Piranha, ~420,000 lines of Java, XML, JSP, HTML, and config files across 180 Maven modules) with a password-protected Server Admin Console, demonstrating spec-kit on a large multi-module Java project with no prior specs or constitution.
|
||||||
|
|
||||||
## 🤖 Supported AI Agents
|
## 🤖 Supported AI Agents
|
||||||
|
|
||||||
@@ -179,8 +181,9 @@ See Spec-Driven Development in action across different scenarios with these comm
|
|||||||
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
|
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
|
||||||
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | ✅ | |
|
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | ✅ | |
|
||||||
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ | |
|
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ | |
|
||||||
|
| [Kimi Code](https://code.kimi.com/) | ✅ | |
|
||||||
| [Windsurf](https://windsurf.com/) | ✅ | |
|
| [Windsurf](https://windsurf.com/) | ✅ | |
|
||||||
| [Antigravity (agy)](https://antigravity.google/) | ✅ | |
|
| [Antigravity (agy)](https://antigravity.google/) | ✅ | Requires `--ai-skills` |
|
||||||
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
|
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
|
||||||
|
|
||||||
## 🔧 Specify CLI Reference
|
## 🔧 Specify CLI Reference
|
||||||
@@ -192,14 +195,14 @@ The `specify` command supports the following options:
|
|||||||
| 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`, `kiro-cli`, `shai`, `qodercli`, `vibe`) |
|
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`, `kimi`) |
|
||||||
|
|
||||||
### `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`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, or `generic` (requires `--ai-commands-dir`) |
|
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, `kimi`, or `generic` (requires `--ai-commands-dir`) |
|
||||||
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
|
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
|
||||||
| `--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 |
|
||||||
@@ -245,7 +248,7 @@ specify init my-project --ai vibe
|
|||||||
specify init my-project --ai bob
|
specify init my-project --ai bob
|
||||||
|
|
||||||
# Initialize with Antigravity support
|
# Initialize with Antigravity support
|
||||||
specify init my-project --ai agy
|
specify init my-project --ai agy --ai-skills
|
||||||
|
|
||||||
# Initialize with an unsupported agent (generic / bring your own agent)
|
# Initialize with an unsupported agent (generic / bring your own agent)
|
||||||
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
|
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
|
||||||
@@ -421,7 +424,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, Tabnine CLI, or Kiro 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, Qoder CLI, Tabnine CLI, Kiro CLI, or Mistral Vibe 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
|
||||||
|
|||||||
@@ -173,6 +173,6 @@ Finally, implement the solution:
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- Read the [complete methodology](../spec-driven.md) for in-depth guidance
|
- Read the [complete methodology](https://github.com/github/spec-kit/blob/main/spec-driven.md) for in-depth guidance
|
||||||
- Check out [more examples](../templates) in the repository
|
- Check out [more examples](https://github.com/github/spec-kit/tree/main/templates) in the repository
|
||||||
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
|
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
|
||||||
|
|||||||
@@ -243,6 +243,34 @@ manager.check_compatibility(
|
|||||||
) # Raises: CompatibilityError if incompatible
|
) # Raises: CompatibilityError if incompatible
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CatalogEntry
|
||||||
|
|
||||||
|
**Module**: `specify_cli.extensions`
|
||||||
|
|
||||||
|
Represents a single catalog in the active catalog stack.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import CatalogEntry
|
||||||
|
|
||||||
|
entry = CatalogEntry(
|
||||||
|
url="https://example.com/catalog.json",
|
||||||
|
name="default",
|
||||||
|
priority=1,
|
||||||
|
install_allowed=True,
|
||||||
|
description="Built-in catalog of installable extensions",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `url` | `str` | Catalog URL (must use HTTPS, or HTTP for localhost) |
|
||||||
|
| `name` | `str` | Human-readable catalog name |
|
||||||
|
| `priority` | `int` | Sort order (lower = higher priority, wins on conflicts) |
|
||||||
|
| `install_allowed` | `bool` | Whether extensions from this catalog can be installed |
|
||||||
|
| `description` | `str` | Optional human-readable description of the catalog (default: empty) |
|
||||||
|
|
||||||
### ExtensionCatalog
|
### ExtensionCatalog
|
||||||
|
|
||||||
**Module**: `specify_cli.extensions`
|
**Module**: `specify_cli.extensions`
|
||||||
@@ -253,30 +281,67 @@ from specify_cli.extensions import ExtensionCatalog
|
|||||||
catalog = ExtensionCatalog(project_root)
|
catalog = ExtensionCatalog(project_root)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Class attributes**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ExtensionCatalog.DEFAULT_CATALOG_URL # default catalog URL
|
||||||
|
ExtensionCatalog.COMMUNITY_CATALOG_URL # community catalog URL
|
||||||
|
```
|
||||||
|
|
||||||
**Methods**:
|
**Methods**:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Fetch catalog
|
# Get the ordered list of active catalogs
|
||||||
|
entries = catalog.get_active_catalogs() # List[CatalogEntry]
|
||||||
|
|
||||||
|
# Fetch catalog (primary catalog, backward compat)
|
||||||
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
|
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
|
||||||
|
|
||||||
# Search extensions
|
# Search extensions across all active catalogs
|
||||||
|
# Each result includes _catalog_name and _install_allowed
|
||||||
results = catalog.search(
|
results = catalog.search(
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
tag: Optional[str] = None,
|
tag: Optional[str] = None,
|
||||||
author: Optional[str] = None,
|
author: Optional[str] = None,
|
||||||
verified_only: bool = False
|
verified_only: bool = False
|
||||||
) # Returns: List[Dict]
|
) # Returns: List[Dict] — each dict includes _catalog_name, _install_allowed
|
||||||
|
|
||||||
# Get extension info
|
# Get extension info (searches all active catalogs)
|
||||||
|
# Returns None if not found; includes _catalog_name and _install_allowed
|
||||||
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
|
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
|
||||||
|
|
||||||
# Check cache validity
|
# Check cache validity (primary catalog)
|
||||||
is_valid = catalog.is_cache_valid() # bool
|
is_valid = catalog.is_cache_valid() # bool
|
||||||
|
|
||||||
# Clear cache
|
# Clear all catalog caches
|
||||||
catalog.clear_cache()
|
catalog.clear_cache()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Result annotation fields**:
|
||||||
|
|
||||||
|
Each extension dict returned by `search()` and `get_extension_info()` includes:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `_catalog_name` | `str` | Name of the source catalog |
|
||||||
|
| `_install_allowed` | `bool` | Whether installation is allowed from this catalog |
|
||||||
|
|
||||||
|
**Catalog config file** (`.specify/extension-catalogs.yml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
catalogs:
|
||||||
|
- name: "default"
|
||||||
|
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
|
||||||
|
priority: 1
|
||||||
|
install_allowed: true
|
||||||
|
description: "Built-in catalog of installable extensions"
|
||||||
|
- name: "community"
|
||||||
|
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
|
||||||
|
priority: 2
|
||||||
|
install_allowed: false
|
||||||
|
description: "Community-contributed extensions (discovery only)"
|
||||||
|
```
|
||||||
|
|
||||||
### HookExecutor
|
### HookExecutor
|
||||||
|
|
||||||
**Module**: `specify_cli.extensions`
|
**Module**: `specify_cli.extensions`
|
||||||
@@ -543,6 +608,39 @@ EXECUTE_COMMAND: {command}
|
|||||||
|
|
||||||
**Output**: List of installed extensions with metadata
|
**Output**: List of installed extensions with metadata
|
||||||
|
|
||||||
|
### extension catalog list
|
||||||
|
|
||||||
|
**Usage**: `specify extension catalog list`
|
||||||
|
|
||||||
|
Lists all active catalogs in the current catalog stack, showing name, description, URL, priority, and `install_allowed` status.
|
||||||
|
|
||||||
|
### extension catalog add
|
||||||
|
|
||||||
|
**Usage**: `specify extension catalog add URL [OPTIONS]`
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
|
||||||
|
- `--name NAME` - Catalog name (required)
|
||||||
|
- `--priority INT` - Priority (lower = higher priority, default: 10)
|
||||||
|
- `--install-allowed / --no-install-allowed` - Allow installs from this catalog (default: false)
|
||||||
|
- `--description TEXT` - Optional description of the catalog
|
||||||
|
|
||||||
|
**Arguments**:
|
||||||
|
|
||||||
|
- `URL` - Catalog URL (must use HTTPS)
|
||||||
|
|
||||||
|
Adds a catalog entry to `.specify/extension-catalogs.yml`.
|
||||||
|
|
||||||
|
### extension catalog remove
|
||||||
|
|
||||||
|
**Usage**: `specify extension catalog remove NAME`
|
||||||
|
|
||||||
|
**Arguments**:
|
||||||
|
|
||||||
|
- `NAME` - Catalog name to remove
|
||||||
|
|
||||||
|
Removes a catalog entry from `.specify/extension-catalogs.yml`.
|
||||||
|
|
||||||
### extension add
|
### extension add
|
||||||
|
|
||||||
**Usage**: `specify extension add EXTENSION [OPTIONS]`
|
**Usage**: `specify extension add EXTENSION [OPTIONS]`
|
||||||
@@ -551,13 +649,13 @@ EXECUTE_COMMAND: {command}
|
|||||||
|
|
||||||
- `--from URL` - Install from custom URL
|
- `--from URL` - Install from custom URL
|
||||||
- `--dev PATH` - Install from local directory
|
- `--dev PATH` - Install from local directory
|
||||||
- `--version VERSION` - Install specific version
|
|
||||||
- `--no-register` - Skip command registration
|
|
||||||
|
|
||||||
**Arguments**:
|
**Arguments**:
|
||||||
|
|
||||||
- `EXTENSION` - Extension name or URL
|
- `EXTENSION` - Extension name or URL
|
||||||
|
|
||||||
|
**Note**: Extensions from catalogs with `install_allowed: false` cannot be installed via this command.
|
||||||
|
|
||||||
### extension remove
|
### extension remove
|
||||||
|
|
||||||
**Usage**: `specify extension remove EXTENSION [OPTIONS]`
|
**Usage**: `specify extension remove EXTENSION [OPTIONS]`
|
||||||
@@ -575,6 +673,8 @@ EXECUTE_COMMAND: {command}
|
|||||||
|
|
||||||
**Usage**: `specify extension search [QUERY] [OPTIONS]`
|
**Usage**: `specify extension search [QUERY] [OPTIONS]`
|
||||||
|
|
||||||
|
Searches all active catalogs simultaneously. Results include source catalog name and install_allowed status.
|
||||||
|
|
||||||
**Options**:
|
**Options**:
|
||||||
|
|
||||||
- `--tag TAG` - Filter by tag
|
- `--tag TAG` - Filter by tag
|
||||||
@@ -589,6 +689,8 @@ EXECUTE_COMMAND: {command}
|
|||||||
|
|
||||||
**Usage**: `specify extension info EXTENSION`
|
**Usage**: `specify extension info EXTENSION`
|
||||||
|
|
||||||
|
Shows source catalog and install_allowed status.
|
||||||
|
|
||||||
**Arguments**:
|
**Arguments**:
|
||||||
|
|
||||||
- `EXTENSION` - Extension ID
|
- `EXTENSION` - Extension ID
|
||||||
|
|||||||
@@ -332,6 +332,67 @@ echo "$config"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Excluding Files with `.extensionignore`
|
||||||
|
|
||||||
|
Extension authors can create a `.extensionignore` file in the extension root to exclude files and folders from being copied when a user installs the extension with `specify extension add`. This is useful for keeping development-only files (tests, CI configs, docs source, etc.) out of the installed copy.
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
The file uses `.gitignore`-compatible patterns (one per line), powered by the [`pathspec`](https://pypi.org/project/pathspec/) library:
|
||||||
|
|
||||||
|
- Blank lines are ignored
|
||||||
|
- Lines starting with `#` are comments
|
||||||
|
- `*` matches anything **except** `/` (does not cross directory boundaries)
|
||||||
|
- `**` matches zero or more directories (e.g., `docs/**/*.draft.md`)
|
||||||
|
- `?` matches any single character except `/`
|
||||||
|
- A trailing `/` restricts a pattern to directories only
|
||||||
|
- Patterns containing `/` (other than a trailing slash) are anchored to the extension root
|
||||||
|
- Patterns without `/` match at any depth in the tree
|
||||||
|
- `!` negates a previously excluded pattern (re-includes a file)
|
||||||
|
- Backslashes in patterns are normalised to forward slashes for cross-platform compatibility
|
||||||
|
- The `.extensionignore` file itself is always excluded automatically
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
# .extensionignore
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
tests/
|
||||||
|
.github/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Documentation source (keep only the built README)
|
||||||
|
docs/
|
||||||
|
CONTRIBUTING.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern Matching
|
||||||
|
|
||||||
|
| Pattern | Matches | Does NOT match |
|
||||||
|
|---------|---------|----------------|
|
||||||
|
| `*.pyc` | Any `.pyc` file in any directory | — |
|
||||||
|
| `tests/` | The `tests` directory (and all its contents) | A file named `tests` |
|
||||||
|
| `docs/*.draft.md` | `docs/api.draft.md` (directly inside `docs/`) | `docs/sub/api.draft.md` (nested) |
|
||||||
|
| `.env` | The `.env` file at any level | — |
|
||||||
|
| `!README.md` | Re-includes `README.md` even if matched by an earlier pattern | — |
|
||||||
|
| `docs/**/*.draft.md` | `docs/api.draft.md`, `docs/sub/api.draft.md` | — |
|
||||||
|
|
||||||
|
### Unsupported Features
|
||||||
|
|
||||||
|
The following `.gitignore` features are **not applicable** in this context:
|
||||||
|
|
||||||
|
- **Multiple `.extensionignore` files**: Only a single file at the extension root is supported (`.gitignore` supports files in subdirectories)
|
||||||
|
- **`$GIT_DIR/info/exclude` and `core.excludesFile`**: These are Git-specific and have no equivalent here
|
||||||
|
- **Negation inside excluded directories**: Because file copying uses `shutil.copytree`, excluding a directory prevents recursion into it entirely. A negation pattern cannot re-include a file inside a directory that was itself excluded. For example, the combination `tests/` followed by `!tests/important.py` will **not** preserve `tests/important.py` — the `tests/` directory is skipped at the root level and its contents are never evaluated. To work around this, exclude the directory's contents individually instead of the directory itself (e.g., `tests/*.pyc` and `tests/.cache/` rather than `tests/`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Validation Rules
|
## Validation Rules
|
||||||
|
|
||||||
### Extension ID
|
### Extension ID
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ vim .specify/extensions/jira/jira-config.yml
|
|||||||
|
|
||||||
## Finding Extensions
|
## Finding Extensions
|
||||||
|
|
||||||
**Note**: By default, `specify extension search` uses your organization's catalog (`catalog.json`). If the catalog is empty, you won't see any results. See [Extension Catalogs](#extension-catalogs) to learn how to populate your catalog from the community reference catalog.
|
`specify extension search` searches **all active catalogs** simultaneously, including the community catalog by default. Results are annotated with their source catalog and install status.
|
||||||
|
|
||||||
### Browse All Extensions
|
### Browse All Extensions
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ vim .specify/extensions/jira/jira-config.yml
|
|||||||
specify extension search
|
specify extension search
|
||||||
```
|
```
|
||||||
|
|
||||||
Shows all extensions in your organization's catalog.
|
Shows all extensions across all active catalogs (default and community by default).
|
||||||
|
|
||||||
### Search by Keyword
|
### Search by Keyword
|
||||||
|
|
||||||
@@ -402,13 +402,13 @@ In addition to extension-specific environment variables (`SPECKIT_{EXT_ID}_*`),
|
|||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `SPECKIT_CATALOG_URL` | Override the extension catalog URL | GitHub-hosted catalog |
|
| `SPECKIT_CATALOG_URL` | Override the full catalog stack with a single URL (backward compat) | Built-in default stack |
|
||||||
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None |
|
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None |
|
||||||
|
|
||||||
#### Example: Using a custom catalog for testing
|
#### Example: Using a custom catalog for testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Point to a local or alternative catalog
|
# Point to a local or alternative catalog (replaces the full stack)
|
||||||
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
|
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
|
||||||
|
|
||||||
# Or use a staging catalog
|
# Or use a staging catalog
|
||||||
@@ -419,13 +419,96 @@ export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
|
|||||||
|
|
||||||
## Extension Catalogs
|
## Extension Catalogs
|
||||||
|
|
||||||
For information about how Spec Kit's dual-catalog system works (`catalog.json` vs `catalog.community.json`), see the main [Extensions README](README.md#extension-catalogs).
|
Spec Kit uses a **catalog stack** — an ordered list of catalogs searched simultaneously. By default, two catalogs are active:
|
||||||
|
|
||||||
|
| Priority | Catalog | Install Allowed | Purpose |
|
||||||
|
|----------|---------|-----------------|---------|
|
||||||
|
| 1 | `catalog.json` (default) | ✅ Yes | Curated extensions available for installation |
|
||||||
|
| 2 | `catalog.community.json` (community) | ❌ No (discovery only) | Browse community extensions |
|
||||||
|
|
||||||
|
### Listing Active Catalogs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension catalog list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Managing Catalogs via CLI
|
||||||
|
|
||||||
|
You can view the main catalog management commands using `--help`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
specify extension catalog --help
|
||||||
|
|
||||||
|
Usage: specify extension catalog [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
Manage extension catalogs
|
||||||
|
╭─ Options ────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ --help Show this message and exit. │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
╭─ Commands ───────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ list List all active extension catalogs. │
|
||||||
|
│ add Add a catalog to .specify/extension-catalogs.yml. │
|
||||||
|
│ remove Remove a catalog from .specify/extension-catalogs.yml. │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a Catalog (Project-scoped)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add an internal catalog that allows installs
|
||||||
|
specify extension catalog add \
|
||||||
|
--name "internal" \
|
||||||
|
--priority 2 \
|
||||||
|
--install-allowed \
|
||||||
|
https://internal.company.com/spec-kit/catalog.json
|
||||||
|
|
||||||
|
# Add a discovery-only catalog
|
||||||
|
specify extension catalog add \
|
||||||
|
--name "partner" \
|
||||||
|
--priority 5 \
|
||||||
|
https://partner.example.com/spec-kit/catalog.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates or updates `.specify/extension-catalogs.yml`.
|
||||||
|
|
||||||
|
### Removing a Catalog
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension catalog remove internal
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Config File
|
||||||
|
|
||||||
|
You can also edit `.specify/extension-catalogs.yml` directly:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
catalogs:
|
||||||
|
- name: "default"
|
||||||
|
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
|
||||||
|
priority: 1
|
||||||
|
install_allowed: true
|
||||||
|
description: "Built-in catalog of installable extensions"
|
||||||
|
|
||||||
|
- name: "internal"
|
||||||
|
url: "https://internal.company.com/spec-kit/catalog.json"
|
||||||
|
priority: 2
|
||||||
|
install_allowed: true
|
||||||
|
description: "Internal company extensions"
|
||||||
|
|
||||||
|
- name: "community"
|
||||||
|
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
|
||||||
|
priority: 3
|
||||||
|
install_allowed: false
|
||||||
|
description: "Community-contributed extensions (discovery only)"
|
||||||
|
```
|
||||||
|
|
||||||
|
A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. Project-level config takes full precedence when it contains one or more catalog entries. An empty `catalogs: []` list falls back to built-in defaults.
|
||||||
|
|
||||||
## Organization Catalog Customization
|
## Organization Catalog Customization
|
||||||
|
|
||||||
### Why Customize Your Catalog
|
### Why Customize Your Catalog
|
||||||
|
|
||||||
Organizations customize their `catalog.json` to:
|
Organizations customize their catalogs to:
|
||||||
|
|
||||||
- **Control available extensions** - Curate which extensions your team can install
|
- **Control available extensions** - Curate which extensions your team can install
|
||||||
- **Host private extensions** - Internal tools that shouldn't be public
|
- **Host private extensions** - Internal tools that shouldn't be public
|
||||||
@@ -503,24 +586,40 @@ Options for hosting your catalog:
|
|||||||
|
|
||||||
#### 3. Configure Your Environment
|
#### 3. Configure Your Environment
|
||||||
|
|
||||||
##### Option A: Environment variable (recommended for CI/CD)
|
##### Option A: Catalog stack config file (recommended)
|
||||||
|
|
||||||
|
Add to `.specify/extension-catalogs.yml` in your project:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
catalogs:
|
||||||
|
- name: "my-org"
|
||||||
|
url: "https://your-org.com/spec-kit/catalog.json"
|
||||||
|
priority: 1
|
||||||
|
install_allowed: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension catalog add \
|
||||||
|
--name "my-org" \
|
||||||
|
--install-allowed \
|
||||||
|
https://your-org.com/spec-kit/catalog.json
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Option B: Environment variable (recommended for CI/CD, single-catalog)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# In ~/.bashrc, ~/.zshrc, or CI pipeline
|
# In ~/.bashrc, ~/.zshrc, or CI pipeline
|
||||||
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
|
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
|
#### 4. Verify Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# List active catalogs
|
||||||
|
specify extension catalog list
|
||||||
|
|
||||||
# Search should now show your catalog's extensions
|
# Search should now show your catalog's extensions
|
||||||
specify extension search
|
specify extension search
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
||||||
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
|
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
|
||||||
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
|
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
|
||||||
|
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
|
||||||
| Ralph Loop | Autonomous implementation loop using AI agent CLI | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
|
| Ralph Loop | Autonomous implementation loop using AI agent CLI | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
|
||||||
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
|
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
|
||||||
| Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |
|
| Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# RFC: Spec Kit Extension System
|
# RFC: Spec Kit Extension System
|
||||||
|
|
||||||
**Status**: Draft
|
**Status**: Implemented
|
||||||
**Author**: Stats Perform Engineering
|
**Author**: Stats Perform Engineering
|
||||||
**Created**: 2026-01-28
|
**Created**: 2026-01-28
|
||||||
**Updated**: 2026-01-28
|
**Updated**: 2026-03-11
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,8 +24,9 @@
|
|||||||
13. [Security Considerations](#security-considerations)
|
13. [Security Considerations](#security-considerations)
|
||||||
14. [Migration Strategy](#migration-strategy)
|
14. [Migration Strategy](#migration-strategy)
|
||||||
15. [Implementation Phases](#implementation-phases)
|
15. [Implementation Phases](#implementation-phases)
|
||||||
16. [Open Questions](#open-questions)
|
16. [Resolved Questions](#resolved-questions)
|
||||||
17. [Appendices](#appendices)
|
17. [Open Questions (Remaining)](#open-questions-remaining)
|
||||||
|
18. [Appendices](#appendices)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -868,7 +869,7 @@ Spec Kit uses two catalog files with different purposes:
|
|||||||
|
|
||||||
- **Purpose**: Organization's curated catalog of approved extensions
|
- **Purpose**: Organization's curated catalog of approved extensions
|
||||||
- **Default State**: Empty by design - users populate with extensions they trust
|
- **Default State**: Empty by design - users populate with extensions they trust
|
||||||
- **Usage**: Default catalog used by `specify extension` CLI commands
|
- **Usage**: Primary catalog (priority 1, `install_allowed: true`) in the default stack
|
||||||
- **Control**: Organizations maintain their own fork/version for their teams
|
- **Control**: Organizations maintain their own fork/version for their teams
|
||||||
|
|
||||||
#### Community Reference Catalog (`catalog.community.json`)
|
#### Community Reference Catalog (`catalog.community.json`)
|
||||||
@@ -879,16 +880,16 @@ Spec Kit uses two catalog files with different purposes:
|
|||||||
- **Verification**: Community extensions may have `verified: false` initially
|
- **Verification**: Community extensions may have `verified: false` initially
|
||||||
- **Status**: Active - open for community contributions
|
- **Status**: Active - open for community contributions
|
||||||
- **Submission**: Via Pull Request following the Extension Publishing Guide
|
- **Submission**: Via Pull Request following the Extension Publishing Guide
|
||||||
- **Usage**: Browse to discover extensions, then copy to your `catalog.json`
|
- **Usage**: Secondary catalog (priority 2, `install_allowed: false`) in the default stack — discovery only
|
||||||
|
|
||||||
**How It Works:**
|
**How It Works (default stack):**
|
||||||
|
|
||||||
1. **Discover**: Browse `catalog.community.json` to find available extensions
|
1. **Discover**: `specify extension search` searches both catalogs — community extensions appear automatically
|
||||||
2. **Review**: Evaluate extensions for security, quality, and organizational fit
|
2. **Review**: Evaluate community extensions for security, quality, and organizational fit
|
||||||
3. **Curate**: Copy approved extension entries from community catalog to your `catalog.json`
|
3. **Curate**: Copy approved entries from community catalog to your `catalog.json`, or add to `.specify/extension-catalogs.yml` with `install_allowed: true`
|
||||||
4. **Install**: Use `specify extension add <name>` (pulls from your curated catalog)
|
4. **Install**: Use `specify extension add <name>` — only allowed from `install_allowed: true` catalogs
|
||||||
|
|
||||||
This approach gives organizations full control over which extensions are available to their teams while maintaining a shared community resource for discovery.
|
This approach gives organizations full control over which extensions can be installed while still providing community discoverability out of the box.
|
||||||
|
|
||||||
### Catalog Format
|
### Catalog Format
|
||||||
|
|
||||||
@@ -961,30 +962,92 @@ specify extension info jira
|
|||||||
|
|
||||||
### Custom Catalogs
|
### Custom Catalogs
|
||||||
|
|
||||||
**⚠️ FUTURE FEATURE - NOT YET IMPLEMENTED**
|
Spec Kit supports a **catalog stack** — an ordered list of catalogs that the CLI merges and searches across. This allows organizations to maintain their own org-approved extensions alongside an internal catalog and community discovery, all at once.
|
||||||
|
|
||||||
The following catalog management commands are proposed design concepts but are not yet available in the current implementation:
|
#### Catalog Stack Resolution
|
||||||
|
|
||||||
```bash
|
The active catalog stack is resolved in this order (first match wins):
|
||||||
# Add custom catalog (FUTURE - NOT AVAILABLE)
|
|
||||||
specify extension add-catalog https://internal.company.com/spec-kit/catalog.json
|
|
||||||
|
|
||||||
# Set as default (FUTURE - NOT AVAILABLE)
|
1. **`SPECKIT_CATALOG_URL` environment variable** — single catalog replacing all defaults (backward compat)
|
||||||
specify extension set-catalog --default https://internal.company.com/spec-kit/catalog.json
|
2. **Project-level `.specify/extension-catalogs.yml`** — full control for the project
|
||||||
|
3. **User-level `~/.specify/extension-catalogs.yml`** — personal defaults
|
||||||
|
4. **Built-in default stack** — `catalog.json` (install_allowed: true) + `catalog.community.json` (install_allowed: false)
|
||||||
|
|
||||||
# List catalogs (FUTURE - NOT AVAILABLE)
|
#### Default Built-in Stack
|
||||||
specify extension catalogs
|
|
||||||
|
When no config file exists, the CLI uses:
|
||||||
|
|
||||||
|
| Priority | Catalog | install_allowed | Purpose |
|
||||||
|
|----------|---------|-----------------|---------|
|
||||||
|
| 1 | `catalog.json` (default) | `true` | Curated extensions available for installation |
|
||||||
|
| 2 | `catalog.community.json` (community) | `false` | Discovery only — browse but not install |
|
||||||
|
|
||||||
|
This means `specify extension search` surfaces community extensions out of the box, while `specify extension add` is still restricted to entries from catalogs with `install_allowed: true`.
|
||||||
|
|
||||||
|
#### `.specify/extension-catalogs.yml` Config File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
catalogs:
|
||||||
|
- name: "default"
|
||||||
|
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
|
||||||
|
priority: 1 # Highest — only approved entries can be installed
|
||||||
|
install_allowed: true
|
||||||
|
description: "Built-in catalog of installable extensions"
|
||||||
|
|
||||||
|
- name: "internal"
|
||||||
|
url: "https://internal.company.com/spec-kit/catalog.json"
|
||||||
|
priority: 2
|
||||||
|
install_allowed: true
|
||||||
|
description: "Internal company extensions"
|
||||||
|
|
||||||
|
- name: "community"
|
||||||
|
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
|
||||||
|
priority: 3 # Lowest — discovery only, not installable
|
||||||
|
install_allowed: false
|
||||||
|
description: "Community-contributed extensions (discovery only)"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Proposed catalog priority** (future design):
|
A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. When a project-level config is present with one or more catalog entries, it takes full control and the built-in defaults are not applied. An empty `catalogs: []` list is treated the same as no config file, falling back to defaults.
|
||||||
|
|
||||||
1. Project-specific catalog (`.specify/extension-catalogs.yml`) - *not implemented*
|
#### Catalog CLI Commands
|
||||||
2. User-level catalog (`~/.specify/extension-catalogs.yml`) - *not implemented*
|
|
||||||
3. Default GitHub catalog
|
|
||||||
|
|
||||||
#### Current Implementation: SPECKIT_CATALOG_URL
|
```bash
|
||||||
|
# List active catalogs with name, URL, priority, and install_allowed
|
||||||
|
specify extension catalog list
|
||||||
|
|
||||||
**The currently available method** for using custom catalogs is the `SPECKIT_CATALOG_URL` environment variable:
|
# Add a catalog (project-scoped)
|
||||||
|
specify extension catalog add --name "internal" --install-allowed \
|
||||||
|
https://internal.company.com/spec-kit/catalog.json
|
||||||
|
|
||||||
|
# Add a discovery-only catalog
|
||||||
|
specify extension catalog add --name "community" \
|
||||||
|
https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json
|
||||||
|
|
||||||
|
# Remove a catalog
|
||||||
|
specify extension catalog remove internal
|
||||||
|
|
||||||
|
# Show which catalog an extension came from
|
||||||
|
specify extension info jira
|
||||||
|
# → Source catalog: default
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Merge Conflict Resolution
|
||||||
|
|
||||||
|
When the same extension `id` appears in multiple catalogs, the higher-priority (lower priority number) catalog wins. Extensions from lower-priority catalogs with the same `id` are ignored.
|
||||||
|
|
||||||
|
#### `install_allowed: false` Behavior
|
||||||
|
|
||||||
|
Extensions from discovery-only catalogs are shown in `specify extension search` results but cannot be installed directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠ 'linear' is available in the 'community' catalog but installation is not allowed from that catalog.
|
||||||
|
|
||||||
|
To enable installation, add 'linear' to an approved catalog (install_allowed: true) in .specify/extension-catalogs.yml.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `SPECKIT_CATALOG_URL` (Backward Compatibility)
|
||||||
|
|
||||||
|
The `SPECKIT_CATALOG_URL` environment variable still works — it is treated as a single `install_allowed: true` catalog, **replacing both defaults** for full backward compatibility:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Point to your organization's catalog
|
# Point to your organization's catalog
|
||||||
@@ -1442,203 +1505,225 @@ AI agent registers both names, so old scripts work.
|
|||||||
|
|
||||||
## Implementation Phases
|
## Implementation Phases
|
||||||
|
|
||||||
### Phase 1: Core Extension System (Week 1-2)
|
### Phase 1: Core Extension System ✅ COMPLETED
|
||||||
|
|
||||||
**Goal**: Basic extension infrastructure
|
**Goal**: Basic extension infrastructure
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [ ] Extension manifest schema (`extension.yml`)
|
- [x] Extension manifest schema (`extension.yml`)
|
||||||
- [ ] Extension directory structure
|
- [x] Extension directory structure
|
||||||
- [ ] CLI commands:
|
- [x] CLI commands:
|
||||||
- [ ] `specify extension list`
|
- [x] `specify extension list`
|
||||||
- [ ] `specify extension add` (from URL)
|
- [x] `specify extension add` (from URL and local `--dev`)
|
||||||
- [ ] `specify extension remove`
|
- [x] `specify extension remove`
|
||||||
- [ ] Extension registry (`.specify/extensions/.registry`)
|
- [x] Extension registry (`.specify/extensions/.registry`)
|
||||||
- [ ] Command registration (Claude only initially)
|
- [x] Command registration (Claude and 15+ other agents)
|
||||||
- [ ] Basic validation (manifest schema, compatibility)
|
- [x] Basic validation (manifest schema, compatibility)
|
||||||
- [ ] Documentation (extension development guide)
|
- [x] Documentation (extension development guide)
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [ ] Unit tests for manifest parsing
|
- [x] Unit tests for manifest parsing
|
||||||
- [ ] Integration test: Install dummy extension
|
- [x] Integration test: Install dummy extension
|
||||||
- [ ] Integration test: Register commands with Claude
|
- [x] Integration test: Register commands with Claude
|
||||||
|
|
||||||
### Phase 2: Jira Extension (Week 3)
|
### Phase 2: Jira Extension ✅ COMPLETED
|
||||||
|
|
||||||
**Goal**: First production extension
|
**Goal**: First production extension
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [ ] Create `spec-kit-jira` repository
|
- [x] Create `spec-kit-jira` repository
|
||||||
- [ ] Port Jira functionality to extension
|
- [x] Port Jira functionality to extension
|
||||||
- [ ] Create `jira-config.yml` template
|
- [x] Create `jira-config.yml` template
|
||||||
- [ ] Commands:
|
- [x] Commands:
|
||||||
- [ ] `specstoissues.md`
|
- [x] `specstoissues.md`
|
||||||
- [ ] `discover-fields.md`
|
- [x] `discover-fields.md`
|
||||||
- [ ] `sync-status.md`
|
- [x] `sync-status.md`
|
||||||
- [ ] Helper scripts
|
- [x] Helper scripts
|
||||||
- [ ] Documentation (README, configuration guide, examples)
|
- [x] Documentation (README, configuration guide, examples)
|
||||||
- [ ] Release v1.0.0
|
- [x] Release v3.0.0
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [ ] Test on `eng-msa-ts` project
|
- [x] Test on `eng-msa-ts` project
|
||||||
- [ ] Verify spec→Epic, phase→Story, task→Issue mapping
|
- [x] Verify spec→Epic, phase→Story, task→Issue mapping
|
||||||
- [ ] Test configuration loading and validation
|
- [x] Test configuration loading and validation
|
||||||
- [ ] Test custom field application
|
- [x] Test custom field application
|
||||||
|
|
||||||
### Phase 3: Extension Catalog (Week 4)
|
### Phase 3: Extension Catalog ✅ COMPLETED
|
||||||
|
|
||||||
**Goal**: Discovery and distribution
|
**Goal**: Discovery and distribution
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [ ] Central catalog (`extensions/catalog.json` in spec-kit repo)
|
- [x] Central catalog (`extensions/catalog.json` in spec-kit repo)
|
||||||
- [ ] Catalog fetch and parsing
|
- [x] Community catalog (`extensions/catalog.community.json`)
|
||||||
- [ ] CLI commands:
|
- [x] Catalog fetch and parsing with multi-catalog support
|
||||||
- [ ] `specify extension search`
|
- [x] CLI commands:
|
||||||
- [ ] `specify extension info`
|
- [x] `specify extension search`
|
||||||
- [ ] Catalog publishing process (GitHub Action)
|
- [x] `specify extension info`
|
||||||
- [ ] Documentation (how to publish extensions)
|
- [x] `specify extension catalog list`
|
||||||
|
- [x] `specify extension catalog add`
|
||||||
|
- [x] `specify extension catalog remove`
|
||||||
|
- [x] Documentation (how to publish extensions)
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [ ] Test catalog fetch
|
- [x] Test catalog fetch
|
||||||
- [ ] Test extension search/filtering
|
- [x] Test extension search/filtering
|
||||||
- [ ] Test catalog caching
|
- [x] Test catalog caching
|
||||||
|
- [x] Test multi-catalog merge with priority
|
||||||
|
|
||||||
### Phase 4: Advanced Features (Week 5-6)
|
### Phase 4: Advanced Features ✅ COMPLETED
|
||||||
|
|
||||||
**Goal**: Hooks, updates, multi-agent support
|
**Goal**: Hooks, updates, multi-agent support
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [ ] Hook system (`hooks` in extension.yml)
|
- [x] Hook system (`hooks` in extension.yml)
|
||||||
- [ ] Hook registration and execution
|
- [x] Hook registration and execution
|
||||||
- [ ] Project extensions config (`.specify/extensions.yml`)
|
- [x] Project extensions config (`.specify/extensions.yml`)
|
||||||
- [ ] CLI commands:
|
- [x] CLI commands:
|
||||||
- [ ] `specify extension update`
|
- [x] `specify extension update` (with atomic backup/restore)
|
||||||
- [ ] `specify extension enable/disable`
|
- [x] `specify extension enable/disable`
|
||||||
- [ ] Command registration for multiple agents (Gemini, Copilot)
|
- [x] Command registration for multiple agents (15+ agents including Claude, Copilot, Gemini, Cursor, etc.)
|
||||||
- [ ] Extension update notifications
|
- [x] Extension update notifications (version comparison)
|
||||||
- [ ] Configuration layer resolution (project, local, env)
|
- [x] Configuration layer resolution (project, local, env)
|
||||||
|
|
||||||
|
**Additional features implemented beyond original RFC**:
|
||||||
|
|
||||||
|
- [x] **Display name resolution**: All commands accept extension display names in addition to IDs
|
||||||
|
- [x] **Ambiguous name handling**: User-friendly tables when multiple extensions match a name
|
||||||
|
- [x] **Atomic update with rollback**: Full backup of extension dir, commands, hooks, and registry with automatic rollback on failure
|
||||||
|
- [x] **Pre-install ID validation**: Validates extension ID from ZIP before installing (security)
|
||||||
|
- [x] **Enabled state preservation**: Disabled extensions stay disabled after update
|
||||||
|
- [x] **Registry update/restore methods**: Clean API for enable/disable and rollback operations
|
||||||
|
- [x] **Catalog error fallback**: `extension info` falls back to local info when catalog unavailable
|
||||||
|
- [x] **`_install_allowed` flag**: Discovery-only catalogs can't be used for installation
|
||||||
|
- [x] **Cache invalidation**: Cache invalidated when `SPECKIT_CATALOG_URL` changes
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [ ] Test hooks in core commands
|
- [x] Test hooks in core commands
|
||||||
- [ ] Test extension updates (preserve config)
|
- [x] Test extension updates (preserve config)
|
||||||
- [ ] Test multi-agent registration
|
- [x] Test multi-agent registration
|
||||||
|
- [x] Test atomic rollback on update failure
|
||||||
|
- [x] Test enabled state preservation
|
||||||
|
- [x] Test display name resolution
|
||||||
|
|
||||||
### Phase 5: Polish & Documentation (Week 7)
|
### Phase 5: Polish & Documentation ✅ COMPLETED
|
||||||
|
|
||||||
**Goal**: Production ready
|
**Goal**: Production ready
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [ ] Comprehensive documentation:
|
- [x] Comprehensive documentation:
|
||||||
- [ ] User guide (installing/using extensions)
|
- [x] User guide (EXTENSION-USER-GUIDE.md)
|
||||||
- [ ] Extension development guide
|
- [x] Extension development guide (EXTENSION-DEV-GUIDE.md)
|
||||||
- [ ] Extension API reference
|
- [x] Extension API reference (EXTENSION-API-REFERENCE.md)
|
||||||
- [ ] Migration guide (core → extension)
|
- [x] Error messages and validation improvements
|
||||||
- [ ] Error messages and validation improvements
|
- [x] CLI help text updates
|
||||||
- [ ] CLI help text updates
|
|
||||||
- [ ] Example extension template (cookiecutter)
|
|
||||||
- [ ] Blog post / announcement
|
|
||||||
- [ ] Video tutorial
|
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [ ] End-to-end testing on multiple projects
|
- [x] End-to-end testing on multiple projects
|
||||||
- [ ] Community beta testing
|
- [x] 163 unit tests passing
|
||||||
- [ ] Performance testing (large projects)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Open Questions
|
## Resolved Questions
|
||||||
|
|
||||||
### 1. Extension Namespace
|
The following questions from the original RFC have been resolved during implementation:
|
||||||
|
|
||||||
|
### 1. Extension Namespace ✅ RESOLVED
|
||||||
|
|
||||||
**Question**: Should extension commands use namespace prefix?
|
**Question**: Should extension commands use namespace prefix?
|
||||||
|
|
||||||
**Options**:
|
**Decision**: **Option C** - Both prefixed and aliases are supported. Commands use `speckit.{extension}.{command}` as canonical name, with optional aliases defined in manifest.
|
||||||
|
|
||||||
- A) Prefixed: `/speckit.jira.specstoissues` (explicit, avoids conflicts)
|
**Implementation**: The `aliases` field in `extension.yml` allows extensions to register additional command names.
|
||||||
- B) Short alias: `/jira.specstoissues` (shorter, less verbose)
|
|
||||||
- C) Both: Register both names, prefer prefixed in docs
|
|
||||||
|
|
||||||
**Recommendation**: C (both), prefixed is canonical
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. Config File Location
|
### 2. Config File Location ✅ RESOLVED
|
||||||
|
|
||||||
**Question**: Where should extension configs live?
|
**Question**: Where should extension configs live?
|
||||||
|
|
||||||
**Options**:
|
**Decision**: **Option A** - Extension directory (`.specify/extensions/{ext-id}/{ext-id}-config.yml`). This keeps extensions self-contained and easier to manage.
|
||||||
|
|
||||||
- A) Extension directory: `.specify/extensions/jira/jira-config.yml` (encapsulated)
|
**Implementation**: Each extension has its own config file within its directory, with layered resolution (defaults → project → local → env vars).
|
||||||
- B) Root level: `.specify/jira-config.yml` (more visible)
|
|
||||||
- C) Unified: `.specify/extensions.yml` (all extension configs in one file)
|
|
||||||
|
|
||||||
**Recommendation**: A (extension directory), cleaner separation
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. Command File Format
|
### 3. Command File Format ✅ RESOLVED
|
||||||
|
|
||||||
**Question**: Should extensions use universal format or agent-specific?
|
**Question**: Should extensions use universal format or agent-specific?
|
||||||
|
|
||||||
**Options**:
|
**Decision**: **Option A** - Universal Markdown format. Extensions write commands once, CLI converts to agent-specific format during registration.
|
||||||
|
|
||||||
- A) Universal Markdown: Extensions write once, CLI converts per-agent
|
**Implementation**: `CommandRegistrar` class handles conversion to 15+ agent formats (Claude, Copilot, Gemini, Cursor, etc.).
|
||||||
- B) Agent-specific: Extensions provide separate files for each agent
|
|
||||||
- C) Hybrid: Universal default, agent-specific overrides
|
|
||||||
|
|
||||||
**Recommendation**: A (universal), reduces duplication
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. Hook Execution Model
|
### 4. Hook Execution Model ✅ RESOLVED
|
||||||
|
|
||||||
**Question**: How should hooks execute?
|
**Question**: How should hooks execute?
|
||||||
|
|
||||||
**Options**:
|
**Decision**: **Option A** - Hooks are registered in `.specify/extensions.yml` and executed by the AI agent when it sees the hook trigger. Hook state (enabled/disabled) is managed per-extension.
|
||||||
|
|
||||||
- A) AI agent interprets: Core commands output `EXECUTE_COMMAND: name`
|
**Implementation**: `HookExecutor` class manages hook registration and state in `extensions.yml`.
|
||||||
- B) CLI executes: Core commands call `specify extension hook after_tasks`
|
|
||||||
- C) Agent built-in: Extension system built into AI agent (Claude SDK)
|
|
||||||
|
|
||||||
**Recommendation**: A initially (simpler), move to C long-term
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Extension Distribution
|
### 5. Extension Distribution ✅ RESOLVED
|
||||||
|
|
||||||
**Question**: How should extensions be packaged?
|
**Question**: How should extensions be packaged?
|
||||||
|
|
||||||
**Options**:
|
**Decision**: **Option A** - ZIP archives downloaded from GitHub releases (via catalog `download_url`). Local development uses `--dev` flag with directory path.
|
||||||
|
|
||||||
- A) ZIP archives: Downloaded from GitHub releases
|
**Implementation**: `ExtensionManager.install_from_zip()` handles ZIP extraction and validation.
|
||||||
- B) Git repos: Cloned directly (`git clone`)
|
|
||||||
- C) Python packages: Installable via `uv tool install`
|
|
||||||
|
|
||||||
**Recommendation**: A (ZIP), simpler for non-Python extensions in future
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. Multi-Version Support
|
### 6. Multi-Version Support ✅ RESOLVED
|
||||||
|
|
||||||
**Question**: Can multiple versions of same extension coexist?
|
**Question**: Can multiple versions of same extension coexist?
|
||||||
|
|
||||||
|
**Decision**: **Option A** - Single version only. Updates replace the existing version with atomic rollback on failure.
|
||||||
|
|
||||||
|
**Implementation**: `extension update` performs atomic backup/restore to ensure safe updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions (Remaining)
|
||||||
|
|
||||||
|
### 1. Sandboxing / Permissions (Future)
|
||||||
|
|
||||||
|
**Question**: Should extensions declare required permissions?
|
||||||
|
|
||||||
**Options**:
|
**Options**:
|
||||||
|
|
||||||
- A) Single version: Only one version installed at a time
|
- A) No sandboxing (current): Extensions run with same privileges as AI agent
|
||||||
- B) Multi-version: Side-by-side versions (`.specify/extensions/jira@1.0/`, `.specify/extensions/jira@2.0/`)
|
- B) Permission declarations: Extensions declare `filesystem:read`, `network:external`, etc.
|
||||||
- C) Per-branch: Different branches use different versions
|
- C) Opt-in sandboxing: Organizations can enable permission enforcement
|
||||||
|
|
||||||
**Recommendation**: A initially (simpler), consider B in future if needed
|
**Status**: Deferred to future version. Currently using trust-based model where users trust extension authors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Package Signatures (Future)
|
||||||
|
|
||||||
|
**Question**: Should extensions be cryptographically signed?
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
|
||||||
|
- A) No signatures (current): Trust based on catalog source
|
||||||
|
- B) GPG/Sigstore signatures: Verify package integrity
|
||||||
|
- C) Catalog-level verification: Catalog maintainers verify packages
|
||||||
|
|
||||||
|
**Status**: Deferred to future version. `checksum` field is available in catalog schema but not enforced.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-03-09T00:00:00Z",
|
"updated_at": "2026-03-13T12:00:00Z",
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"azure-devops": {
|
"azure-devops": {
|
||||||
@@ -74,6 +74,37 @@
|
|||||||
"created_at": "2026-02-22T00:00:00Z",
|
"created_at": "2026-02-22T00:00:00Z",
|
||||||
"updated_at": "2026-02-22T00:00:00Z"
|
"updated_at": "2026-02-22T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"doctor": {
|
||||||
|
"name": "Project Health Check",
|
||||||
|
"id": "doctor",
|
||||||
|
"description": "Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git.",
|
||||||
|
"author": "KhawarHabibKhan",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/KhawarHabibKhan/spec-kit-doctor/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/KhawarHabibKhan/spec-kit-doctor",
|
||||||
|
"homepage": "https://github.com/KhawarHabibKhan/spec-kit-doctor",
|
||||||
|
"documentation": "https://github.com/KhawarHabibKhan/spec-kit-doctor/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/KhawarHabibKhan/spec-kit-doctor/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 1,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"diagnostics",
|
||||||
|
"health-check",
|
||||||
|
"validation",
|
||||||
|
"project-structure"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-13T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-13T00:00:00Z"
|
||||||
|
},
|
||||||
"fleet": {
|
"fleet": {
|
||||||
"name": "Fleet Orchestrator",
|
"name": "Fleet Orchestrator",
|
||||||
"id": "fleet",
|
"id": "fleet",
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-02-03T00:00:00Z",
|
"updated_at": "2026-03-10T00:00:00Z",
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
||||||
"extensions": {}
|
"extensions": {
|
||||||
|
"selftest": {
|
||||||
|
"name": "Spec Kit Self-Test Utility",
|
||||||
|
"id": "selftest",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Verifies catalog extensions by programmatically walking through the discovery, installation, and registration lifecycle.",
|
||||||
|
"author": "spec-kit-core",
|
||||||
|
"repository": "https://github.com/github/spec-kit",
|
||||||
|
"download_url": "https://github.com/github/spec-kit/releases/download/selftest-v1.0.0/selftest.zip",
|
||||||
|
"tags": [
|
||||||
|
"testing",
|
||||||
|
"core",
|
||||||
|
"utility"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
69
extensions/selftest/commands/selftest.md
Normal file
69
extensions/selftest/commands/selftest.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
description: "Validate the lifecycle of an extension from the catalog."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Extension Self-Test: `$ARGUMENTS`
|
||||||
|
|
||||||
|
This command drives a self-test simulating the developer experience with the `$ARGUMENTS` extension.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Validate the end-to-end lifecycle (discovery, installation, registration) for the extension: `$ARGUMENTS`.
|
||||||
|
If `$ARGUMENTS` is empty, you must tell the user to provide an extension name, for example: `/speckit.selftest.extension linear`.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Catalog Discovery Validation
|
||||||
|
|
||||||
|
Check if the extension exists in the Spec Kit catalog.
|
||||||
|
Execute this command and verify that it completes successfully and that the returned extension ID exactly matches `$ARGUMENTS`. If the command fails or the ID does not match `$ARGUMENTS`, fail the test.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension info "$ARGUMENTS"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Simulate Installation
|
||||||
|
|
||||||
|
First, try to add the extension to the current workspace configuration directly. If the catalog provides the extension as `install_allowed: false` (discovery-only), this step is *expected* to fail.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension add "$ARGUMENTS"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, simulate adding the extension by installing it from its catalog download URL, which should bypass the restriction.
|
||||||
|
Obtain the extension's `download_url` from the catalog metadata (for example, via a catalog info command or UI), then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension add "$ARGUMENTS" --from "<download_url>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Registration Verification
|
||||||
|
|
||||||
|
Once the `add` command completes, verify the installation by checking the project configuration.
|
||||||
|
Use terminal tools (like `cat`) to verify that the following file contains a record for `$ARGUMENTS`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat .specify/extensions/.registry/$ARGUMENTS.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Verification Report
|
||||||
|
|
||||||
|
Analyze the standard output of the three steps.
|
||||||
|
Generate a terminal-style test output format detailing the results of discovery, installation, and registration. Return this directly to the user.
|
||||||
|
|
||||||
|
Example output format:
|
||||||
|
```text
|
||||||
|
============================= test session starts ==============================
|
||||||
|
collected 3 items
|
||||||
|
|
||||||
|
test_selftest_discovery.py::test_catalog_search [PASS/FAIL]
|
||||||
|
Details: [Provide execution result of specify extension search]
|
||||||
|
|
||||||
|
test_selftest_installation.py::test_extension_add [PASS/FAIL]
|
||||||
|
Details: [Provide execution result of specify extension add]
|
||||||
|
|
||||||
|
test_selftest_registration.py::test_config_verification [PASS/FAIL]
|
||||||
|
Details: [Provide execution result of registry record verification]
|
||||||
|
|
||||||
|
============================== [X] passed in ... ==============================
|
||||||
|
```
|
||||||
16
extensions/selftest/extension.yml
Normal file
16
extensions/selftest/extension.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
schema_version: "1.0"
|
||||||
|
extension:
|
||||||
|
id: selftest
|
||||||
|
name: Spec Kit Self-Test Utility
|
||||||
|
version: 1.0.0
|
||||||
|
description: Verifies catalog extensions by programmatically walking through the discovery, installation, and registration lifecycle.
|
||||||
|
author: spec-kit-core
|
||||||
|
repository: https://github.com/github/spec-kit
|
||||||
|
license: MIT
|
||||||
|
requires:
|
||||||
|
speckit_version: ">=0.2.0"
|
||||||
|
provides:
|
||||||
|
commands:
|
||||||
|
- name: speckit.selftest.extension
|
||||||
|
file: commands/selftest.md
|
||||||
|
description: Validate the lifecycle of an extension from the catalog.
|
||||||
54
newsletters/2026-February.md
Normal file
54
newsletters/2026-February.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Spec Kit - February 2026 Newsletter
|
||||||
|
|
||||||
|
This edition covers Spec Kit activity in February 2026. Versions v0.1.7 through v0.1.13 shipped during the month, addressing bugs and adding features including a dual-catalog extension system and additional agent integrations. Community activity included blog posts, tutorials, and meetup sessions. A category summary is in the table below, followed by details.
|
||||||
|
|
||||||
|
| **Spec Kit Core (Feb 2026)** | **Community & Content** | **Roadmap & Next** |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Versions **v0.1.7** through **v0.1.13** shipped with bug fixes and features, including a **dual-catalog extension system** and new agent integrations. Over 300 issues were closed (of ~800 filed). The repo reached 71k stars and 6.4k forks. [\[github.com\]](https://github.com/github/spec-kit/releases) [\[github.com\]](https://github.com/github/spec-kit/issues) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit) | Eduardo Luz published a LinkedIn article on SDD and Spec Kit [\[linkedin.com\]](https://www.linkedin.com/pulse/specification-driven-development-sdd-github-spec-kit-elevating-luz-tojmc?tl=en). Erick Matsen blogged a walkthrough of building a bioinformatics pipeline with Spec Kit [\[matsen.fredhutch.org\]](https://matsen.fredhutch.org/general/2026/02/10/spec-kit-walkthrough.html). Microsoft MVP [Eric Boyd](https://ericboyd.com/) (not the Microsoft AI Platform VP of the same name) presented at the Cleveland .NET User Group [\[ericboyd.com\]](https://ericboyd.com/events/cleveland-csharp-user-group-february-25-2026-spec-driven-development-sdd-github-spec-kit). | **v0.2.0** was released in early March, consolidating February's work. It added extensions for Jira and Azure DevOps, community plugin support, and agents for Tabnine CLI and Kiro CLI [\[github.com\]](https://github.com/github/spec-kit/releases). Future work includes spec lifecycle management and progress toward a stable 1.0 release [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html). |
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
## Spec Kit Project Updates
|
||||||
|
|
||||||
|
Spec Kit released versions **v0.1.7** through **v0.1.13** during February. Version 0.1.7 (early February) updated documentation for the newly introduced **dual-catalog extension system**, which allows both core and community extension catalogs to coexist. Subsequent patches (0.1.8, 0.1.9, etc.) bumped dependencies such as GitHub Actions versions and resolved minor issues. **v0.1.10** fixed YAML front-matter handling in generated files. By late February, **v0.1.12** and **v0.1.13** shipped with additional fixes in preparation for the next version bump. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||||
|
|
||||||
|
The main architectural addition was the **modular extension system** with separate "core" and "community" extension catalogs for third-party add-ons. Multiple community-contributed extensions were merged during the month, including a **Jira extension** for issue tracker integration, an **Azure DevOps extension**, and utility extensions for code review, retrospective documentation, and CI/CD sync. The pending 0.2.0 release changelog lists over a dozen changes from February, including the extension additions and support for **multiple agent catalogs concurrently**. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||||
|
|
||||||
|
By end of February, **over 330 issues/feature requests had been closed on GitHub** (out of ~870 filed to date). External contributors submitted pull requests including the **Tabnine CLI support**, which was merged in late February. The repository reached ~71k stars and crossed 6,000 forks. [\[github.com\]](https://github.com/github/spec-kit/issues) [\[github.com\]](https://github.com/github/spec-kit/releases) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit)
|
||||||
|
|
||||||
|
On the stability side, February's work focused on tightening core workflows and fixing edge-case bugs in the specification, planning, and task-generation commands. The team addressed file-handling issues (e.g., clarifying how output files are created/appended) and improved the reliability of the automated release pipeline. The project also added **Kiro CLI** to the supported agent list and updated integration scripts for Cursor and Code Interpreter, bringing the total number of supported AI coding assistants to over 20. [\[github.com\]](https://github.com/github/spec-kit/releases) [\[github.com\]](https://github.com/github/spec-kit)
|
||||||
|
|
||||||
|
## Community & Content
|
||||||
|
|
||||||
|
**Eduardo Luz** published a LinkedIn article on Feb 15 titled *"Specification Driven Development (SDD) and the GitHub Spec Kit: Elevating Software Engineering."* The article draws on his experience as a senior engineer to describe common causes of technical debt and inconsistent designs, and how SDD addresses them. It walks through Spec Kit's **four-layer approach** (Constitution, Design, Tasks, Implementation) and discusses treating specifications as a source of truth. The post generated discussion among software architects on LinkedIn about reducing misunderstandings and rework through spec-driven workflows. [\[linkedin.com\]](https://www.linkedin.com/pulse/specification-driven-development-sdd-github-spec-kit-elevating-luz-tojmc?tl=en)
|
||||||
|
|
||||||
|
**Erick Matsen** (Fred Hutchinson Cancer Center) posted a detailed walkthrough on Feb 10 titled *"Spec-Driven Development with spec-kit."* He describes building a **bioinformatics pipeline** in a single day using Spec Kit's workflow (from `speckit.constitution` to `speckit.implement`). The post includes command outputs and notes on decisions made along the way, such as refining the spec to add domain-specific requirements. He writes: "I really recommend this approach. This feels like the way software development should be." [\[matsen.fredhutch.org\]](https://matsen.fredhutch.org/general/2026/02/10/spec-kit-walkthrough.html) [\[github.com\]](https://github.com/mnriem/spec-kit-dotnet-cli-demo)
|
||||||
|
|
||||||
|
Several other tutorials and guides appeared during the month. An article on *IntuitionLabs* (updated Feb 21) provided a guide to Spec Kit covering the philosophy behind SDD and a walkthrough of the four-phase workflow with examples. A piece by Ry Walker (Feb 22) summarized key aspects of Spec Kit, noting its agent-agnostic design and 71k-star count. Microsoft's Developer Blog post from late 2025 (*"Diving Into Spec-Driven Development with GitHub Spec Kit"* by Den Delimarsky) continued to circulate among new users. [\[intuitionlabs.ai\]](https://intuitionlabs.ai/articles/spec-driven-development-spec-kit) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit)
|
||||||
|
|
||||||
|
On **Feb 25**, the Cleveland C# .NET User Group hosted a session titled *"Spec Driven Development with GitHub Spec Kit."* The talk was delivered by Microsoft MVP **[Eric Boyd](https://ericboyd.com/)** (Cleveland-based .NET developer; not to be confused with the Microsoft AI Platform VP of the same name). Boyd covered how specs change an AI coding assistant's output, patterns for iterating and refining specs over multiple cycles, and moving from ad-hoc prompting to a repeatable spec-driven workflow. Other groups, including GDG Madison, also listed sessions on spec-driven development in late February and early March. [\[ericboyd.com\]](https://ericboyd.com/events/cleveland-csharp-user-group-february-25-2026-spec-driven-development-sdd-github-spec-kit)
|
||||||
|
|
||||||
|
On GitHub, the **Spec Kit Discussions forum** saw activity around installation troubleshooting, handling multi-feature projects with Spec Kit's branching model, and feature suggestions. One thread discussed how Spec Kit treats each spec as a short-lived artifact tied to a feature branch, which led to discussion about future support for long-running "spec of record" use cases. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
|
||||||
|
|
||||||
|
## SDD Ecosystem
|
||||||
|
|
||||||
|
Other spec-driven development tools also saw activity in February.
|
||||||
|
|
||||||
|
AWS **Kiro** released version 0.10 on Feb 18 with two new spec workflows: a **Design-First** mode (starting from architecture/pseudocode to derive requirements) and a **Bugfix** mode (structured root-cause analysis producing a `bugfix.md` spec file). Kiro also added hunk-level code review for AI-generated changes and pre/post task hooks for custom automation. AWS expanded Kiro to GovCloud regions on Feb 17 for government compliance use cases. [\[kiro.dev\]](https://kiro.dev/changelog/)
|
||||||
|
|
||||||
|
**OpenSpec** (by Fission AI), a lightweight SDD framework, reached ~29.3k stars and nearly 2k forks. Its community published guides and comparisons during the month, including *"Spec-Driven Development Made Easy: A Practical Guide with OpenSpec."* OpenSpec emphasizes simplicity and flexibility, integrating with multiple AI coding assistants via YAML configs.
|
||||||
|
|
||||||
|
**Tessl** remained in private beta. As described by Thoughtworks writer Birgitta Boeckeler, Tessl pursues a **spec-as-source** model where specifications are maintained long-term and directly generate code files one-to-one, with generated code labeled as "do not edit." This contrasts with Spec Kit's current approach of creating specs per feature/branch. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
|
||||||
|
|
||||||
|
An **arXiv preprint** (January 2026) categorized SDD implementations into three levels: *spec-first*, *spec-anchored*, and *spec-as-source*. Spec Kit was identified as primarily spec-first with elements of spec-anchored. Tech media published reviews including a *Vibe Coding* "GitHub Spec Kit Review (2026)" and a blog post titled *"Putting Spec Kit Through Its Paces: Radical Idea or Reinvented Waterfall?"* which concluded that SDD with AI assistance is more iterative than traditional Waterfall. [\[intuitionlabs.ai\]](https://intuitionlabs.ai/articles/spec-driven-development-spec-kit) [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
**v0.2.0** was released on March 10, 2026, consolidating the month's work. It includes new extensions (Jira, Azure DevOps, review, sync), support for multiple extension catalogs and community plugins, and additional agent integrations (Tabnine CLI, Kiro CLI). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||||
|
|
||||||
|
Areas under discussion or in progress for future development:
|
||||||
|
|
||||||
|
- **Spec lifecycle management** -- supporting longer-lived specifications that can evolve across multiple iterations, rather than being tied to a single feature branch. Users have raised this in GitHub Discussions, and the concept of "spec-anchored" development is under consideration. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
|
||||||
|
- **CI/CD integration** -- incorporating Spec Kit verification (e.g., `speckit.checklist` or `speckit.verify`) into pull request workflows and project management tools. February's Jira and Azure DevOps extensions are a step in this direction. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||||
|
- **Continued agent support** -- adding integrations as new AI coding assistants emerge. The project currently supports over 20 agents and has been adding new ones (Kiro CLI, Tabnine CLI) as they become available. [\[github.com\]](https://github.com/github/spec-kit)
|
||||||
|
- **Community ecosystem** -- the open extension model allows external contributors to add functionality directly. February's Jira and Azure DevOps plugins were community-contributed. The Spec Kit README now links to community walkthrough demos for .NET, Spring Boot, and other stacks. [\[github.com\]](https://github.com/github/spec-kit)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.1.14"
|
version = "0.2.1"
|
||||||
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 = [
|
||||||
@@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"truststore>=0.10.4",
|
"truststore>=0.10.4",
|
||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"packaging>=23.0",
|
"packaging>=23.0",
|
||||||
|
"pathspec>=0.12.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -79,15 +79,28 @@ SCRIPT_DIR="$(CDPATH="" 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
|
||||||
eval $(get_feature_paths)
|
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||||
|
eval "$_paths_output"
|
||||||
|
unset _paths_output
|
||||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||||
|
|
||||||
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
|
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
|
||||||
if $PATHS_ONLY; then
|
if $PATHS_ONLY; then
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
# Minimal JSON paths payload (no validation performed)
|
# Minimal JSON paths payload (no validation performed)
|
||||||
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
if has_jq; then
|
||||||
"$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
|
jq -cn \
|
||||||
|
--arg repo_root "$REPO_ROOT" \
|
||||||
|
--arg branch "$CURRENT_BRANCH" \
|
||||||
|
--arg feature_dir "$FEATURE_DIR" \
|
||||||
|
--arg feature_spec "$FEATURE_SPEC" \
|
||||||
|
--arg impl_plan "$IMPL_PLAN" \
|
||||||
|
--arg tasks "$TASKS" \
|
||||||
|
'{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'
|
||||||
|
else
|
||||||
|
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
||||||
|
"$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "REPO_ROOT: $REPO_ROOT"
|
echo "REPO_ROOT: $REPO_ROOT"
|
||||||
echo "BRANCH: $CURRENT_BRANCH"
|
echo "BRANCH: $CURRENT_BRANCH"
|
||||||
@@ -141,14 +154,25 @@ fi
|
|||||||
# Output results
|
# Output results
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
# Build JSON array of documents
|
# Build JSON array of documents
|
||||||
if [[ ${#docs[@]} -eq 0 ]]; then
|
if has_jq; then
|
||||||
json_docs="[]"
|
if [[ ${#docs[@]} -eq 0 ]]; then
|
||||||
|
json_docs="[]"
|
||||||
|
else
|
||||||
|
json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
|
||||||
|
fi
|
||||||
|
jq -cn \
|
||||||
|
--arg feature_dir "$FEATURE_DIR" \
|
||||||
|
--argjson docs "$json_docs" \
|
||||||
|
'{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'
|
||||||
else
|
else
|
||||||
json_docs=$(printf '"%s",' "${docs[@]}")
|
if [[ ${#docs[@]} -eq 0 ]]; then
|
||||||
json_docs="[${json_docs%,}]"
|
json_docs="[]"
|
||||||
|
else
|
||||||
|
json_docs=$(printf '"%s",' "${docs[@]}")
|
||||||
|
json_docs="[${json_docs%,}]"
|
||||||
|
fi
|
||||||
|
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
|
|
||||||
else
|
else
|
||||||
# Text output
|
# Text output
|
||||||
echo "FEATURE_DIR:$FEATURE_DIR"
|
echo "FEATURE_DIR:$FEATURE_DIR"
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ find_feature_dir_by_prefix() {
|
|||||||
# Multiple matches - this shouldn't happen with proper naming convention
|
# Multiple matches - this shouldn't happen with proper naming convention
|
||||||
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
|
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
|
||||||
echo "Please ensure only one spec directory exists per numeric prefix." >&2
|
echo "Please ensure only one spec directory exists per numeric prefix." >&2
|
||||||
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,21 +134,42 @@ get_feature_paths() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Use prefix-based lookup to support multiple branches per spec
|
# Use prefix-based lookup to support multiple branches per spec
|
||||||
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
|
local feature_dir
|
||||||
|
if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
|
||||||
|
echo "ERROR: Failed to resolve feature directory" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
# Use printf '%q' to safely quote values, preventing shell injection
|
||||||
REPO_ROOT='$repo_root'
|
# via crafted branch names or paths containing special characters
|
||||||
CURRENT_BRANCH='$current_branch'
|
printf 'REPO_ROOT=%q\n' "$repo_root"
|
||||||
HAS_GIT='$has_git_repo'
|
printf 'CURRENT_BRANCH=%q\n' "$current_branch"
|
||||||
FEATURE_DIR='$feature_dir'
|
printf 'HAS_GIT=%q\n' "$has_git_repo"
|
||||||
FEATURE_SPEC='$feature_dir/spec.md'
|
printf 'FEATURE_DIR=%q\n' "$feature_dir"
|
||||||
IMPL_PLAN='$feature_dir/plan.md'
|
printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
|
||||||
TASKS='$feature_dir/tasks.md'
|
printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
|
||||||
RESEARCH='$feature_dir/research.md'
|
printf 'TASKS=%q\n' "$feature_dir/tasks.md"
|
||||||
DATA_MODEL='$feature_dir/data-model.md'
|
printf 'RESEARCH=%q\n' "$feature_dir/research.md"
|
||||||
QUICKSTART='$feature_dir/quickstart.md'
|
printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
|
||||||
CONTRACTS_DIR='$feature_dir/contracts'
|
printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
|
||||||
EOF
|
printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if jq is available for safe JSON construction
|
||||||
|
has_jq() {
|
||||||
|
command -v jq >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
|
||||||
|
# Handles backslash, double-quote, and control characters (newline, tab, carriage return).
|
||||||
|
json_escape() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s//\\/\\\\}"
|
||||||
|
s="${s//\"/\\\"}"
|
||||||
|
s="${s//$'\n'/\\n}"
|
||||||
|
s="${s//$'\t'/\\t}"
|
||||||
|
s="${s//$'\r'/\\r}"
|
||||||
|
printf '%s' "$s"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||||
|
|||||||
@@ -162,6 +162,17 @@ clean_branch_name() {
|
|||||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
|
||||||
|
json_escape() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s//\\/\\\\}"
|
||||||
|
s="${s//\"/\\\"}"
|
||||||
|
s="${s//$'\n'/\\n}"
|
||||||
|
s="${s//$'\t'/\\t}"
|
||||||
|
s="${s//$'\r'/\\r}"
|
||||||
|
printf '%s' "$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.
|
||||||
@@ -300,14 +311,22 @@ TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
|
|||||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||||
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
||||||
|
|
||||||
# Set the SPECIFY_FEATURE environment variable for the current session
|
# Inform the user how to persist the feature variable in their own shell
|
||||||
export SPECIFY_FEATURE="$BRANCH_NAME"
|
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
|
||||||
|
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
jq -cn \
|
||||||
|
--arg branch_name "$BRANCH_NAME" \
|
||||||
|
--arg spec_file "$SPEC_FILE" \
|
||||||
|
--arg feature_num "$FEATURE_NUM" \
|
||||||
|
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
|
||||||
|
else
|
||||||
|
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||||
echo "SPEC_FILE: $SPEC_FILE"
|
echo "SPEC_FILE: $SPEC_FILE"
|
||||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||||
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ SCRIPT_DIR="$(CDPATH="" 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
|
||||||
eval $(get_feature_paths)
|
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||||
|
eval "$_paths_output"
|
||||||
|
unset _paths_output
|
||||||
|
|
||||||
# Check if we're on a proper feature branch (only for git repos)
|
# Check if we're on a proper feature branch (only for git repos)
|
||||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||||
@@ -49,8 +51,18 @@ fi
|
|||||||
|
|
||||||
# Output results
|
# Output results
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
if has_jq; then
|
||||||
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
|
jq -cn \
|
||||||
|
--arg feature_spec "$FEATURE_SPEC" \
|
||||||
|
--arg impl_plan "$IMPL_PLAN" \
|
||||||
|
--arg specs_dir "$FEATURE_DIR" \
|
||||||
|
--arg branch "$CURRENT_BRANCH" \
|
||||||
|
--arg has_git "$HAS_GIT" \
|
||||||
|
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'
|
||||||
|
else
|
||||||
|
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
||||||
|
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||||
|
|||||||
@@ -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, Tabnine CLI, Kiro CLI, Mistral Vibe or Antigravity
|
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Antigravity or Generic
|
||||||
# - 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|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic
|
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic
|
||||||
# Leave empty to update all existing agent files
|
# Leave empty to update all existing agent files
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -53,7 +53,9 @@ SCRIPT_DIR="$(CDPATH="" 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
|
||||||
eval $(get_feature_paths)
|
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||||
|
eval "$_paths_output"
|
||||||
|
unset _paths_output
|
||||||
|
|
||||||
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
|
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
|
||||||
AGENT_TYPE="${1:-}"
|
AGENT_TYPE="${1:-}"
|
||||||
@@ -71,13 +73,16 @@ 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"
|
QODER_FILE="$REPO_ROOT/QODER.md"
|
||||||
AMP_FILE="$REPO_ROOT/AGENTS.md"
|
# AMP, Kiro CLI, and IBM Bob all share AGENTS.md — use AGENTS_FILE to avoid
|
||||||
|
# updating the same file multiple times.
|
||||||
|
AMP_FILE="$AGENTS_FILE"
|
||||||
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
||||||
TABNINE_FILE="$REPO_ROOT/TABNINE.md"
|
TABNINE_FILE="$REPO_ROOT/TABNINE.md"
|
||||||
KIRO_FILE="$REPO_ROOT/AGENTS.md"
|
KIRO_FILE="$AGENTS_FILE"
|
||||||
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
||||||
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
BOB_FILE="$AGENTS_FILE"
|
||||||
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
|
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
|
||||||
|
KIMI_FILE="$REPO_ROOT/KIMI.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"
|
||||||
@@ -111,6 +116,8 @@ log_warning() {
|
|||||||
# Cleanup function for temporary files
|
# Cleanup function for temporary files
|
||||||
cleanup() {
|
cleanup() {
|
||||||
local exit_code=$?
|
local exit_code=$?
|
||||||
|
# Disarm traps to prevent re-entrant loop
|
||||||
|
trap - EXIT INT TERM
|
||||||
rm -f /tmp/agent_update_*_$$
|
rm -f /tmp/agent_update_*_$$
|
||||||
rm -f /tmp/manual_additions_$$
|
rm -f /tmp/manual_additions_$$
|
||||||
exit $exit_code
|
exit $exit_code
|
||||||
@@ -475,7 +482,7 @@ update_existing_agent_file() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Update timestamp
|
# Update timestamp
|
||||||
if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
|
if [[ "$line" =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
|
||||||
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
|
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
|
||||||
else
|
else
|
||||||
echo "$line" >> "$temp_file"
|
echo "$line" >> "$temp_file"
|
||||||
@@ -606,71 +613,74 @@ update_specific_agent() {
|
|||||||
|
|
||||||
case "$agent_type" in
|
case "$agent_type" in
|
||||||
claude)
|
claude)
|
||||||
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
|
||||||
;;
|
;;
|
||||||
gemini)
|
gemini)
|
||||||
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1
|
||||||
;;
|
;;
|
||||||
copilot)
|
copilot)
|
||||||
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1
|
||||||
;;
|
;;
|
||||||
cursor-agent)
|
cursor-agent)
|
||||||
update_agent_file "$CURSOR_FILE" "Cursor IDE"
|
update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1
|
||||||
;;
|
;;
|
||||||
qwen)
|
qwen)
|
||||||
update_agent_file "$QWEN_FILE" "Qwen Code"
|
update_agent_file "$QWEN_FILE" "Qwen Code" || return 1
|
||||||
;;
|
;;
|
||||||
opencode)
|
opencode)
|
||||||
update_agent_file "$AGENTS_FILE" "opencode"
|
update_agent_file "$AGENTS_FILE" "opencode" || return 1
|
||||||
;;
|
;;
|
||||||
codex)
|
codex)
|
||||||
update_agent_file "$AGENTS_FILE" "Codex CLI"
|
update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1
|
||||||
;;
|
;;
|
||||||
windsurf)
|
windsurf)
|
||||||
update_agent_file "$WINDSURF_FILE" "Windsurf"
|
update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1
|
||||||
;;
|
;;
|
||||||
kilocode)
|
kilocode)
|
||||||
update_agent_file "$KILOCODE_FILE" "Kilo Code"
|
update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1
|
||||||
;;
|
;;
|
||||||
auggie)
|
auggie)
|
||||||
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
|
update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1
|
||||||
;;
|
;;
|
||||||
roo)
|
roo)
|
||||||
update_agent_file "$ROO_FILE" "Roo Code"
|
update_agent_file "$ROO_FILE" "Roo Code" || return 1
|
||||||
;;
|
;;
|
||||||
codebuddy)
|
codebuddy)
|
||||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1
|
||||||
;;
|
;;
|
||||||
qodercli)
|
qodercli)
|
||||||
update_agent_file "$QODER_FILE" "Qoder CLI"
|
update_agent_file "$QODER_FILE" "Qoder CLI" || return 1
|
||||||
;;
|
;;
|
||||||
amp)
|
amp)
|
||||||
update_agent_file "$AMP_FILE" "Amp"
|
update_agent_file "$AMP_FILE" "Amp" || return 1
|
||||||
;;
|
;;
|
||||||
shai)
|
shai)
|
||||||
update_agent_file "$SHAI_FILE" "SHAI"
|
update_agent_file "$SHAI_FILE" "SHAI" || return 1
|
||||||
;;
|
;;
|
||||||
tabnine)
|
tabnine)
|
||||||
update_agent_file "$TABNINE_FILE" "Tabnine CLI"
|
update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1
|
||||||
;;
|
;;
|
||||||
kiro-cli)
|
kiro-cli)
|
||||||
update_agent_file "$KIRO_FILE" "Kiro CLI"
|
update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1
|
||||||
;;
|
;;
|
||||||
agy)
|
agy)
|
||||||
update_agent_file "$AGY_FILE" "Antigravity"
|
update_agent_file "$AGY_FILE" "Antigravity" || return 1
|
||||||
;;
|
;;
|
||||||
bob)
|
bob)
|
||||||
update_agent_file "$BOB_FILE" "IBM Bob"
|
update_agent_file "$BOB_FILE" "IBM Bob" || return 1
|
||||||
;;
|
;;
|
||||||
vibe)
|
vibe)
|
||||||
update_agent_file "$VIBE_FILE" "Mistral Vibe"
|
update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1
|
||||||
|
;;
|
||||||
|
kimi)
|
||||||
|
update_agent_file "$KIMI_FILE" "Kimi Code" || return 1
|
||||||
;;
|
;;
|
||||||
generic)
|
generic)
|
||||||
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
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|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic"
|
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -678,101 +688,53 @@ update_specific_agent() {
|
|||||||
|
|
||||||
update_all_existing_agents() {
|
update_all_existing_agents() {
|
||||||
local found_agent=false
|
local found_agent=false
|
||||||
|
local _updated_paths=()
|
||||||
|
|
||||||
# Check each possible agent file and update if it exists
|
# Helper: skip non-existent files and files already updated (dedup by
|
||||||
if [[ -f "$CLAUDE_FILE" ]]; then
|
# realpath so that variables pointing to the same file — e.g. AMP_FILE,
|
||||||
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once).
|
||||||
|
# Uses a linear array instead of associative array for bash 3.2 compatibility.
|
||||||
|
update_if_new() {
|
||||||
|
local file="$1" name="$2"
|
||||||
|
[[ -f "$file" ]] || return 0
|
||||||
|
local real_path
|
||||||
|
real_path=$(realpath "$file" 2>/dev/null || echo "$file")
|
||||||
|
local p
|
||||||
|
if [[ ${#_updated_paths[@]} -gt 0 ]]; then
|
||||||
|
for p in "${_updated_paths[@]}"; do
|
||||||
|
[[ "$p" == "$real_path" ]] && return 0
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
update_agent_file "$file" "$name" || return 1
|
||||||
|
_updated_paths+=("$real_path")
|
||||||
found_agent=true
|
found_agent=true
|
||||||
fi
|
}
|
||||||
|
|
||||||
if [[ -f "$GEMINI_FILE" ]]; then
|
update_if_new "$CLAUDE_FILE" "Claude Code"
|
||||||
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
update_if_new "$GEMINI_FILE" "Gemini CLI"
|
||||||
found_agent=true
|
update_if_new "$COPILOT_FILE" "GitHub Copilot"
|
||||||
fi
|
update_if_new "$CURSOR_FILE" "Cursor IDE"
|
||||||
|
update_if_new "$QWEN_FILE" "Qwen Code"
|
||||||
if [[ -f "$COPILOT_FILE" ]]; then
|
update_if_new "$AGENTS_FILE" "Codex/opencode"
|
||||||
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
update_if_new "$AMP_FILE" "Amp"
|
||||||
found_agent=true
|
update_if_new "$KIRO_FILE" "Kiro CLI"
|
||||||
fi
|
update_if_new "$BOB_FILE" "IBM Bob"
|
||||||
|
update_if_new "$WINDSURF_FILE" "Windsurf"
|
||||||
if [[ -f "$CURSOR_FILE" ]]; then
|
update_if_new "$KILOCODE_FILE" "Kilo Code"
|
||||||
update_agent_file "$CURSOR_FILE" "Cursor IDE"
|
update_if_new "$AUGGIE_FILE" "Auggie CLI"
|
||||||
found_agent=true
|
update_if_new "$ROO_FILE" "Roo Code"
|
||||||
fi
|
update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||||
|
update_if_new "$SHAI_FILE" "SHAI"
|
||||||
if [[ -f "$QWEN_FILE" ]]; then
|
update_if_new "$TABNINE_FILE" "Tabnine CLI"
|
||||||
update_agent_file "$QWEN_FILE" "Qwen Code"
|
update_if_new "$QODER_FILE" "Qoder CLI"
|
||||||
found_agent=true
|
update_if_new "$AGY_FILE" "Antigravity"
|
||||||
fi
|
update_if_new "$VIBE_FILE" "Mistral Vibe"
|
||||||
|
update_if_new "$KIMI_FILE" "Kimi Code"
|
||||||
if [[ -f "$AGENTS_FILE" ]]; then
|
|
||||||
update_agent_file "$AGENTS_FILE" "Codex/opencode"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$WINDSURF_FILE" ]]; then
|
|
||||||
update_agent_file "$WINDSURF_FILE" "Windsurf"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$KILOCODE_FILE" ]]; then
|
|
||||||
update_agent_file "$KILOCODE_FILE" "Kilo Code"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$AUGGIE_FILE" ]]; then
|
|
||||||
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$ROO_FILE" ]]; then
|
|
||||||
update_agent_file "$ROO_FILE" "Roo Code"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$CODEBUDDY_FILE" ]]; then
|
|
||||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$SHAI_FILE" ]]; then
|
|
||||||
update_agent_file "$SHAI_FILE" "SHAI"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$TABNINE_FILE" ]]; then
|
|
||||||
update_agent_file "$TABNINE_FILE" "Tabnine CLI"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$QODER_FILE" ]]; then
|
|
||||||
update_agent_file "$QODER_FILE" "Qoder CLI"
|
|
||||||
found_agent=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$KIRO_FILE" ]]; then
|
|
||||||
update_agent_file "$KIRO_FILE" "Kiro CLI"
|
|
||||||
found_agent=true
|
|
||||||
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 [[ -f "$VIBE_FILE" ]]; then
|
|
||||||
update_agent_file "$VIBE_FILE" "Mistral Vibe"
|
|
||||||
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..."
|
||||||
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
print_summary() {
|
print_summary() {
|
||||||
@@ -792,7 +754,7 @@ print_summary() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic]"
|
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic]"
|
||||||
}
|
}
|
||||||
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ if ($branchName.Length -gt $maxBranchLength) {
|
|||||||
if ($hasGit) {
|
if ($hasGit) {
|
||||||
$branchCreated = $false
|
$branchCreated = $false
|
||||||
try {
|
try {
|
||||||
git checkout -b $branchName 2>$null | Out-Null
|
git checkout -q -b $branchName 2>$null | Out-Null
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
$branchCreated = $true
|
$branchCreated = $true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, tabnine, kiro-cli, agy, bob, vibe, qodercli)
|
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, generic)
|
||||||
|
|
||||||
.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','tabnine','kiro-cli','agy','bob','qodercli','vibe','generic')]
|
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','qodercli','vibe','kimi','generic')]
|
||||||
[string]$AgentType
|
[string]$AgentType
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,6 +63,7 @@ $KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
|||||||
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
|
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
|
||||||
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
$VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-agents.md'
|
$VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-agents.md'
|
||||||
|
$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.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'
|
||||||
|
|
||||||
@@ -330,7 +331,7 @@ function Update-ExistingAgentFile {
|
|||||||
if ($existingChanges -lt 2) { $output.Add($line); $existingChanges++ }
|
if ($existingChanges -lt 2) { $output.Add($line); $existingChanges++ }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ($line -match '\*\*Last updated\*\*: .*\d{4}-\d{2}-\d{2}') {
|
if ($line -match '(\*\*)?Last updated(\*\*)?: .*\d{4}-\d{2}-\d{2}') {
|
||||||
$output.Add(($line -replace '\d{4}-\d{2}-\d{2}',$Date.ToString('yyyy-MM-dd')))
|
$output.Add(($line -replace '\d{4}-\d{2}-\d{2}',$Date.ToString('yyyy-MM-dd')))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -406,8 +407,9 @@ function Update-SpecificAgent {
|
|||||||
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
|
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
|
||||||
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
|
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
|
||||||
'vibe' { Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe' }
|
'vibe' { Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe' }
|
||||||
|
'kimi' { Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code' }
|
||||||
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
|
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
|
||||||
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|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic'; return $false }
|
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|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic'; return $false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,6 +434,7 @@ function Update-AllExistingAgents {
|
|||||||
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $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 (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $VIBE_FILE) { if (-not (Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }; $found = $true }
|
if (Test-Path $VIBE_FILE) { if (-not (Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }; $found = $true }
|
||||||
|
if (Test-Path $KIMI_FILE) { if (-not (Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code')) { $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 }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,14 +8,19 @@ without bloating the core framework.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
import shutil
|
import shutil
|
||||||
|
import copy
|
||||||
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict, List, Any
|
from typing import Optional, Dict, List, Any, Callable, Set
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import pathspec
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from packaging import version as pkg_version
|
from packaging import version as pkg_version
|
||||||
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
||||||
@@ -36,6 +41,16 @@ class CompatibilityError(ExtensionError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CatalogEntry:
|
||||||
|
"""Represents a single catalog entry in the catalog stack."""
|
||||||
|
url: str
|
||||||
|
name: str
|
||||||
|
priority: int
|
||||||
|
install_allowed: bool
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
class ExtensionManifest:
|
class ExtensionManifest:
|
||||||
"""Represents and validates an extension manifest (extension.yml)."""
|
"""Represents and validates an extension manifest (extension.yml)."""
|
||||||
|
|
||||||
@@ -214,6 +229,54 @@ class ExtensionRegistry:
|
|||||||
}
|
}
|
||||||
self._save()
|
self._save()
|
||||||
|
|
||||||
|
def update(self, extension_id: str, metadata: dict):
|
||||||
|
"""Update extension metadata in registry, merging with existing entry.
|
||||||
|
|
||||||
|
Merges the provided metadata with the existing entry, preserving any
|
||||||
|
fields not specified in the new metadata. The installed_at timestamp
|
||||||
|
is always preserved from the original entry.
|
||||||
|
|
||||||
|
Use this method instead of add() when updating existing extension
|
||||||
|
metadata (e.g., enabling/disabling) to preserve the original
|
||||||
|
installation timestamp and other existing fields.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
extension_id: Extension ID
|
||||||
|
metadata: Extension metadata fields to update (merged with existing)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: If extension is not installed
|
||||||
|
"""
|
||||||
|
if extension_id not in self.data["extensions"]:
|
||||||
|
raise KeyError(f"Extension '{extension_id}' is not installed")
|
||||||
|
# Merge new metadata with existing, preserving original installed_at
|
||||||
|
existing = self.data["extensions"][extension_id]
|
||||||
|
# Merge: existing fields preserved, new fields override
|
||||||
|
merged = {**existing, **metadata}
|
||||||
|
# Always preserve original installed_at based on key existence, not truthiness,
|
||||||
|
# to handle cases where the field exists but may be falsy (legacy/corruption)
|
||||||
|
if "installed_at" in existing:
|
||||||
|
merged["installed_at"] = existing["installed_at"]
|
||||||
|
else:
|
||||||
|
# If not present in existing, explicitly remove from merged if caller provided it
|
||||||
|
merged.pop("installed_at", None)
|
||||||
|
self.data["extensions"][extension_id] = merged
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def restore(self, extension_id: str, metadata: dict):
|
||||||
|
"""Restore extension metadata to registry without modifying timestamps.
|
||||||
|
|
||||||
|
Use this method for rollback scenarios where you have a complete backup
|
||||||
|
of the registry entry (including installed_at) and want to restore it
|
||||||
|
exactly as it was.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
extension_id: Extension ID
|
||||||
|
metadata: Complete extension metadata including installed_at
|
||||||
|
"""
|
||||||
|
self.data["extensions"][extension_id] = dict(metadata)
|
||||||
|
self._save()
|
||||||
|
|
||||||
def remove(self, extension_id: str):
|
def remove(self, extension_id: str):
|
||||||
"""Remove extension from registry.
|
"""Remove extension from registry.
|
||||||
|
|
||||||
@@ -227,21 +290,28 @@ class ExtensionRegistry:
|
|||||||
def get(self, extension_id: str) -> Optional[dict]:
|
def get(self, extension_id: str) -> Optional[dict]:
|
||||||
"""Get extension metadata from registry.
|
"""Get extension metadata from registry.
|
||||||
|
|
||||||
|
Returns a deep copy to prevent callers from accidentally mutating
|
||||||
|
nested internal registry state without going through the write path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
extension_id: Extension ID
|
extension_id: Extension ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Extension metadata or None if not found
|
Deep copy of extension metadata, or None if not found
|
||||||
"""
|
"""
|
||||||
return self.data["extensions"].get(extension_id)
|
entry = self.data["extensions"].get(extension_id)
|
||||||
|
return copy.deepcopy(entry) if entry is not None else None
|
||||||
|
|
||||||
def list(self) -> Dict[str, dict]:
|
def list(self) -> Dict[str, dict]:
|
||||||
"""Get all installed extensions.
|
"""Get all installed extensions.
|
||||||
|
|
||||||
|
Returns a deep copy of the extensions mapping to prevent callers
|
||||||
|
from accidentally mutating nested internal registry state.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary of extension_id -> metadata
|
Dictionary of extension_id -> metadata (deep copies)
|
||||||
"""
|
"""
|
||||||
return self.data["extensions"]
|
return copy.deepcopy(self.data["extensions"])
|
||||||
|
|
||||||
def is_installed(self, extension_id: str) -> bool:
|
def is_installed(self, extension_id: str) -> bool:
|
||||||
"""Check if extension is installed.
|
"""Check if extension is installed.
|
||||||
@@ -268,6 +338,70 @@ class ExtensionManager:
|
|||||||
self.extensions_dir = project_root / ".specify" / "extensions"
|
self.extensions_dir = project_root / ".specify" / "extensions"
|
||||||
self.registry = ExtensionRegistry(self.extensions_dir)
|
self.registry = ExtensionRegistry(self.extensions_dir)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_extensionignore(source_dir: Path) -> Optional[Callable[[str, List[str]], Set[str]]]:
|
||||||
|
"""Load .extensionignore and return an ignore function for shutil.copytree.
|
||||||
|
|
||||||
|
The .extensionignore file uses .gitignore-compatible patterns (one per line).
|
||||||
|
Lines starting with '#' are comments. Blank lines are ignored.
|
||||||
|
The .extensionignore file itself is always excluded.
|
||||||
|
|
||||||
|
Pattern semantics mirror .gitignore:
|
||||||
|
- '*' matches anything except '/'
|
||||||
|
- '**' matches zero or more directories
|
||||||
|
- '?' matches any single character except '/'
|
||||||
|
- Trailing '/' restricts a pattern to directories only
|
||||||
|
- Patterns with '/' (other than trailing) are anchored to the root
|
||||||
|
- '!' negates a previously excluded pattern
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_dir: Path to the extension source directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An ignore function compatible with shutil.copytree, or None
|
||||||
|
if no .extensionignore file exists.
|
||||||
|
"""
|
||||||
|
ignore_file = source_dir / ".extensionignore"
|
||||||
|
if not ignore_file.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
lines: List[str] = ignore_file.read_text().splitlines()
|
||||||
|
|
||||||
|
# Normalise backslashes in patterns so Windows-authored files work
|
||||||
|
normalised: List[str] = []
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped and not stripped.startswith("#"):
|
||||||
|
normalised.append(stripped.replace("\\", "/"))
|
||||||
|
else:
|
||||||
|
# Preserve blanks/comments so pathspec line numbers stay stable
|
||||||
|
normalised.append(line)
|
||||||
|
|
||||||
|
# Always ignore the .extensionignore file itself
|
||||||
|
normalised.append(".extensionignore")
|
||||||
|
|
||||||
|
spec = pathspec.GitIgnoreSpec.from_lines(normalised)
|
||||||
|
|
||||||
|
def _ignore(directory: str, entries: List[str]) -> Set[str]:
|
||||||
|
ignored: Set[str] = set()
|
||||||
|
rel_dir = Path(directory).relative_to(source_dir)
|
||||||
|
for entry in entries:
|
||||||
|
rel_path = str(rel_dir / entry) if str(rel_dir) != "." else entry
|
||||||
|
# Normalise to forward slashes for consistent matching
|
||||||
|
rel_path_fwd = rel_path.replace("\\", "/")
|
||||||
|
|
||||||
|
entry_full = Path(directory) / entry
|
||||||
|
if entry_full.is_dir():
|
||||||
|
# Append '/' so directory-only patterns (e.g. tests/) match
|
||||||
|
if spec.match_file(rel_path_fwd + "/"):
|
||||||
|
ignored.add(entry)
|
||||||
|
else:
|
||||||
|
if spec.match_file(rel_path_fwd):
|
||||||
|
ignored.add(entry)
|
||||||
|
return ignored
|
||||||
|
|
||||||
|
return _ignore
|
||||||
|
|
||||||
def check_compatibility(
|
def check_compatibility(
|
||||||
self,
|
self,
|
||||||
manifest: ExtensionManifest,
|
manifest: ExtensionManifest,
|
||||||
@@ -341,7 +475,8 @@ class ExtensionManager:
|
|||||||
if dest_dir.exists():
|
if dest_dir.exists():
|
||||||
shutil.rmtree(dest_dir)
|
shutil.rmtree(dest_dir)
|
||||||
|
|
||||||
shutil.copytree(source_dir, dest_dir)
|
ignore_fn = self._load_extensionignore(source_dir)
|
||||||
|
shutil.copytree(source_dir, dest_dir, ignore=ignore_fn)
|
||||||
|
|
||||||
# Register commands with AI agents
|
# Register commands with AI agents
|
||||||
registered_commands = {}
|
registered_commands = {}
|
||||||
@@ -521,7 +656,7 @@ class ExtensionManager:
|
|||||||
result.append({
|
result.append({
|
||||||
"id": ext_id,
|
"id": ext_id,
|
||||||
"name": manifest.name,
|
"name": manifest.name,
|
||||||
"version": metadata["version"],
|
"version": metadata.get("version", "unknown"),
|
||||||
"description": manifest.description,
|
"description": manifest.description,
|
||||||
"enabled": metadata.get("enabled", True),
|
"enabled": metadata.get("enabled", True),
|
||||||
"installed_at": metadata.get("installed_at"),
|
"installed_at": metadata.get("installed_at"),
|
||||||
@@ -613,9 +748,9 @@ class CommandRegistrar:
|
|||||||
},
|
},
|
||||||
"qwen": {
|
"qwen": {
|
||||||
"dir": ".qwen/commands",
|
"dir": ".qwen/commands",
|
||||||
"format": "toml",
|
"format": "markdown",
|
||||||
"args": "{{args}}",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".toml"
|
"extension": ".md"
|
||||||
},
|
},
|
||||||
"opencode": {
|
"opencode": {
|
||||||
"dir": ".opencode/command",
|
"dir": ".opencode/command",
|
||||||
@@ -623,6 +758,12 @@ class CommandRegistrar:
|
|||||||
"args": "$ARGUMENTS",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md"
|
"extension": ".md"
|
||||||
},
|
},
|
||||||
|
"codex": {
|
||||||
|
"dir": ".codex/prompts",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md"
|
||||||
|
},
|
||||||
"windsurf": {
|
"windsurf": {
|
||||||
"dir": ".windsurf/workflows",
|
"dir": ".windsurf/workflows",
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
@@ -642,7 +783,7 @@ class CommandRegistrar:
|
|||||||
"extension": ".md"
|
"extension": ".md"
|
||||||
},
|
},
|
||||||
"roo": {
|
"roo": {
|
||||||
"dir": ".roo/rules",
|
"dir": ".roo/commands",
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"args": "$ARGUMENTS",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md"
|
"extension": ".md"
|
||||||
@@ -688,6 +829,12 @@ class CommandRegistrar:
|
|||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"args": "$ARGUMENTS",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md"
|
"extension": ".md"
|
||||||
|
},
|
||||||
|
"kimi": {
|
||||||
|
"dir": ".kimi/skills",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": "/SKILL.md"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,6 +1028,7 @@ class CommandRegistrar:
|
|||||||
|
|
||||||
# Write command file
|
# Write command file
|
||||||
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
|
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
|
||||||
|
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
dest_file.write_text(output)
|
dest_file.write_text(output)
|
||||||
|
|
||||||
# Generate companion .prompt.md for Copilot agents
|
# Generate companion .prompt.md for Copilot agents
|
||||||
@@ -892,6 +1040,7 @@ class CommandRegistrar:
|
|||||||
# Register aliases
|
# Register aliases
|
||||||
for alias in cmd_info.get("aliases", []):
|
for alias in cmd_info.get("aliases", []):
|
||||||
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
|
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
|
||||||
|
alias_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
alias_file.write_text(output)
|
alias_file.write_text(output)
|
||||||
# Generate companion .prompt.md for alias too
|
# Generate companion .prompt.md for alias too
|
||||||
if agent_name == "copilot":
|
if agent_name == "copilot":
|
||||||
@@ -976,6 +1125,7 @@ class ExtensionCatalog:
|
|||||||
"""Manages extension catalog fetching, caching, and searching."""
|
"""Manages extension catalog fetching, caching, and searching."""
|
||||||
|
|
||||||
DEFAULT_CATALOG_URL = "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
|
DEFAULT_CATALOG_URL = "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
|
||||||
|
COMMUNITY_CATALOG_URL = "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
|
||||||
CACHE_DURATION = 3600 # 1 hour in seconds
|
CACHE_DURATION = 3600 # 1 hour in seconds
|
||||||
|
|
||||||
def __init__(self, project_root: Path):
|
def __init__(self, project_root: Path):
|
||||||
@@ -990,43 +1140,123 @@ class ExtensionCatalog:
|
|||||||
self.cache_file = self.cache_dir / "catalog.json"
|
self.cache_file = self.cache_dir / "catalog.json"
|
||||||
self.cache_metadata_file = self.cache_dir / "catalog-metadata.json"
|
self.cache_metadata_file = self.cache_dir / "catalog-metadata.json"
|
||||||
|
|
||||||
def get_catalog_url(self) -> str:
|
def _validate_catalog_url(self, url: str) -> None:
|
||||||
"""Get catalog URL from config or use default.
|
"""Validate that a catalog URL uses HTTPS (localhost HTTP allowed).
|
||||||
|
|
||||||
Checks in order:
|
Args:
|
||||||
1. SPECKIT_CATALOG_URL environment variable
|
url: URL to validate
|
||||||
2. Default catalog URL
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
URL to fetch catalog from
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValidationError: If custom URL is invalid (non-HTTPS)
|
ValidationError: If URL is invalid or uses non-HTTPS scheme
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
# Environment variable override (useful for testing)
|
parsed = urlparse(url)
|
||||||
|
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
|
||||||
|
if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
|
||||||
|
raise ValidationError(
|
||||||
|
f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
|
||||||
|
"HTTP is only allowed for localhost."
|
||||||
|
)
|
||||||
|
if not parsed.netloc:
|
||||||
|
raise ValidationError("Catalog URL must be a valid URL with a host.")
|
||||||
|
|
||||||
|
def _load_catalog_config(self, config_path: Path) -> Optional[List[CatalogEntry]]:
|
||||||
|
"""Load catalog stack configuration from a YAML file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Path to extension-catalogs.yml
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ordered list of CatalogEntry objects, or None if file doesn't exist.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If any catalog entry has an invalid URL,
|
||||||
|
the file cannot be parsed, a priority value is invalid,
|
||||||
|
or the file exists but contains no valid catalog entries
|
||||||
|
(fail-closed for security).
|
||||||
|
"""
|
||||||
|
if not config_path.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(config_path.read_text()) or {}
|
||||||
|
except (yaml.YAMLError, OSError) as e:
|
||||||
|
raise ValidationError(
|
||||||
|
f"Failed to read catalog config {config_path}: {e}"
|
||||||
|
)
|
||||||
|
catalogs_data = data.get("catalogs", [])
|
||||||
|
if not catalogs_data:
|
||||||
|
# File exists but has no catalogs key or empty list - fail closed
|
||||||
|
raise ValidationError(
|
||||||
|
f"Catalog config {config_path} exists but contains no 'catalogs' entries. "
|
||||||
|
f"Remove the file to use built-in defaults, or add valid catalog entries."
|
||||||
|
)
|
||||||
|
if not isinstance(catalogs_data, list):
|
||||||
|
raise ValidationError(
|
||||||
|
f"Invalid catalog config: 'catalogs' must be a list, got {type(catalogs_data).__name__}"
|
||||||
|
)
|
||||||
|
entries: List[CatalogEntry] = []
|
||||||
|
skipped_entries: List[int] = []
|
||||||
|
for idx, item in enumerate(catalogs_data):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValidationError(
|
||||||
|
f"Invalid catalog entry at index {idx}: expected a mapping, got {type(item).__name__}"
|
||||||
|
)
|
||||||
|
url = str(item.get("url", "")).strip()
|
||||||
|
if not url:
|
||||||
|
skipped_entries.append(idx)
|
||||||
|
continue
|
||||||
|
self._validate_catalog_url(url)
|
||||||
|
try:
|
||||||
|
priority = int(item.get("priority", idx + 1))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValidationError(
|
||||||
|
f"Invalid priority for catalog '{item.get('name', idx + 1)}': "
|
||||||
|
f"expected integer, got {item.get('priority')!r}"
|
||||||
|
)
|
||||||
|
raw_install = item.get("install_allowed", False)
|
||||||
|
if isinstance(raw_install, str):
|
||||||
|
install_allowed = raw_install.strip().lower() in ("true", "yes", "1")
|
||||||
|
else:
|
||||||
|
install_allowed = bool(raw_install)
|
||||||
|
entries.append(CatalogEntry(
|
||||||
|
url=url,
|
||||||
|
name=str(item.get("name", f"catalog-{idx + 1}")),
|
||||||
|
priority=priority,
|
||||||
|
install_allowed=install_allowed,
|
||||||
|
description=str(item.get("description", "")),
|
||||||
|
))
|
||||||
|
entries.sort(key=lambda e: e.priority)
|
||||||
|
if not entries:
|
||||||
|
# All entries were invalid (missing URLs) - fail closed for security
|
||||||
|
raise ValidationError(
|
||||||
|
f"Catalog config {config_path} contains {len(catalogs_data)} entries but none have valid URLs "
|
||||||
|
f"(entries at indices {skipped_entries} were skipped). "
|
||||||
|
f"Each catalog entry must have a 'url' field."
|
||||||
|
)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def get_active_catalogs(self) -> List[CatalogEntry]:
|
||||||
|
"""Get the ordered list of active catalogs.
|
||||||
|
|
||||||
|
Resolution order:
|
||||||
|
1. SPECKIT_CATALOG_URL env var — single catalog replacing all defaults
|
||||||
|
2. Project-level .specify/extension-catalogs.yml
|
||||||
|
3. User-level ~/.specify/extension-catalogs.yml
|
||||||
|
4. Built-in default stack (default + community)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of CatalogEntry objects sorted by priority (ascending)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If a catalog URL is invalid
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 1. SPECKIT_CATALOG_URL env var replaces all defaults for backward compat
|
||||||
if env_value := os.environ.get("SPECKIT_CATALOG_URL"):
|
if env_value := os.environ.get("SPECKIT_CATALOG_URL"):
|
||||||
catalog_url = env_value.strip()
|
catalog_url = env_value.strip()
|
||||||
parsed = urlparse(catalog_url)
|
self._validate_catalog_url(catalog_url)
|
||||||
|
|
||||||
# Require HTTPS for security (prevent man-in-the-middle attacks)
|
|
||||||
# Allow http://localhost for local development/testing
|
|
||||||
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
|
|
||||||
if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
|
|
||||||
raise ValidationError(
|
|
||||||
f"Invalid SPECKIT_CATALOG_URL: must use HTTPS (got {parsed.scheme}://). "
|
|
||||||
"HTTP is only allowed for localhost."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not parsed.netloc:
|
|
||||||
raise ValidationError(
|
|
||||||
"Invalid SPECKIT_CATALOG_URL: must be a valid URL with a host."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Warn users when using a non-default catalog (once per instance)
|
|
||||||
if catalog_url != self.DEFAULT_CATALOG_URL:
|
if catalog_url != self.DEFAULT_CATALOG_URL:
|
||||||
if not getattr(self, "_non_default_catalog_warning_shown", False):
|
if not getattr(self, "_non_default_catalog_warning_shown", False):
|
||||||
print(
|
print(
|
||||||
@@ -1035,11 +1265,163 @@ class ExtensionCatalog:
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
self._non_default_catalog_warning_shown = True
|
self._non_default_catalog_warning_shown = True
|
||||||
|
return [CatalogEntry(url=catalog_url, name="custom", priority=1, install_allowed=True, description="Custom catalog via SPECKIT_CATALOG_URL")]
|
||||||
|
|
||||||
return catalog_url
|
# 2. Project-level config overrides all defaults
|
||||||
|
project_config_path = self.project_root / ".specify" / "extension-catalogs.yml"
|
||||||
|
catalogs = self._load_catalog_config(project_config_path)
|
||||||
|
if catalogs is not None:
|
||||||
|
return catalogs
|
||||||
|
|
||||||
# TODO: Support custom catalogs from .specify/extension-catalogs.yml
|
# 3. User-level config
|
||||||
return self.DEFAULT_CATALOG_URL
|
user_config_path = Path.home() / ".specify" / "extension-catalogs.yml"
|
||||||
|
catalogs = self._load_catalog_config(user_config_path)
|
||||||
|
if catalogs is not None:
|
||||||
|
return catalogs
|
||||||
|
|
||||||
|
# 4. Built-in default stack
|
||||||
|
return [
|
||||||
|
CatalogEntry(url=self.DEFAULT_CATALOG_URL, name="default", priority=1, install_allowed=True, description="Built-in catalog of installable extensions"),
|
||||||
|
CatalogEntry(url=self.COMMUNITY_CATALOG_URL, name="community", priority=2, install_allowed=False, description="Community-contributed extensions (discovery only)"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_catalog_url(self) -> str:
|
||||||
|
"""Get the primary catalog URL.
|
||||||
|
|
||||||
|
Returns the URL of the highest-priority catalog. Kept for backward
|
||||||
|
compatibility. Use get_active_catalogs() for full multi-catalog support.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
URL of the primary catalog
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If a catalog URL is invalid
|
||||||
|
"""
|
||||||
|
active = self.get_active_catalogs()
|
||||||
|
return active[0].url if active else self.DEFAULT_CATALOG_URL
|
||||||
|
|
||||||
|
def _fetch_single_catalog(self, entry: CatalogEntry, force_refresh: bool = False) -> Dict[str, Any]:
|
||||||
|
"""Fetch a single catalog with per-URL caching.
|
||||||
|
|
||||||
|
For the DEFAULT_CATALOG_URL, uses legacy cache files (self.cache_file /
|
||||||
|
self.cache_metadata_file) for backward compatibility. For all other URLs,
|
||||||
|
uses URL-hash-based cache files in self.cache_dir.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry: CatalogEntry describing the catalog to fetch
|
||||||
|
force_refresh: If True, bypass cache
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Catalog data dictionary
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ExtensionError: If catalog cannot be fetched or has invalid format
|
||||||
|
"""
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
# Determine cache file paths (backward compat for default catalog)
|
||||||
|
if entry.url == self.DEFAULT_CATALOG_URL:
|
||||||
|
cache_file = self.cache_file
|
||||||
|
cache_meta_file = self.cache_metadata_file
|
||||||
|
is_valid = not force_refresh and self.is_cache_valid()
|
||||||
|
else:
|
||||||
|
url_hash = hashlib.sha256(entry.url.encode()).hexdigest()[:16]
|
||||||
|
cache_file = self.cache_dir / f"catalog-{url_hash}.json"
|
||||||
|
cache_meta_file = self.cache_dir / f"catalog-{url_hash}-metadata.json"
|
||||||
|
is_valid = False
|
||||||
|
if not force_refresh and cache_file.exists() and cache_meta_file.exists():
|
||||||
|
try:
|
||||||
|
metadata = json.loads(cache_meta_file.read_text())
|
||||||
|
cached_at = datetime.fromisoformat(metadata.get("cached_at", ""))
|
||||||
|
if cached_at.tzinfo is None:
|
||||||
|
cached_at = cached_at.replace(tzinfo=timezone.utc)
|
||||||
|
age = (datetime.now(timezone.utc) - cached_at).total_seconds()
|
||||||
|
is_valid = age < self.CACHE_DURATION
|
||||||
|
except (json.JSONDecodeError, ValueError, KeyError, TypeError):
|
||||||
|
# If metadata is invalid or missing expected fields, treat cache as invalid
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Use cache if valid
|
||||||
|
if is_valid:
|
||||||
|
try:
|
||||||
|
return json.loads(cache_file.read_text())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fetch from network
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(entry.url, timeout=10) as response:
|
||||||
|
catalog_data = json.loads(response.read())
|
||||||
|
|
||||||
|
if "schema_version" not in catalog_data or "extensions" not in catalog_data:
|
||||||
|
raise ExtensionError(f"Invalid catalog format from {entry.url}")
|
||||||
|
|
||||||
|
# Save to cache
|
||||||
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
cache_file.write_text(json.dumps(catalog_data, indent=2))
|
||||||
|
cache_meta_file.write_text(json.dumps({
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"catalog_url": entry.url,
|
||||||
|
}, indent=2))
|
||||||
|
|
||||||
|
return catalog_data
|
||||||
|
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
raise ExtensionError(f"Failed to fetch catalog from {entry.url}: {e}")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ExtensionError(f"Invalid JSON in catalog from {entry.url}: {e}")
|
||||||
|
|
||||||
|
def _get_merged_extensions(self, force_refresh: bool = False) -> List[Dict[str, Any]]:
|
||||||
|
"""Fetch and merge extensions from all active catalogs.
|
||||||
|
|
||||||
|
Higher-priority (lower priority number) catalogs win on conflicts
|
||||||
|
(same extension id in two catalogs). Each extension dict is annotated with:
|
||||||
|
- _catalog_name: name of the source catalog
|
||||||
|
- _install_allowed: whether installation is allowed from this catalog
|
||||||
|
|
||||||
|
Catalogs that fail to fetch are skipped. Raises ExtensionError only if
|
||||||
|
ALL catalogs fail.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force_refresh: If True, bypass all caches
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of merged extension dicts
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ExtensionError: If all catalogs fail to fetch
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
active_catalogs = self.get_active_catalogs()
|
||||||
|
merged: Dict[str, Dict[str, Any]] = {}
|
||||||
|
any_success = False
|
||||||
|
|
||||||
|
for catalog_entry in active_catalogs:
|
||||||
|
try:
|
||||||
|
catalog_data = self._fetch_single_catalog(catalog_entry, force_refresh)
|
||||||
|
any_success = True
|
||||||
|
except ExtensionError as e:
|
||||||
|
print(
|
||||||
|
f"Warning: Could not fetch catalog '{catalog_entry.name}': {e}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ext_id, ext_data in catalog_data.get("extensions", {}).items():
|
||||||
|
if ext_id not in merged: # Higher-priority catalog wins
|
||||||
|
merged[ext_id] = {
|
||||||
|
**ext_data,
|
||||||
|
"id": ext_id,
|
||||||
|
"_catalog_name": catalog_entry.name,
|
||||||
|
"_install_allowed": catalog_entry.install_allowed,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not any_success and active_catalogs:
|
||||||
|
raise ExtensionError("Failed to fetch any extension catalog")
|
||||||
|
|
||||||
|
return list(merged.values())
|
||||||
|
|
||||||
def is_cache_valid(self) -> bool:
|
def is_cache_valid(self) -> bool:
|
||||||
"""Check if cached catalog is still valid.
|
"""Check if cached catalog is still valid.
|
||||||
@@ -1053,9 +1435,11 @@ class ExtensionCatalog:
|
|||||||
try:
|
try:
|
||||||
metadata = json.loads(self.cache_metadata_file.read_text())
|
metadata = json.loads(self.cache_metadata_file.read_text())
|
||||||
cached_at = datetime.fromisoformat(metadata.get("cached_at", ""))
|
cached_at = datetime.fromisoformat(metadata.get("cached_at", ""))
|
||||||
|
if cached_at.tzinfo is None:
|
||||||
|
cached_at = cached_at.replace(tzinfo=timezone.utc)
|
||||||
age_seconds = (datetime.now(timezone.utc) - cached_at).total_seconds()
|
age_seconds = (datetime.now(timezone.utc) - cached_at).total_seconds()
|
||||||
return age_seconds < self.CACHE_DURATION
|
return age_seconds < self.CACHE_DURATION
|
||||||
except (json.JSONDecodeError, ValueError, KeyError):
|
except (json.JSONDecodeError, ValueError, KeyError, TypeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def fetch_catalog(self, force_refresh: bool = False) -> Dict[str, Any]:
|
def fetch_catalog(self, force_refresh: bool = False) -> Dict[str, Any]:
|
||||||
@@ -1116,7 +1500,7 @@ class ExtensionCatalog:
|
|||||||
author: Optional[str] = None,
|
author: Optional[str] = None,
|
||||||
verified_only: bool = False,
|
verified_only: bool = False,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Search catalog for extensions.
|
"""Search catalog for extensions across all active catalogs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: Search query (searches name, description, tags)
|
query: Search query (searches name, description, tags)
|
||||||
@@ -1125,14 +1509,16 @@ class ExtensionCatalog:
|
|||||||
verified_only: If True, show only verified extensions
|
verified_only: If True, show only verified extensions
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of matching extension metadata
|
List of matching extension metadata, each annotated with
|
||||||
|
``_catalog_name`` and ``_install_allowed`` from its source catalog.
|
||||||
"""
|
"""
|
||||||
catalog = self.fetch_catalog()
|
all_extensions = self._get_merged_extensions()
|
||||||
extensions = catalog.get("extensions", {})
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for ext_id, ext_data in extensions.items():
|
for ext_data in all_extensions:
|
||||||
|
ext_id = ext_data["id"]
|
||||||
|
|
||||||
# Apply filters
|
# Apply filters
|
||||||
if verified_only and not ext_data.get("verified", False):
|
if verified_only and not ext_data.get("verified", False):
|
||||||
continue
|
continue
|
||||||
@@ -1158,25 +1544,26 @@ class ExtensionCatalog:
|
|||||||
if query_lower not in searchable_text:
|
if query_lower not in searchable_text:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
results.append({"id": ext_id, **ext_data})
|
results.append(ext_data)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_extension_info(self, extension_id: str) -> Optional[Dict[str, Any]]:
|
def get_extension_info(self, extension_id: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Get detailed information about a specific extension.
|
"""Get detailed information about a specific extension.
|
||||||
|
|
||||||
|
Searches all active catalogs in priority order.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
extension_id: ID of the extension
|
extension_id: ID of the extension
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Extension metadata or None if not found
|
Extension metadata (annotated with ``_catalog_name`` and
|
||||||
|
``_install_allowed``) or None if not found.
|
||||||
"""
|
"""
|
||||||
catalog = self.fetch_catalog()
|
all_extensions = self._get_merged_extensions()
|
||||||
extensions = catalog.get("extensions", {})
|
for ext_data in all_extensions:
|
||||||
|
if ext_data["id"] == extension_id:
|
||||||
if extension_id in extensions:
|
return ext_data
|
||||||
return {"id": extension_id, **extensions[extension_id]}
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def download_extension(self, extension_id: str, target_dir: Optional[Path] = None) -> Path:
|
def download_extension(self, extension_id: str, target_dir: Optional[Path] = None) -> Path:
|
||||||
@@ -1236,11 +1623,18 @@ class ExtensionCatalog:
|
|||||||
raise ExtensionError(f"Failed to save extension ZIP: {e}")
|
raise ExtensionError(f"Failed to save extension ZIP: {e}")
|
||||||
|
|
||||||
def clear_cache(self):
|
def clear_cache(self):
|
||||||
"""Clear the catalog cache."""
|
"""Clear the catalog cache (both legacy and URL-hash-based files)."""
|
||||||
if self.cache_file.exists():
|
if self.cache_file.exists():
|
||||||
self.cache_file.unlink()
|
self.cache_file.unlink()
|
||||||
if self.cache_metadata_file.exists():
|
if self.cache_metadata_file.exists():
|
||||||
self.cache_metadata_file.unlink()
|
self.cache_metadata_file.unlink()
|
||||||
|
# Also clear any per-URL hash-based cache files
|
||||||
|
if self.cache_dir.exists():
|
||||||
|
for extra_cache in self.cache_dir.glob("catalog-*.json"):
|
||||||
|
if extra_cache != self.cache_file:
|
||||||
|
extra_cache.unlink(missing_ok=True)
|
||||||
|
for extra_meta in self.cache_dir.glob("catalog-*-metadata.json"):
|
||||||
|
extra_meta.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ Given that feature description, do this:
|
|||||||
|
|
||||||
c. **Handle Validation Results**:
|
c. **Handle Validation Results**:
|
||||||
|
|
||||||
- **If all items pass**: Mark checklist complete and proceed to step 6
|
- **If all items pass**: Mark checklist complete and proceed to step 7
|
||||||
|
|
||||||
- **If items fail (excluding [NEEDS CLARIFICATION])**:
|
- **If items fail (excluding [NEEDS CLARIFICATION])**:
|
||||||
1. List the failing items and specific issues
|
1. List the failing items and specific issues
|
||||||
@@ -178,8 +178,6 @@ Given that feature description, do this:
|
|||||||
|
|
||||||
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
|
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
|
||||||
|
|
||||||
## General Guidelines
|
|
||||||
|
|
||||||
## Quick Guidelines
|
## Quick Guidelines
|
||||||
|
|
||||||
- Focus on **WHAT** users need and **WHY**.
|
- Focus on **WHAT** users need and **WHY**.
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ class TestAgentConfigConsistency:
|
|||||||
assert cfg["kiro-cli"]["dir"] == ".kiro/prompts"
|
assert cfg["kiro-cli"]["dir"] == ".kiro/prompts"
|
||||||
assert "q" not in cfg
|
assert "q" not in cfg
|
||||||
|
|
||||||
|
def test_extension_registrar_includes_codex(self):
|
||||||
|
"""Extension command registrar should include codex targeting .codex/prompts."""
|
||||||
|
cfg = CommandRegistrar.AGENT_CONFIGS
|
||||||
|
|
||||||
|
assert "codex" in cfg
|
||||||
|
assert cfg["codex"]["dir"] == ".codex/prompts"
|
||||||
|
|
||||||
def test_release_agent_lists_include_kiro_cli_and_exclude_q(self):
|
def test_release_agent_lists_include_kiro_cli_and_exclude_q(self):
|
||||||
"""Bash and PowerShell release scripts should agree on agent key set for Kiro."""
|
"""Bash and PowerShell release scripts should agree on agent key set for Kiro."""
|
||||||
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
|
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
|
||||||
@@ -55,7 +62,14 @@ class TestAgentConfigConsistency:
|
|||||||
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
|
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert re.search(r"'shai'\s*\{.*?\.shai/commands", ps_text, re.S) is not None
|
assert re.search(r"'shai'\s*\{.*?\.shai/commands", ps_text, re.S) is not None
|
||||||
assert re.search(r"'agy'\s*\{.*?\.agent/workflows", ps_text, re.S) is not None
|
assert re.search(r"'agy'\s*\{.*?\.agent/commands", ps_text, re.S) is not None
|
||||||
|
|
||||||
|
def test_release_sh_switch_has_shai_and_agy_generation(self):
|
||||||
|
"""Bash release builder must generate files for shai and agy agents."""
|
||||||
|
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert re.search(r"shai\)\s*\n.*?\.shai/commands", sh_text, re.S) is not None
|
||||||
|
assert re.search(r"agy\)\s*\n.*?\.agent/commands", sh_text, re.S) is not None
|
||||||
|
|
||||||
def test_init_ai_help_includes_roo_and_kiro_alias(self):
|
def test_init_ai_help_includes_roo_and_kiro_alias(self):
|
||||||
"""CLI help text for --ai should stay in sync with agent config and alias guidance."""
|
"""CLI help text for --ai should stay in sync with agent config and alias guidance."""
|
||||||
@@ -164,3 +178,58 @@ class TestAgentConfigConsistency:
|
|||||||
def test_ai_help_includes_tabnine(self):
|
def test_ai_help_includes_tabnine(self):
|
||||||
"""CLI help text for --ai should include tabnine."""
|
"""CLI help text for --ai should include tabnine."""
|
||||||
assert "tabnine" in AI_ASSISTANT_HELP
|
assert "tabnine" in AI_ASSISTANT_HELP
|
||||||
|
|
||||||
|
# --- Kimi Code CLI consistency checks ---
|
||||||
|
|
||||||
|
def test_kimi_in_agent_config(self):
|
||||||
|
"""AGENT_CONFIG should include kimi with correct folder and commands_subdir."""
|
||||||
|
assert "kimi" in AGENT_CONFIG
|
||||||
|
assert AGENT_CONFIG["kimi"]["folder"] == ".kimi/"
|
||||||
|
assert AGENT_CONFIG["kimi"]["commands_subdir"] == "skills"
|
||||||
|
assert AGENT_CONFIG["kimi"]["requires_cli"] is True
|
||||||
|
|
||||||
|
def test_kimi_in_extension_registrar(self):
|
||||||
|
"""Extension command registrar should include kimi using .kimi/skills and SKILL.md."""
|
||||||
|
cfg = CommandRegistrar.AGENT_CONFIGS
|
||||||
|
|
||||||
|
assert "kimi" in cfg
|
||||||
|
kimi_cfg = cfg["kimi"]
|
||||||
|
assert kimi_cfg["dir"] == ".kimi/skills"
|
||||||
|
assert kimi_cfg["extension"] == "/SKILL.md"
|
||||||
|
|
||||||
|
def test_kimi_in_release_agent_lists(self):
|
||||||
|
"""Bash and PowerShell release scripts should include kimi in agent lists."""
|
||||||
|
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
|
||||||
|
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
sh_match = re.search(r"ALL_AGENTS=\(([^)]*)\)", sh_text)
|
||||||
|
assert sh_match is not None
|
||||||
|
sh_agents = sh_match.group(1).split()
|
||||||
|
|
||||||
|
ps_match = re.search(r"\$AllAgents = @\(([^)]*)\)", ps_text)
|
||||||
|
assert ps_match is not None
|
||||||
|
ps_agents = re.findall(r"'([^']+)'", ps_match.group(1))
|
||||||
|
|
||||||
|
assert "kimi" in sh_agents
|
||||||
|
assert "kimi" in ps_agents
|
||||||
|
|
||||||
|
def test_kimi_in_powershell_validate_set(self):
|
||||||
|
"""PowerShell update-agent-context script should include 'kimi' in ValidateSet."""
|
||||||
|
ps_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
|
||||||
|
assert validate_set_match is not None
|
||||||
|
validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1))
|
||||||
|
|
||||||
|
assert "kimi" in validate_set_values
|
||||||
|
|
||||||
|
def test_kimi_in_github_release_output(self):
|
||||||
|
"""GitHub release script should include kimi template packages."""
|
||||||
|
gh_release_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-github-release.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "spec-kit-template-kimi-sh-" in gh_release_text
|
||||||
|
assert "spec-kit-template-kimi-ps-" in gh_release_text
|
||||||
|
|
||||||
|
def test_ai_help_includes_kimi(self):
|
||||||
|
"""CLI help text for --ai should include kimi."""
|
||||||
|
assert "kimi" in AI_ASSISTANT_HELP
|
||||||
|
|||||||
@@ -132,6 +132,16 @@ def commands_dir_gemini(project_dir):
|
|||||||
return cmd_dir
|
return cmd_dir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def commands_dir_qwen(project_dir):
|
||||||
|
"""Create a populated .qwen/commands directory (Markdown format)."""
|
||||||
|
cmd_dir = project_dir / ".qwen" / "commands"
|
||||||
|
cmd_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for name in ["speckit.specify.md", "speckit.plan.md", "speckit.tasks.md"]:
|
||||||
|
(cmd_dir / name).write_text(f"# {name}\nContent here\n")
|
||||||
|
return cmd_dir
|
||||||
|
|
||||||
|
|
||||||
# ===== _get_skills_dir Tests =====
|
# ===== _get_skills_dir Tests =====
|
||||||
|
|
||||||
class TestGetSkillsDir:
|
class TestGetSkillsDir:
|
||||||
@@ -390,6 +400,28 @@ class TestInstallAiSkills:
|
|||||||
# .toml commands should be untouched
|
# .toml commands should be untouched
|
||||||
assert (cmds_dir / "speckit.specify.toml").exists()
|
assert (cmds_dir / "speckit.specify.toml").exists()
|
||||||
|
|
||||||
|
def test_qwen_md_commands_dir_installs_skills(self, project_dir):
|
||||||
|
"""Qwen now uses Markdown format; skills should install directly from .qwen/commands/."""
|
||||||
|
cmds_dir = project_dir / ".qwen" / "commands"
|
||||||
|
cmds_dir.mkdir(parents=True)
|
||||||
|
(cmds_dir / "speckit.specify.md").write_text(
|
||||||
|
"---\ndescription: Create or update the feature specification.\n---\n\n# Specify\n\nBody.\n"
|
||||||
|
)
|
||||||
|
(cmds_dir / "speckit.plan.md").write_text(
|
||||||
|
"---\ndescription: Generate implementation plan.\n---\n\n# Plan\n\nBody.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = install_ai_skills(project_dir, "qwen")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
skills_dir = project_dir / ".qwen" / "skills"
|
||||||
|
assert skills_dir.exists()
|
||||||
|
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
||||||
|
assert len(skill_dirs) >= 1
|
||||||
|
# .md commands should be untouched
|
||||||
|
assert (cmds_dir / "speckit.specify.md").exists()
|
||||||
|
assert (cmds_dir / "speckit.plan.md").exists()
|
||||||
|
|
||||||
@pytest.mark.parametrize("agent_key", [k for k in AGENT_CONFIG.keys() if k != "generic"])
|
@pytest.mark.parametrize("agent_key", [k for k in AGENT_CONFIG.keys() if k != "generic"])
|
||||||
def test_skills_install_for_all_agents(self, temp_dir, agent_key):
|
def test_skills_install_for_all_agents(self, temp_dir, agent_key):
|
||||||
"""install_ai_skills should produce skills for every configured agent."""
|
"""install_ai_skills should produce skills for every configured agent."""
|
||||||
@@ -410,8 +442,11 @@ class TestInstallAiSkills:
|
|||||||
skills_dir = _get_skills_dir(proj, agent_key)
|
skills_dir = _get_skills_dir(proj, agent_key)
|
||||||
assert skills_dir.exists()
|
assert skills_dir.exists()
|
||||||
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
||||||
assert "speckit-specify" in skill_dirs
|
# Kimi uses dot-separator (speckit.specify) to match /skill:speckit.* invocation;
|
||||||
assert (skills_dir / "speckit-specify" / "SKILL.md").exists()
|
# all other agents use hyphen-separator (speckit-specify).
|
||||||
|
expected_skill_name = "speckit.specify" if agent_key == "kimi" else "speckit-specify"
|
||||||
|
assert expected_skill_name in skill_dirs
|
||||||
|
assert (skills_dir / expected_skill_name / "SKILL.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -443,6 +478,15 @@ class TestCommandCoexistence:
|
|||||||
remaining = list(commands_dir_gemini.glob("speckit.*"))
|
remaining = list(commands_dir_gemini.glob("speckit.*"))
|
||||||
assert len(remaining) == 3
|
assert len(remaining) == 3
|
||||||
|
|
||||||
|
def test_existing_commands_preserved_qwen(self, project_dir, templates_dir, commands_dir_qwen):
|
||||||
|
"""install_ai_skills must NOT remove pre-existing .qwen/commands files."""
|
||||||
|
assert len(list(commands_dir_qwen.glob("speckit.*"))) == 3
|
||||||
|
|
||||||
|
install_ai_skills(project_dir, "qwen")
|
||||||
|
|
||||||
|
remaining = list(commands_dir_qwen.glob("speckit.*"))
|
||||||
|
assert len(remaining) == 3
|
||||||
|
|
||||||
def test_commands_dir_not_removed(self, project_dir, templates_dir, commands_dir_claude):
|
def test_commands_dir_not_removed(self, project_dir, templates_dir, commands_dir_claude):
|
||||||
"""install_ai_skills must not remove the commands directory."""
|
"""install_ai_skills must not remove the commands directory."""
|
||||||
install_ai_skills(project_dir, "claude")
|
install_ai_skills(project_dir, "claude")
|
||||||
@@ -658,6 +702,59 @@ class TestCliValidation:
|
|||||||
assert "Usage:" in result.output
|
assert "Usage:" in result.output
|
||||||
assert "--ai" in result.output
|
assert "--ai" in result.output
|
||||||
|
|
||||||
|
def test_agy_without_ai_skills_fails(self):
|
||||||
|
"""--ai agy without --ai-skills should fail with exit code 1."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(app, ["init", "test-proj", "--ai", "agy"])
|
||||||
|
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "Explicit command support was deprecated in Antigravity version 1.20.5." in result.output
|
||||||
|
assert "--ai-skills" in result.output
|
||||||
|
|
||||||
|
def test_interactive_agy_without_ai_skills_prompts_skills(self, monkeypatch):
|
||||||
|
"""Interactive selector returning agy without --ai-skills should automatically enable --ai-skills."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
# Mock select_with_arrows to simulate the user picking 'agy' for AI,
|
||||||
|
# and return a deterministic default for any other prompts to avoid
|
||||||
|
# calling the real interactive implementation.
|
||||||
|
def _fake_select_with_arrows(*args, **kwargs):
|
||||||
|
options = kwargs.get("options")
|
||||||
|
if options is None and len(args) >= 1:
|
||||||
|
options = args[0]
|
||||||
|
|
||||||
|
# If the options include 'agy', simulate selecting it.
|
||||||
|
if isinstance(options, dict) and "agy" in options:
|
||||||
|
return "agy"
|
||||||
|
if isinstance(options, (list, tuple)) and "agy" in options:
|
||||||
|
return "agy"
|
||||||
|
|
||||||
|
# For any other prompt, return a deterministic, non-interactive default:
|
||||||
|
# pick the first option if available.
|
||||||
|
if isinstance(options, dict) and options:
|
||||||
|
return next(iter(options.keys()))
|
||||||
|
if isinstance(options, (list, tuple)) and options:
|
||||||
|
return options[0]
|
||||||
|
|
||||||
|
# If no options are provided, fall back to None (should not occur in normal use).
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("specify_cli.select_with_arrows", _fake_select_with_arrows)
|
||||||
|
|
||||||
|
# Mock download_and_extract_template to prevent real HTTP downloads during testing
|
||||||
|
monkeypatch.setattr("specify_cli.download_and_extract_template", lambda *args, **kwargs: None)
|
||||||
|
# We need to bypass the `git init` step, wait, it has `--no-git` by default in tests maybe?
|
||||||
|
runner = CliRunner()
|
||||||
|
# Create temp dir to avoid directory already exists errors or whatever
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(app, ["init", "test-proj", "--no-git"])
|
||||||
|
|
||||||
|
# Interactive selection should NOT raise the deprecation error!
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Explicit command support was deprecated" not in result.output
|
||||||
|
|
||||||
def test_ai_skills_flag_appears_in_help(self):
|
def test_ai_skills_flag_appears_in_help(self):
|
||||||
"""--ai-skills should appear in init --help output."""
|
"""--ai-skills should appear in init --help output."""
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user