mirror of
https://github.com/github/spec-kit.git
synced 2026-03-17 19:03:08 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4fe2d35a9 |
@@ -8,15 +8,15 @@ run_command() {
|
|||||||
local command_to_run="$*"
|
local command_to_run="$*"
|
||||||
local output
|
local output
|
||||||
local exit_code
|
local exit_code
|
||||||
|
|
||||||
# Capture all output (stdout and stderr)
|
# Capture all output (stdout and stderr)
|
||||||
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
|
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
|
||||||
exit_code=${exit_code:-0}
|
exit_code=${exit_code:-0}
|
||||||
|
|
||||||
if [ $exit_code -ne 0 ]; then
|
if [ $exit_code -ne 0 ]; then
|
||||||
echo -e "\033[0;31m[ERROR] Command failed (Exit Code $exit_code): $command_to_run\033[0m" >&2
|
echo -e "\033[0;31m[ERROR] Command failed (Exit Code $exit_code): $command_to_run\033[0m" >&2
|
||||||
echo -e "\033[0;31m$output\033[0m" >&2
|
echo -e "\033[0;31m$output\033[0m" >&2
|
||||||
|
|
||||||
exit $exit_code
|
exit $exit_code
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -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://kiro.dev/install.sh"
|
KIRO_INSTALLER_URL="https://cli.kiro.dev/install"
|
||||||
KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
|
KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
|
||||||
KIRO_INSTALLER_PATH="$(mktemp)"
|
KIRO_INSTALLER_PATH="$(mktemp)"
|
||||||
|
|
||||||
@@ -80,11 +80,6 @@ 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"
|
||||||
|
|||||||
8
.github/workflows/scripts/create-github-release.sh
vendored
Executable file → Normal file
8
.github/workflows/scripts/create-github-release.sh
vendored
Executable file → Normal file
@@ -46,20 +46,12 @@ gh release create "$VERSION" \
|
|||||||
.genreleases/spec-kit-template-amp-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-amp-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-shai-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-shai-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-shai-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-shai-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-tabnine-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-tabnine-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kiro-cli-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-kiro-cli-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-kiro-cli-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-kiro-cli-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-agy-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-agy-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-agy-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-agy-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-bob-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-bob-sh-"$VERSION".zip \
|
||||||
.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-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kimi-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kimi-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-trae-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-trae-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" \
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
create-release-packages.ps1 (workflow-local)
|
create-release-packages.ps1 (workflow-local)
|
||||||
Build Spec Kit template release archives for each supported AI assistant and script type.
|
Build Spec Kit template release archives for each supported AI assistant and script type.
|
||||||
|
|
||||||
.PARAMETER Version
|
.PARAMETER Version
|
||||||
Version string with leading 'v' (e.g., v0.2.0)
|
Version string with leading 'v' (e.g., v0.2.0)
|
||||||
|
|
||||||
.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, kimi, trae, generic
|
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, agy, 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)
|
||||||
@@ -33,10 +33,10 @@
|
|||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true, Position=0)]
|
[Parameter(Mandatory=$true, Position=0)]
|
||||||
[string]$Version,
|
[string]$Version,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$Agents = "",
|
[string]$Agents = "",
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$Scripts = ""
|
[string]$Scripts = ""
|
||||||
)
|
)
|
||||||
@@ -60,7 +60,7 @@ New-Item -ItemType Directory -Path $GenReleasesDir -Force | Out-Null
|
|||||||
|
|
||||||
function Rewrite-Paths {
|
function Rewrite-Paths {
|
||||||
param([string]$Content)
|
param([string]$Content)
|
||||||
|
|
||||||
$Content = $Content -replace '(/?)\bmemory/', '.specify/memory/'
|
$Content = $Content -replace '(/?)\bmemory/', '.specify/memory/'
|
||||||
$Content = $Content -replace '(/?)\bscripts/', '.specify/scripts/'
|
$Content = $Content -replace '(/?)\bscripts/', '.specify/scripts/'
|
||||||
$Content = $Content -replace '(/?)\btemplates/', '.specify/templates/'
|
$Content = $Content -replace '(/?)\btemplates/', '.specify/templates/'
|
||||||
@@ -75,55 +75,55 @@ function Generate-Commands {
|
|||||||
[string]$OutputDir,
|
[string]$OutputDir,
|
||||||
[string]$ScriptVariant
|
[string]$ScriptVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||||
|
|
||||||
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
|
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
foreach ($template in $templates) {
|
foreach ($template in $templates) {
|
||||||
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
|
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
|
||||||
|
|
||||||
# Read file content and normalize line endings
|
# Read file content and normalize line endings
|
||||||
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
|
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
|
||||||
|
|
||||||
# Extract description from YAML frontmatter
|
# Extract description from YAML frontmatter
|
||||||
$description = ""
|
$description = ""
|
||||||
if ($fileContent -match '(?m)^description:\s*(.+)$') {
|
if ($fileContent -match '(?m)^description:\s*(.+)$') {
|
||||||
$description = $matches[1]
|
$description = $matches[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract script command from YAML frontmatter
|
# Extract script command from YAML frontmatter
|
||||||
$scriptCommand = ""
|
$scriptCommand = ""
|
||||||
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
|
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
|
||||||
$scriptCommand = $matches[1]
|
$scriptCommand = $matches[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([string]::IsNullOrEmpty($scriptCommand)) {
|
if ([string]::IsNullOrEmpty($scriptCommand)) {
|
||||||
Write-Warning "No script command found for $ScriptVariant in $($template.Name)"
|
Write-Warning "No script command found for $ScriptVariant in $($template.Name)"
|
||||||
$scriptCommand = "(Missing script command for $ScriptVariant)"
|
$scriptCommand = "(Missing script command for $ScriptVariant)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract agent_script command from YAML frontmatter if present
|
# Extract agent_script command from YAML frontmatter if present
|
||||||
$agentScriptCommand = ""
|
$agentScriptCommand = ""
|
||||||
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
|
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
|
||||||
$agentScriptCommand = $matches[1].Trim()
|
$agentScriptCommand = $matches[1].Trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace {SCRIPT} placeholder with the script command
|
# Replace {SCRIPT} placeholder with the script command
|
||||||
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
|
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
|
||||||
|
|
||||||
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
||||||
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
|
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
|
||||||
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
|
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove the scripts: and agent_scripts: sections from frontmatter
|
# Remove the scripts: and agent_scripts: sections from frontmatter
|
||||||
$lines = $body -split "`n"
|
$lines = $body -split "`n"
|
||||||
$outputLines = @()
|
$outputLines = @()
|
||||||
$inFrontmatter = $false
|
$inFrontmatter = $false
|
||||||
$skipScripts = $false
|
$skipScripts = $false
|
||||||
$dashCount = 0
|
$dashCount = 0
|
||||||
|
|
||||||
foreach ($line in $lines) {
|
foreach ($line in $lines) {
|
||||||
if ($line -match '^---$') {
|
if ($line -match '^---$') {
|
||||||
$outputLines += $line
|
$outputLines += $line
|
||||||
@@ -135,7 +135,7 @@ function Generate-Commands {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($inFrontmatter) {
|
if ($inFrontmatter) {
|
||||||
if ($line -match '^(scripts|agent_scripts):$') {
|
if ($line -match '^(scripts|agent_scripts):$') {
|
||||||
$skipScripts = $true
|
$skipScripts = $true
|
||||||
@@ -148,20 +148,20 @@ function Generate-Commands {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$outputLines += $line
|
$outputLines += $line
|
||||||
}
|
}
|
||||||
|
|
||||||
$body = $outputLines -join "`n"
|
$body = $outputLines -join "`n"
|
||||||
|
|
||||||
# Apply other substitutions
|
# Apply other substitutions
|
||||||
$body = $body -replace '\{ARGS\}', $ArgFormat
|
$body = $body -replace '\{ARGS\}', $ArgFormat
|
||||||
$body = $body -replace '__AGENT__', $Agent
|
$body = $body -replace '__AGENT__', $Agent
|
||||||
$body = Rewrite-Paths -Content $body
|
$body = Rewrite-Paths -Content $body
|
||||||
|
|
||||||
# Generate output file based on extension
|
# Generate output file based on extension
|
||||||
$outputFile = Join-Path $OutputDir "speckit.$name.$Extension"
|
$outputFile = Join-Path $OutputDir "speckit.$name.$Extension"
|
||||||
|
|
||||||
switch ($Extension) {
|
switch ($Extension) {
|
||||||
'toml' {
|
'toml' {
|
||||||
$body = $body -replace '\\', '\\'
|
$body = $body -replace '\\', '\\'
|
||||||
@@ -183,15 +183,15 @@ function Generate-CopilotPrompts {
|
|||||||
[string]$AgentsDir,
|
[string]$AgentsDir,
|
||||||
[string]$PromptsDir
|
[string]$PromptsDir
|
||||||
)
|
)
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $PromptsDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $PromptsDir -Force | Out-Null
|
||||||
|
|
||||||
$agentFiles = Get-ChildItem -Path "$AgentsDir/speckit.*.agent.md" -File -ErrorAction SilentlyContinue
|
$agentFiles = Get-ChildItem -Path "$AgentsDir/speckit.*.agent.md" -File -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
foreach ($agentFile in $agentFiles) {
|
foreach ($agentFile in $agentFiles) {
|
||||||
$basename = $agentFile.Name -replace '\.agent\.md$', ''
|
$basename = $agentFile.Name -replace '\.agent\.md$', ''
|
||||||
$promptFile = Join-Path $PromptsDir "$basename.prompt.md"
|
$promptFile = Join-Path $PromptsDir "$basename.prompt.md"
|
||||||
|
|
||||||
$content = @"
|
$content = @"
|
||||||
---
|
---
|
||||||
agent: $basename
|
agent: $basename
|
||||||
@@ -201,118 +201,31 @@ 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,
|
||||||
[string]$Script
|
[string]$Script
|
||||||
)
|
)
|
||||||
|
|
||||||
$baseDir = Join-Path $GenReleasesDir "sdd-${Agent}-package-${Script}"
|
$baseDir = Join-Path $GenReleasesDir "sdd-${Agent}-package-${Script}"
|
||||||
Write-Host "Building $Agent ($Script) package..."
|
Write-Host "Building $Agent ($Script) package..."
|
||||||
New-Item -ItemType Directory -Path $baseDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $baseDir -Force | Out-Null
|
||||||
|
|
||||||
# Copy base structure but filter scripts by variant
|
# Copy base structure but filter scripts by variant
|
||||||
$specDir = Join-Path $baseDir ".specify"
|
$specDir = Join-Path $baseDir ".specify"
|
||||||
New-Item -ItemType Directory -Path $specDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $specDir -Force | Out-Null
|
||||||
|
|
||||||
# Copy memory directory
|
# Copy memory directory
|
||||||
if (Test-Path "memory") {
|
if (Test-Path "memory") {
|
||||||
Copy-Item -Path "memory" -Destination $specDir -Recurse -Force
|
Copy-Item -Path "memory" -Destination $specDir -Recurse -Force
|
||||||
Write-Host "Copied memory -> .specify"
|
Write-Host "Copied memory -> .specify"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only copy the relevant script variant directory
|
# Only copy the relevant script variant directory
|
||||||
if (Test-Path "scripts") {
|
if (Test-Path "scripts") {
|
||||||
$scriptsDestDir = Join-Path $specDir "scripts"
|
$scriptsDestDir = Join-Path $specDir "scripts"
|
||||||
New-Item -ItemType Directory -Path $scriptsDestDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $scriptsDestDir -Force | Out-Null
|
||||||
|
|
||||||
switch ($Script) {
|
switch ($Script) {
|
||||||
'sh' {
|
'sh' {
|
||||||
if (Test-Path "scripts/bash") {
|
if (Test-Path "scripts/bash") {
|
||||||
@@ -327,17 +240,18 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Copy templates (excluding commands directory and vscode-settings.json)
|
# Copy templates (excluding commands directory and vscode-settings.json)
|
||||||
if (Test-Path "templates") {
|
if (Test-Path "templates") {
|
||||||
$templatesDestDir = Join-Path $specDir "templates"
|
$templatesDestDir = Join-Path $specDir "templates"
|
||||||
New-Item -ItemType Directory -Path $templatesDestDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $templatesDestDir -Force | Out-Null
|
||||||
|
|
||||||
Get-ChildItem -Path "templates" -Recurse -File | Where-Object {
|
Get-ChildItem -Path "templates" -Recurse -File | Where-Object {
|
||||||
$_.FullName -notmatch 'templates[/\\]commands[/\\]' -and $_.Name -ne 'vscode-settings.json'
|
$_.FullName -notmatch 'templates[/\\]commands[/\\]' -and $_.Name -ne 'vscode-settings.json'
|
||||||
} | ForEach-Object {
|
} | ForEach-Object {
|
||||||
@@ -349,7 +263,7 @@ function Build-Variant {
|
|||||||
}
|
}
|
||||||
Write-Host "Copied templates -> .specify/templates"
|
Write-Host "Copied templates -> .specify/templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate agent-specific command files
|
# Generate agent-specific command files
|
||||||
switch ($Agent) {
|
switch ($Agent) {
|
||||||
'claude' {
|
'claude' {
|
||||||
@@ -366,10 +280,12 @@ function Build-Variant {
|
|||||||
'copilot' {
|
'copilot' {
|
||||||
$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") {
|
||||||
@@ -382,7 +298,7 @@ function Build-Variant {
|
|||||||
}
|
}
|
||||||
'qwen' {
|
'qwen' {
|
||||||
$cmdDir = Join-Path $baseDir ".qwen/commands"
|
$cmdDir = Join-Path $baseDir ".qwen/commands"
|
||||||
Generate-Commands -Agent 'qwen' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'qwen' -Extension 'toml' -ArgFormat '{{args}}' -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")
|
||||||
}
|
}
|
||||||
@@ -435,30 +351,10 @@ function Build-Variant {
|
|||||||
$cmdDir = Join-Path $baseDir ".shai/commands"
|
$cmdDir = Join-Path $baseDir ".shai/commands"
|
||||||
Generate-Commands -Agent 'shai' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'shai' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
}
|
}
|
||||||
'tabnine' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".tabnine/agent/commands"
|
|
||||||
Generate-Commands -Agent 'tabnine' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
$tabnineTemplate = Join-Path 'agent_templates' 'tabnine/TABNINE.md'
|
|
||||||
if (Test-Path $tabnineTemplate) { Copy-Item $tabnineTemplate (Join-Path $baseDir 'TABNINE.md') }
|
|
||||||
}
|
|
||||||
'agy' {
|
'agy' {
|
||||||
$cmdDir = Join-Path $baseDir ".agent/commands"
|
$cmdDir = Join-Path $baseDir ".agent/workflows"
|
||||||
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
}
|
}
|
||||||
'vibe' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".vibe/prompts"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
'trae' {
|
|
||||||
$rulesDir = Join-Path $baseDir ".trae/rules"
|
|
||||||
New-Item -ItemType Directory -Force -Path $rulesDir | Out-Null
|
|
||||||
Generate-Commands -Agent 'trae' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $rulesDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'generic' {
|
'generic' {
|
||||||
$cmdDir = Join-Path $baseDir ".speckit/commands"
|
$cmdDir = Join-Path $baseDir ".speckit/commands"
|
||||||
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
@@ -467,7 +363,7 @@ function Build-Variant {
|
|||||||
throw "Unsupported agent '$Agent'."
|
throw "Unsupported agent '$Agent'."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create zip archive
|
# Create zip archive
|
||||||
$zipFile = Join-Path $GenReleasesDir "spec-kit-template-${Agent}-${Script}-${Version}.zip"
|
$zipFile = Join-Path $GenReleasesDir "spec-kit-template-${Agent}-${Script}-${Version}.zip"
|
||||||
Compress-Archive -Path "$baseDir/*" -DestinationPath $zipFile -Force
|
Compress-Archive -Path "$baseDir/*" -DestinationPath $zipFile -Force
|
||||||
@@ -475,16 +371,17 @@ 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', 'kimi', 'trae', 'generic')
|
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'agy', 'generic')
|
||||||
$AllScripts = @('sh', 'ps')
|
$AllScripts = @('sh', 'ps')
|
||||||
|
|
||||||
function Normalize-List {
|
function Normalize-List {
|
||||||
param([string]$Input)
|
param([string]$Input)
|
||||||
|
|
||||||
if ([string]::IsNullOrEmpty($Input)) {
|
if ([string]::IsNullOrEmpty($Input)) {
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -495,7 +392,7 @@ function Validate-Subset {
|
|||||||
[string[]]$Allowed,
|
[string[]]$Allowed,
|
||||||
[string[]]$Items
|
[string[]]$Items
|
||||||
)
|
)
|
||||||
|
|
||||||
$ok = $true
|
$ok = $true
|
||||||
foreach ($item in $Items) {
|
foreach ($item in $Items) {
|
||||||
if ($item -notin $Allowed) {
|
if ($item -notin $Allowed) {
|
||||||
|
|||||||
136
.github/workflows/scripts/create-release-packages.sh
vendored
136
.github/workflows/scripts/create-release-packages.sh
vendored
@@ -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 kimi trae 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 kiro-cli agy bob qodercli 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
|
||||||
@@ -45,19 +45,19 @@ generate_commands() {
|
|||||||
[[ -f "$template" ]] || continue
|
[[ -f "$template" ]] || continue
|
||||||
local name description script_command agent_script_command body
|
local name description script_command agent_script_command body
|
||||||
name=$(basename "$template" .md)
|
name=$(basename "$template" .md)
|
||||||
|
|
||||||
# Normalize line endings
|
# Normalize line endings
|
||||||
file_content=$(tr -d '\r' < "$template")
|
file_content=$(tr -d '\r' < "$template")
|
||||||
|
|
||||||
# Extract description and script command from YAML frontmatter
|
# Extract description and script command from YAML frontmatter
|
||||||
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
||||||
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
|
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
|
||||||
|
|
||||||
if [[ -z $script_command ]]; then
|
if [[ -z $script_command ]]; then
|
||||||
echo "Warning: no script command found for $script_variant in $template" >&2
|
echo "Warning: no script command found for $script_variant in $template" >&2
|
||||||
script_command="(Missing script command for $script_variant)"
|
script_command="(Missing script command for $script_variant)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract agent_script command from YAML frontmatter if present
|
# Extract agent_script command from YAML frontmatter if present
|
||||||
agent_script_command=$(printf '%s\n' "$file_content" | awk '
|
agent_script_command=$(printf '%s\n' "$file_content" | awk '
|
||||||
/^agent_scripts:$/ { in_agent_scripts=1; next }
|
/^agent_scripts:$/ { in_agent_scripts=1; next }
|
||||||
@@ -68,15 +68,15 @@ generate_commands() {
|
|||||||
}
|
}
|
||||||
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
|
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
|
||||||
')
|
')
|
||||||
|
|
||||||
# Replace {SCRIPT} placeholder with the script command
|
# Replace {SCRIPT} placeholder with the script command
|
||||||
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
||||||
|
|
||||||
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
||||||
if [[ -n $agent_script_command ]]; then
|
if [[ -n $agent_script_command ]]; then
|
||||||
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
|
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
|
# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
|
||||||
body=$(printf '%s\n' "$body" | awk '
|
body=$(printf '%s\n' "$body" | awk '
|
||||||
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
||||||
@@ -86,10 +86,10 @@ generate_commands() {
|
|||||||
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
|
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
|
||||||
{ print }
|
{ print }
|
||||||
')
|
')
|
||||||
|
|
||||||
# Apply other substitutions
|
# Apply other substitutions
|
||||||
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
|
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
|
||||||
|
|
||||||
case $ext in
|
case $ext in
|
||||||
toml)
|
toml)
|
||||||
body=$(printf '%s\n' "$body" | sed 's/\\/\\\\/g')
|
body=$(printf '%s\n' "$body" | sed 's/\\/\\\\/g')
|
||||||
@@ -105,14 +105,15 @@ generate_commands() {
|
|||||||
generate_copilot_prompts() {
|
generate_copilot_prompts() {
|
||||||
local agents_dir=$1 prompts_dir=$2
|
local agents_dir=$1 prompts_dir=$2
|
||||||
mkdir -p "$prompts_dir"
|
mkdir -p "$prompts_dir"
|
||||||
|
|
||||||
# Generate a .prompt.md file for each .agent.md file
|
# Generate a .prompt.md file for each .agent.md file
|
||||||
for agent_file in "$agents_dir"/speckit.*.agent.md; do
|
for agent_file in "$agents_dir"/speckit.*.agent.md; do
|
||||||
[[ -f "$agent_file" ]] || continue
|
[[ -f "$agent_file" ]] || continue
|
||||||
|
|
||||||
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}
|
||||||
@@ -121,104 +122,41 @@ 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}"
|
||||||
echo "Building $agent ($script) package..."
|
echo "Building $agent ($script) package..."
|
||||||
mkdir -p "$base_dir"
|
mkdir -p "$base_dir"
|
||||||
|
|
||||||
# Copy base structure but filter scripts by variant
|
# Copy base structure but filter scripts by variant
|
||||||
SPEC_DIR="$base_dir/.specify"
|
SPEC_DIR="$base_dir/.specify"
|
||||||
mkdir -p "$SPEC_DIR"
|
mkdir -p "$SPEC_DIR"
|
||||||
|
|
||||||
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
|
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
|
||||||
|
|
||||||
# Only copy the relevant script variant directory
|
# Only copy the relevant script variant directory
|
||||||
if [[ -d scripts ]]; then
|
if [[ -d scripts ]]; then
|
||||||
mkdir -p "$SPEC_DIR/scripts"
|
mkdir -p "$SPEC_DIR/scripts"
|
||||||
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -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): {{args}}
|
||||||
|
# This keeps formats readable without extra abstraction.
|
||||||
|
|
||||||
case $agent in
|
case $agent in
|
||||||
claude)
|
claude)
|
||||||
@@ -231,7 +169,9 @@ 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"
|
||||||
;;
|
;;
|
||||||
@@ -240,7 +180,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 md "\$ARGUMENTS" "$base_dir/.qwen/commands" "$script"
|
generate_commands qwen toml "{{args}}" "$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"
|
||||||
@@ -272,28 +212,15 @@ build_variant() {
|
|||||||
shai)
|
shai)
|
||||||
mkdir -p "$base_dir/.shai/commands"
|
mkdir -p "$base_dir/.shai/commands"
|
||||||
generate_commands shai md "\$ARGUMENTS" "$base_dir/.shai/commands" "$script" ;;
|
generate_commands shai md "\$ARGUMENTS" "$base_dir/.shai/commands" "$script" ;;
|
||||||
tabnine)
|
|
||||||
mkdir -p "$base_dir/.tabnine/agent/commands"
|
|
||||||
generate_commands tabnine toml "{{args}}" "$base_dir/.tabnine/agent/commands" "$script"
|
|
||||||
[[ -f agent_templates/tabnine/TABNINE.md ]] && cp agent_templates/tabnine/TABNINE.md "$base_dir/TABNINE.md" ;;
|
|
||||||
kiro-cli)
|
kiro-cli)
|
||||||
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/commands"
|
mkdir -p "$base_dir/.agent/workflows"
|
||||||
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/commands" "$script" ;;
|
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/workflows" "$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)
|
|
||||||
mkdir -p "$base_dir/.vibe/prompts"
|
|
||||||
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" ;;
|
|
||||||
trae)
|
|
||||||
mkdir -p "$base_dir/.trae/rules"
|
|
||||||
generate_commands trae md "\$ARGUMENTS" "$base_dir/.trae/rules" "$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" ;;
|
||||||
@@ -303,10 +230,11 @@ 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 kimi trae generic)
|
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai kiro-cli agy bob qodercli 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")}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
AGENTS.md
17
AGENTS.md
@@ -10,6 +10,10 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## General practices
|
||||||
|
|
||||||
|
- Any changes to `__init__.py` for the Specify CLI require a version rev in `pyproject.toml` and addition of entries to `CHANGELOG.md`.
|
||||||
|
|
||||||
## Adding New Agent Support
|
## Adding New Agent Support
|
||||||
|
|
||||||
This section explains how to add support for new AI agents/assistants to the Specify CLI. Use this guide as a reference when integrating new AI tools into the Spec-Driven Development workflow.
|
This section explains how to add support for new AI agents/assistants to the Specify CLI. Use this guide as a reference when integrating new AI tools into the Spec-Driven Development workflow.
|
||||||
@@ -31,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/` | Markdown | `qwen` | Alibaba's Qwen Code CLI |
|
| **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI |
|
||||||
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
|
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
|
||||||
| **Codex CLI** | `.codex/commands/` | Markdown | `codex` | Codex CLI |
|
| **Codex CLI** | `.codex/commands/` | Markdown | `codex` | Codex CLI |
|
||||||
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
|
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
|
||||||
@@ -43,10 +47,7 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
| **Kiro CLI** | `.kiro/prompts/` | Markdown | `kiro-cli` | Kiro CLI |
|
| **Kiro CLI** | `.kiro/prompts/` | Markdown | `kiro-cli` | Kiro CLI |
|
||||||
| **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 |
|
|
||||||
| **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 |
|
||||||
| **Trae** | `.trae/rules/` | Markdown | N/A (IDE-based) | Trae 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 |
|
||||||
|
|
||||||
### Step-by-Step Integration Guide
|
### Step-by-Step Integration Guide
|
||||||
@@ -85,7 +86,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), `"prompts"` (codex, kiro-cli), `"command"` (opencode - singular)
|
- Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode, agy), `"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
|
||||||
@@ -321,8 +322,6 @@ Require a command-line tool to be installed:
|
|||||||
- **Qoder CLI**: `qodercli` CLI
|
- **Qoder CLI**: `qodercli` CLI
|
||||||
- **Amp**: `amp` CLI
|
- **Amp**: `amp` CLI
|
||||||
- **SHAI**: `shai` CLI
|
- **SHAI**: `shai` CLI
|
||||||
- **Tabnine CLI**: `tabnine` CLI
|
|
||||||
- **Kimi Code**: `kimi` CLI
|
|
||||||
|
|
||||||
### IDE-Based Agents
|
### IDE-Based Agents
|
||||||
|
|
||||||
@@ -336,7 +335,7 @@ Work within integrated development environments:
|
|||||||
|
|
||||||
### Markdown Format
|
### Markdown Format
|
||||||
|
|
||||||
Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code, Qwen
|
Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob
|
||||||
|
|
||||||
**Standard format:**
|
**Standard format:**
|
||||||
|
|
||||||
@@ -361,7 +360,7 @@ Command content with {SCRIPT} and $ARGUMENTS placeholders.
|
|||||||
|
|
||||||
### TOML Format
|
### TOML Format
|
||||||
|
|
||||||
Used by: Gemini, Tabnine
|
Used by: Gemini, Qwen
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
description = "Command description"
|
description = "Command description"
|
||||||
|
|||||||
167
CHANGELOG.md
167
CHANGELOG.md
@@ -7,173 +7,6 @@ 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.3.1] - 2026-03-17
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- docs: add greenfield Spring Boot pirate-speak preset demo to README (#1878)
|
|
||||||
- fix(ai-skills): exclude non-speckit copilot agent markdown from skills (#1867)
|
|
||||||
- feat: add Trae IDE support as a new agent (#1817)
|
|
||||||
- feat(cli): polite deep merge for settings.json and support JSONC (#1874)
|
|
||||||
- feat(extensions,presets): add priority-based resolution ordering (#1855)
|
|
||||||
- fix(scripts): suppress stdout from git fetch in create-new-feature.sh (#1876)
|
|
||||||
- fix(scripts): harden bash scripts — escape, compat, and error handling (#1869)
|
|
||||||
- Add cognitive-squad to community extension catalog (#1870)
|
|
||||||
- docs: add Go / React brownfield walkthrough to community walkthroughs (#1868)
|
|
||||||
- chore: update DocGuard extension to v0.9.8 (#1859)
|
|
||||||
- Feature: add specify status command (#1837)
|
|
||||||
- fix(extensions): show extension ID in list output (#1843)
|
|
||||||
- feat(extensions): add Archive and Reconcile extensions to community catalog (#1844)
|
|
||||||
- feat: Add DocGuard CDD enforcement extension to community catalog (#1838)
|
|
||||||
|
|
||||||
|
|
||||||
## [0.3.0] - 2026-03-13
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- No changes have been documented for this release yet.
|
|
||||||
|
|
||||||
<!-- Entries for 0.2.x and earlier releases are documented in their respective sections below. -->
|
|
||||||
- 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(cli): polite deep merge for VSCode settings.json with JSONC support via `json5` and zero-data-loss fallbacks
|
|
||||||
- feat(presets): Pluggable preset system with preset catalog and template resolver
|
|
||||||
- Preset manifest (`preset.yml`) with validation for artifact, command, and script types
|
|
||||||
- `PresetManifest`, `PresetRegistry`, `PresetManager`, `PresetCatalog`, `PresetResolver` classes in `src/specify_cli/presets.py`
|
|
||||||
- CLI commands: `specify preset search`, `specify preset add`, `specify preset list`, `specify preset remove`, `specify preset resolve`, `specify preset info`
|
|
||||||
- CLI commands: `specify preset catalog list`, `specify preset catalog add`, `specify preset catalog remove` for multi-catalog management
|
|
||||||
- `PresetCatalogEntry` dataclass and multi-catalog support mirroring the extension catalog system
|
|
||||||
- `--preset` option for `specify init` to install presets during initialization
|
|
||||||
- Priority-based preset resolution: presets with lower priority number win (`--priority` flag)
|
|
||||||
- `resolve_template()` / `Resolve-Template` helpers in bash and PowerShell common scripts
|
|
||||||
- Template resolution priority stack: overrides → presets → extensions → core
|
|
||||||
- Preset catalog files (`presets/catalog.json`, `presets/catalog.community.json`)
|
|
||||||
- Preset scaffold directory (`presets/scaffold/`)
|
|
||||||
- Scripts updated to use template resolution instead of hardcoded paths
|
|
||||||
- feat(presets): Preset command overrides now propagate to agent skills when `--ai-skills` was used during init
|
|
||||||
- feat: `specify init` persists CLI options to `.specify/init-options.json` for downstream operations
|
|
||||||
- feat(extensions): support `.extensionignore` to exclude files/folders during `specify extension add` (#1781)
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
|
|
||||||
## [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
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -22,7 +22,6 @@
|
|||||||
- [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development)
|
- [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development)
|
||||||
- [⚡ Get Started](#-get-started)
|
- [⚡ Get Started](#-get-started)
|
||||||
- [📽️ Video Overview](#️-video-overview)
|
- [📽️ Video Overview](#️-video-overview)
|
||||||
- [🚶 Community Walkthroughs](#-community-walkthroughs)
|
|
||||||
- [🤖 Supported AI Agents](#-supported-ai-agents)
|
- [🤖 Supported AI Agents](#-supported-ai-agents)
|
||||||
- [🔧 Specify CLI Reference](#-specify-cli-reference)
|
- [🔧 Specify CLI Reference](#-specify-cli-reference)
|
||||||
- [📚 Core Philosophy](#-core-philosophy)
|
- [📚 Core Philosophy](#-core-philosophy)
|
||||||
@@ -80,13 +79,7 @@ uv tool install specify-cli --force --from git+https://github.com/github/spec-ki
|
|||||||
Run directly without installing:
|
Run directly without installing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create new project
|
|
||||||
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
|
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
|
||||||
|
|
||||||
# Or initialize in existing project
|
|
||||||
uvx --from git+https://github.com/github/spec-kit.git specify init . --ai claude
|
|
||||||
# or
|
|
||||||
uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai claude
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Benefits of persistent installation:**
|
**Benefits of persistent installation:**
|
||||||
@@ -146,22 +139,6 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
|
|||||||
|
|
||||||
[](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
|
[](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
|
||||||
|
|
||||||
## 🚶 Community Walkthroughs
|
|
||||||
|
|
||||||
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
|
|
||||||
|
|
||||||
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
|
|
||||||
|
|
||||||
- **[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, ~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.
|
|
||||||
|
|
||||||
- **[Brownfield Go / React dashboard demo](https://github.com/mnriem/spec-kit-go-brownfield-demo)** — Demonstrates spec-kit driven entirely from the **terminal using GitHub Copilot CLI**. Extends NASA's open-source Hermes ground support system (Go) with a lightweight React-based web telemetry dashboard, showing that the full constitution → specify → plan → tasks → implement workflow works from the terminal.
|
|
||||||
|
|
||||||
- **[Greenfield Spring Boot MVC with a custom preset](https://github.com/mnriem/spec-kit-pirate-speak-preset-demo)** — Builds a Spring Boot MVC application from scratch using a custom pirate-speak preset, demonstrating how presets can reshape the entire spec-kit experience: specifications become "Voyage Manifests," plans become "Battle Plans," and tasks become "Crew Assignments" — all generated in full pirate vernacular without changing any tooling.
|
|
||||||
|
|
||||||
## 🤖 Supported AI Agents
|
## 🤖 Supported AI Agents
|
||||||
|
|
||||||
| Agent | Support | Notes |
|
| Agent | Support | Notes |
|
||||||
@@ -183,12 +160,8 @@ See Spec-Driven Development in action across different scenarios with these comm
|
|||||||
| [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ | |
|
| [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ | |
|
||||||
| [Roo Code](https://roocode.com/) | ✅ | |
|
| [Roo Code](https://roocode.com/) | ✅ | |
|
||||||
| [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) | ✅ | |
|
|
||||||
| [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/) | ✅ | Requires `--ai-skills` |
|
| [Antigravity (agy)](https://agy.ai/) | ✅ | |
|
||||||
| [Trae](https://www.trae.ai/) | ✅ | |
|
|
||||||
| 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
|
||||||
@@ -200,14 +173,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`, `kimi`) |
|
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`) |
|
||||||
|
|
||||||
### `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`, `kimi`, 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`, 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 |
|
||||||
@@ -246,15 +219,9 @@ specify init my-project --ai amp
|
|||||||
# Initialize with SHAI support
|
# Initialize with SHAI support
|
||||||
specify init my-project --ai shai
|
specify init my-project --ai shai
|
||||||
|
|
||||||
# Initialize with Mistral Vibe support
|
|
||||||
specify init my-project --ai vibe
|
|
||||||
|
|
||||||
# Initialize with IBM Bob support
|
# Initialize with IBM Bob support
|
||||||
specify init my-project --ai bob
|
specify init my-project --ai bob
|
||||||
|
|
||||||
# Initialize with Antigravity support
|
|
||||||
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/
|
||||||
|
|
||||||
@@ -429,7 +396,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, 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:
|
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder 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:
|
||||||
|
|
||||||
```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](https://github.com/github/spec-kit/blob/main/spec-driven.md) for in-depth guidance
|
- Read the [complete methodology](../spec-driven.md) for in-depth guidance
|
||||||
- Check out [more examples](https://github.com/github/spec-kit/tree/main/templates) in the repository
|
- Check out [more examples](../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,34 +243,6 @@ 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`
|
||||||
@@ -281,67 +253,30 @@ 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
|
||||||
# Get the ordered list of active catalogs
|
# Fetch catalog
|
||||||
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 across all active catalogs
|
# Search extensions
|
||||||
# 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] — each dict includes _catalog_name, _install_allowed
|
) # Returns: List[Dict]
|
||||||
|
|
||||||
# Get extension info (searches all active catalogs)
|
# Get extension info
|
||||||
# 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 (primary catalog)
|
# Check cache validity
|
||||||
is_valid = catalog.is_cache_valid() # bool
|
is_valid = catalog.is_cache_valid() # bool
|
||||||
|
|
||||||
# Clear all catalog caches
|
# Clear cache
|
||||||
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`
|
||||||
@@ -608,39 +543,6 @@ 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]`
|
||||||
@@ -649,13 +551,13 @@ Removes a catalog entry from `.specify/extension-catalogs.yml`.
|
|||||||
|
|
||||||
- `--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]`
|
||||||
@@ -673,8 +575,6 @@ Removes a catalog entry from `.specify/extension-catalogs.yml`.
|
|||||||
|
|
||||||
**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
|
||||||
@@ -689,8 +589,6 @@ Searches all active catalogs simultaneously. Results include source catalog name
|
|||||||
|
|
||||||
**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,67 +332,6 @@ 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
|
||||||
|
|
||||||
`specify extension search` searches **all active catalogs** simultaneously, including the community catalog by default. Results are annotated with their source catalog and install status.
|
**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.
|
||||||
|
|
||||||
### 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 across all active catalogs (default and community by default).
|
Shows all extensions in your organization's catalog.
|
||||||
|
|
||||||
### 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 full catalog stack with a single URL (backward compat) | Built-in default stack |
|
| `SPECKIT_CATALOG_URL` | Override the extension catalog URL | GitHub-hosted catalog |
|
||||||
| `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 (replaces the full stack)
|
# Point to a local or alternative catalog
|
||||||
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,96 +419,13 @@ export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
|
|||||||
|
|
||||||
## Extension Catalogs
|
## Extension Catalogs
|
||||||
|
|
||||||
Spec Kit uses a **catalog stack** — an ordered list of catalogs searched simultaneously. By default, two catalogs are active:
|
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).
|
||||||
|
|
||||||
| 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 catalogs to:
|
Organizations customize their `catalog.json` 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
|
||||||
@@ -586,40 +503,24 @@ Options for hosting your catalog:
|
|||||||
|
|
||||||
#### 3. Configure Your Environment
|
#### 3. Configure Your Environment
|
||||||
|
|
||||||
##### Option A: Catalog stack config file (recommended)
|
##### Option A: Environment variable (recommended for CI/CD)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -72,22 +72,9 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
|
|
||||||
| Extension | Purpose | URL |
|
| Extension | Purpose | URL |
|
||||||
|-----------|---------|-----|
|
|-----------|---------|-----|
|
||||||
| Archive Extension | Archive merged features into main project memory. | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
|
|
||||||
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
|
||||||
| 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) |
|
||||||
| Cognitive Squad | 19-function cognitive agent squad for autonomous pre-code analysis — 7 core agents, 7 specialists, 4 learning functions with feedback loop | [cognitive-squad](https://github.com/Testimonial/cognitive-squad) |
|
|
||||||
| DocGuard — CDD Enforcement | Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero dependencies. | [spec-kit-docguard](https://github.com/raccioly/docguard) |
|
|
||||||
| 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) |
|
|
||||||
| Learning Extension | Generate educational guides from implementations and enhance clarifications with mentoring context | [spec-kit-learn](https://github.com/imviancagrace/spec-kit-learn) |
|
|
||||||
| 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) |
|
|
||||||
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |
|
|
||||||
| Ralph Loop | Autonomous implementation loop using AI agent CLI | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
|
|
||||||
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |
|
|
||||||
| 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) |
|
|
||||||
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
||||||
| Understanding | Automated requirements quality analysis — 31 deterministic metrics against IEEE/ISO standards with experimental energy-based ambiguity detection | [understanding](https://github.com/Testimonial/understanding) |
|
|
||||||
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
||||||
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# RFC: Spec Kit Extension System
|
# RFC: Spec Kit Extension System
|
||||||
|
|
||||||
**Status**: Implemented
|
**Status**: Draft
|
||||||
**Author**: Stats Perform Engineering
|
**Author**: Stats Perform Engineering
|
||||||
**Created**: 2026-01-28
|
**Created**: 2026-01-28
|
||||||
**Updated**: 2026-03-11
|
**Updated**: 2026-01-28
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,9 +24,8 @@
|
|||||||
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. [Resolved Questions](#resolved-questions)
|
16. [Open Questions](#open-questions)
|
||||||
17. [Open Questions (Remaining)](#open-questions-remaining)
|
17. [Appendices](#appendices)
|
||||||
18. [Appendices](#appendices)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -359,15 +358,12 @@ specify extension add jira
|
|||||||
"installed_at": "2026-01-28T14:30:00Z",
|
"installed_at": "2026-01-28T14:30:00Z",
|
||||||
"source": "catalog",
|
"source": "catalog",
|
||||||
"manifest_hash": "sha256:abc123...",
|
"manifest_hash": "sha256:abc123...",
|
||||||
"enabled": true,
|
"enabled": true
|
||||||
"priority": 10
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Priority Field**: Extensions are ordered by `priority` (lower = higher precedence). Default is 10. Used for template resolution when multiple extensions provide the same template.
|
|
||||||
|
|
||||||
### 3. Configuration
|
### 3. Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -872,7 +868,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**: Primary catalog (priority 1, `install_allowed: true`) in the default stack
|
- **Usage**: Default catalog used by `specify extension` CLI commands
|
||||||
- **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`)
|
||||||
@@ -883,16 +879,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**: Secondary catalog (priority 2, `install_allowed: false`) in the default stack — discovery only
|
- **Usage**: Browse to discover extensions, then copy to your `catalog.json`
|
||||||
|
|
||||||
**How It Works (default stack):**
|
**How It Works:**
|
||||||
|
|
||||||
1. **Discover**: `specify extension search` searches both catalogs — community extensions appear automatically
|
1. **Discover**: Browse `catalog.community.json` to find available extensions
|
||||||
2. **Review**: Evaluate community extensions for security, quality, and organizational fit
|
2. **Review**: Evaluate extensions for security, quality, and organizational fit
|
||||||
3. **Curate**: Copy approved entries from community catalog to your `catalog.json`, or add to `.specify/extension-catalogs.yml` with `install_allowed: true`
|
3. **Curate**: Copy approved extension entries from community catalog to your `catalog.json`
|
||||||
4. **Install**: Use `specify extension add <name>` — only allowed from `install_allowed: true` catalogs
|
4. **Install**: Use `specify extension add <name>` (pulls from your curated catalog)
|
||||||
|
|
||||||
This approach gives organizations full control over which extensions can be installed while still providing community discoverability out of the box.
|
This approach gives organizations full control over which extensions are available to their teams while maintaining a shared community resource for discovery.
|
||||||
|
|
||||||
### Catalog Format
|
### Catalog Format
|
||||||
|
|
||||||
@@ -965,92 +961,30 @@ specify extension info jira
|
|||||||
|
|
||||||
### Custom Catalogs
|
### Custom Catalogs
|
||||||
|
|
||||||
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.
|
**⚠️ FUTURE FEATURE - NOT YET IMPLEMENTED**
|
||||||
|
|
||||||
#### Catalog Stack Resolution
|
The following catalog management commands are proposed design concepts but are not yet available in the current implementation:
|
||||||
|
|
||||||
The active catalog stack is resolved in this order (first match wins):
|
|
||||||
|
|
||||||
1. **`SPECKIT_CATALOG_URL` environment variable** — single catalog replacing all defaults (backward compat)
|
|
||||||
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)
|
|
||||||
|
|
||||||
#### Default Built-in Stack
|
|
||||||
|
|
||||||
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)"
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
#### Catalog CLI Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List active catalogs with name, URL, priority, and install_allowed
|
# Add custom catalog (FUTURE - NOT AVAILABLE)
|
||||||
specify extension catalog list
|
specify extension add-catalog https://internal.company.com/spec-kit/catalog.json
|
||||||
|
|
||||||
# Add a catalog (project-scoped)
|
# Set as default (FUTURE - NOT AVAILABLE)
|
||||||
specify extension catalog add --name "internal" --install-allowed \
|
specify extension set-catalog --default https://internal.company.com/spec-kit/catalog.json
|
||||||
https://internal.company.com/spec-kit/catalog.json
|
|
||||||
|
|
||||||
# Add a discovery-only catalog
|
# List catalogs (FUTURE - NOT AVAILABLE)
|
||||||
specify extension catalog add --name "community" \
|
specify extension catalogs
|
||||||
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
|
**Proposed catalog priority** (future design):
|
||||||
|
|
||||||
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.
|
1. Project-specific catalog (`.specify/extension-catalogs.yml`) - *not implemented*
|
||||||
|
2. User-level catalog (`~/.specify/extension-catalogs.yml`) - *not implemented*
|
||||||
|
3. Default GitHub catalog
|
||||||
|
|
||||||
#### `install_allowed: false` Behavior
|
#### Current Implementation: SPECKIT_CATALOG_URL
|
||||||
|
|
||||||
Extensions from discovery-only catalogs are shown in `specify extension search` results but cannot be installed directly:
|
**The currently available method** for using custom catalogs is the `SPECKIT_CATALOG_URL` environment variable:
|
||||||
|
|
||||||
```
|
|
||||||
⚠ '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
|
||||||
@@ -1087,15 +1021,11 @@ List installed extensions in current project.
|
|||||||
$ specify extension list
|
$ specify extension list
|
||||||
|
|
||||||
Installed Extensions:
|
Installed Extensions:
|
||||||
✓ Jira Integration (v1.0.0)
|
✓ jira (v1.0.0) - Jira Integration
|
||||||
jira
|
Commands: 3 | Hooks: 2 | Status: Enabled
|
||||||
Create Jira issues from spec-kit artifacts
|
|
||||||
Commands: 3 | Hooks: 2 | Priority: 10 | Status: Enabled
|
|
||||||
|
|
||||||
✓ Linear Integration (v0.9.0)
|
✓ linear (v0.9.0) - Linear Integration
|
||||||
linear
|
Commands: 1 | Hooks: 1 | Status: Enabled
|
||||||
Create Linear issues from spec-kit artifacts
|
|
||||||
Commands: 1 | Hooks: 1 | Priority: 10 | Status: Enabled
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
@@ -1203,9 +1133,10 @@ Next steps:
|
|||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
|
|
||||||
- `--from URL`: Install from a remote URL (archive). Does not accept Git repositories directly.
|
- `--from URL`: Install from custom URL or Git repo
|
||||||
- `--dev`: Install from a local path in development mode (the PATH is the positional `extension` argument).
|
- `--version VERSION`: Install specific version
|
||||||
- `--priority NUMBER`: Set resolution priority (lower = higher precedence, default 10)
|
- `--dev PATH`: Install from local path (development mode)
|
||||||
|
- `--no-register`: Skip command registration (manual setup)
|
||||||
|
|
||||||
#### `specify extension remove NAME`
|
#### `specify extension remove NAME`
|
||||||
|
|
||||||
@@ -1286,29 +1217,6 @@ $ specify extension disable jira
|
|||||||
To re-enable: specify extension enable jira
|
To re-enable: specify extension enable jira
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `specify extension set-priority NAME PRIORITY`
|
|
||||||
|
|
||||||
Change the resolution priority of an installed extension.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ specify extension set-priority jira 5
|
|
||||||
|
|
||||||
✓ Extension 'Jira Integration' priority changed: 10 → 5
|
|
||||||
|
|
||||||
Lower priority = higher precedence in template resolution
|
|
||||||
```
|
|
||||||
|
|
||||||
**Priority Values:**
|
|
||||||
|
|
||||||
- Lower numbers = higher precedence (checked first in resolution)
|
|
||||||
- Default priority is 10
|
|
||||||
- Must be a positive integer (1 or higher)
|
|
||||||
|
|
||||||
**Use Cases:**
|
|
||||||
|
|
||||||
- Ensure a critical extension's templates take precedence
|
|
||||||
- Override default resolution order when multiple extensions provide similar templates
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Compatibility & Versioning
|
## Compatibility & Versioning
|
||||||
@@ -1534,225 +1442,203 @@ AI agent registers both names, so old scripts work.
|
|||||||
|
|
||||||
## Implementation Phases
|
## Implementation Phases
|
||||||
|
|
||||||
### Phase 1: Core Extension System ✅ COMPLETED
|
### Phase 1: Core Extension System (Week 1-2)
|
||||||
|
|
||||||
**Goal**: Basic extension infrastructure
|
**Goal**: Basic extension infrastructure
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Extension manifest schema (`extension.yml`)
|
- [ ] Extension manifest schema (`extension.yml`)
|
||||||
- [x] Extension directory structure
|
- [ ] Extension directory structure
|
||||||
- [x] CLI commands:
|
- [ ] CLI commands:
|
||||||
- [x] `specify extension list`
|
- [ ] `specify extension list`
|
||||||
- [x] `specify extension add` (from URL and local `--dev`)
|
- [ ] `specify extension add` (from URL)
|
||||||
- [x] `specify extension remove`
|
- [ ] `specify extension remove`
|
||||||
- [x] Extension registry (`.specify/extensions/.registry`)
|
- [ ] Extension registry (`.specify/extensions/.registry`)
|
||||||
- [x] Command registration (Claude and 15+ other agents)
|
- [ ] Command registration (Claude only initially)
|
||||||
- [x] Basic validation (manifest schema, compatibility)
|
- [ ] Basic validation (manifest schema, compatibility)
|
||||||
- [x] Documentation (extension development guide)
|
- [ ] Documentation (extension development guide)
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] Unit tests for manifest parsing
|
- [ ] Unit tests for manifest parsing
|
||||||
- [x] Integration test: Install dummy extension
|
- [ ] Integration test: Install dummy extension
|
||||||
- [x] Integration test: Register commands with Claude
|
- [ ] Integration test: Register commands with Claude
|
||||||
|
|
||||||
### Phase 2: Jira Extension ✅ COMPLETED
|
### Phase 2: Jira Extension (Week 3)
|
||||||
|
|
||||||
**Goal**: First production extension
|
**Goal**: First production extension
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Create `spec-kit-jira` repository
|
- [ ] Create `spec-kit-jira` repository
|
||||||
- [x] Port Jira functionality to extension
|
- [ ] Port Jira functionality to extension
|
||||||
- [x] Create `jira-config.yml` template
|
- [ ] Create `jira-config.yml` template
|
||||||
- [x] Commands:
|
- [ ] Commands:
|
||||||
- [x] `specstoissues.md`
|
- [ ] `specstoissues.md`
|
||||||
- [x] `discover-fields.md`
|
- [ ] `discover-fields.md`
|
||||||
- [x] `sync-status.md`
|
- [ ] `sync-status.md`
|
||||||
- [x] Helper scripts
|
- [ ] Helper scripts
|
||||||
- [x] Documentation (README, configuration guide, examples)
|
- [ ] Documentation (README, configuration guide, examples)
|
||||||
- [x] Release v3.0.0
|
- [ ] Release v1.0.0
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] Test on `eng-msa-ts` project
|
- [ ] Test on `eng-msa-ts` project
|
||||||
- [x] Verify spec→Epic, phase→Story, task→Issue mapping
|
- [ ] Verify spec→Epic, phase→Story, task→Issue mapping
|
||||||
- [x] Test configuration loading and validation
|
- [ ] Test configuration loading and validation
|
||||||
- [x] Test custom field application
|
- [ ] Test custom field application
|
||||||
|
|
||||||
### Phase 3: Extension Catalog ✅ COMPLETED
|
### Phase 3: Extension Catalog (Week 4)
|
||||||
|
|
||||||
**Goal**: Discovery and distribution
|
**Goal**: Discovery and distribution
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Central catalog (`extensions/catalog.json` in spec-kit repo)
|
- [ ] Central catalog (`extensions/catalog.json` in spec-kit repo)
|
||||||
- [x] Community catalog (`extensions/catalog.community.json`)
|
- [ ] Catalog fetch and parsing
|
||||||
- [x] Catalog fetch and parsing with multi-catalog support
|
- [ ] CLI commands:
|
||||||
- [x] CLI commands:
|
- [ ] `specify extension search`
|
||||||
- [x] `specify extension search`
|
- [ ] `specify extension info`
|
||||||
- [x] `specify extension info`
|
- [ ] Catalog publishing process (GitHub Action)
|
||||||
- [x] `specify extension catalog list`
|
- [ ] Documentation (how to publish extensions)
|
||||||
- [x] `specify extension catalog add`
|
|
||||||
- [x] `specify extension catalog remove`
|
|
||||||
- [x] Documentation (how to publish extensions)
|
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] Test catalog fetch
|
- [ ] Test catalog fetch
|
||||||
- [x] Test extension search/filtering
|
- [ ] Test extension search/filtering
|
||||||
- [x] Test catalog caching
|
- [ ] Test catalog caching
|
||||||
- [x] Test multi-catalog merge with priority
|
|
||||||
|
|
||||||
### Phase 4: Advanced Features ✅ COMPLETED
|
### Phase 4: Advanced Features (Week 5-6)
|
||||||
|
|
||||||
**Goal**: Hooks, updates, multi-agent support
|
**Goal**: Hooks, updates, multi-agent support
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Hook system (`hooks` in extension.yml)
|
- [ ] Hook system (`hooks` in extension.yml)
|
||||||
- [x] Hook registration and execution
|
- [ ] Hook registration and execution
|
||||||
- [x] Project extensions config (`.specify/extensions.yml`)
|
- [ ] Project extensions config (`.specify/extensions.yml`)
|
||||||
- [x] CLI commands:
|
- [ ] CLI commands:
|
||||||
- [x] `specify extension update` (with atomic backup/restore)
|
- [ ] `specify extension update`
|
||||||
- [x] `specify extension enable/disable`
|
- [ ] `specify extension enable/disable`
|
||||||
- [x] Command registration for multiple agents (15+ agents including Claude, Copilot, Gemini, Cursor, etc.)
|
- [ ] Command registration for multiple agents (Gemini, Copilot)
|
||||||
- [x] Extension update notifications (version comparison)
|
- [ ] Extension update notifications
|
||||||
- [x] Configuration layer resolution (project, local, env)
|
- [ ] 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**:
|
||||||
|
|
||||||
- [x] Test hooks in core commands
|
- [ ] Test hooks in core commands
|
||||||
- [x] Test extension updates (preserve config)
|
- [ ] Test extension updates (preserve config)
|
||||||
- [x] Test multi-agent registration
|
- [ ] 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 ✅ COMPLETED
|
### Phase 5: Polish & Documentation (Week 7)
|
||||||
|
|
||||||
**Goal**: Production ready
|
**Goal**: Production ready
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Comprehensive documentation:
|
- [ ] Comprehensive documentation:
|
||||||
- [x] User guide (EXTENSION-USER-GUIDE.md)
|
- [ ] User guide (installing/using extensions)
|
||||||
- [x] Extension development guide (EXTENSION-DEV-GUIDE.md)
|
- [ ] Extension development guide
|
||||||
- [x] Extension API reference (EXTENSION-API-REFERENCE.md)
|
- [ ] Extension API reference
|
||||||
- [x] Error messages and validation improvements
|
- [ ] Migration guide (core → extension)
|
||||||
- [x] CLI help text updates
|
- [ ] Error messages and validation improvements
|
||||||
|
- [ ] CLI help text updates
|
||||||
|
- [ ] Example extension template (cookiecutter)
|
||||||
|
- [ ] Blog post / announcement
|
||||||
|
- [ ] Video tutorial
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] End-to-end testing on multiple projects
|
- [ ] End-to-end testing on multiple projects
|
||||||
- [x] 163 unit tests passing
|
- [ ] Community beta testing
|
||||||
|
- [ ] Performance testing (large projects)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resolved Questions
|
## Open Questions
|
||||||
|
|
||||||
The following questions from the original RFC have been resolved during implementation:
|
### 1. Extension Namespace
|
||||||
|
|
||||||
### 1. Extension Namespace ✅ RESOLVED
|
|
||||||
|
|
||||||
**Question**: Should extension commands use namespace prefix?
|
**Question**: Should extension commands use namespace prefix?
|
||||||
|
|
||||||
**Decision**: **Option C** - Both prefixed and aliases are supported. Commands use `speckit.{extension}.{command}` as canonical name, with optional aliases defined in manifest.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: The `aliases` field in `extension.yml` allows extensions to register additional command names.
|
- A) Prefixed: `/speckit.jira.specstoissues` (explicit, avoids conflicts)
|
||||||
|
- 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 ✅ RESOLVED
|
### 2. Config File Location
|
||||||
|
|
||||||
**Question**: Where should extension configs live?
|
**Question**: Where should extension configs live?
|
||||||
|
|
||||||
**Decision**: **Option A** - Extension directory (`.specify/extensions/{ext-id}/{ext-id}-config.yml`). This keeps extensions self-contained and easier to manage.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: Each extension has its own config file within its directory, with layered resolution (defaults → project → local → env vars).
|
- A) Extension directory: `.specify/extensions/jira/jira-config.yml` (encapsulated)
|
||||||
|
- 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 ✅ RESOLVED
|
### 3. Command File Format
|
||||||
|
|
||||||
**Question**: Should extensions use universal format or agent-specific?
|
**Question**: Should extensions use universal format or agent-specific?
|
||||||
|
|
||||||
**Decision**: **Option A** - Universal Markdown format. Extensions write commands once, CLI converts to agent-specific format during registration.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: `CommandRegistrar` class handles conversion to 15+ agent formats (Claude, Copilot, Gemini, Cursor, etc.).
|
- A) Universal Markdown: Extensions write once, CLI converts per-agent
|
||||||
|
- 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 ✅ RESOLVED
|
### 4. Hook Execution Model
|
||||||
|
|
||||||
**Question**: How should hooks execute?
|
**Question**: How should hooks execute?
|
||||||
|
|
||||||
**Decision**: **Option A** - Hooks are registered in `.specify/extensions.yml` and executed by the AI agent when it sees the hook trigger. Hook state (enabled/disabled) is managed per-extension.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: `HookExecutor` class manages hook registration and state in `extensions.yml`.
|
- A) AI agent interprets: Core commands output `EXECUTE_COMMAND: name`
|
||||||
|
- 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 ✅ RESOLVED
|
### 5. Extension Distribution
|
||||||
|
|
||||||
**Question**: How should extensions be packaged?
|
**Question**: How should extensions be packaged?
|
||||||
|
|
||||||
**Decision**: **Option A** - ZIP archives downloaded from GitHub releases (via catalog `download_url`). Local development uses `--dev` flag with directory path.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: `ExtensionManager.install_from_zip()` handles ZIP extraction and validation.
|
- A) ZIP archives: Downloaded from GitHub releases
|
||||||
|
- 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 ✅ RESOLVED
|
### 6. Multi-Version Support
|
||||||
|
|
||||||
**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) No sandboxing (current): Extensions run with same privileges as AI agent
|
- A) Single version: Only one version installed at a time
|
||||||
- B) Permission declarations: Extensions declare `filesystem:read`, `network:external`, etc.
|
- B) Multi-version: Side-by-side versions (`.specify/extensions/jira@1.0/`, `.specify/extensions/jira@2.0/`)
|
||||||
- C) Opt-in sandboxing: Organizations can enable permission enforcement
|
- C) Per-branch: Different branches use different versions
|
||||||
|
|
||||||
**Status**: Deferred to future version. Currently using trust-based model where users trust extension authors.
|
**Recommendation**: A initially (simpler), consider B in future if needed
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 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,78 +1,8 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-03-17T00:00:00Z",
|
"updated_at": "2026-03-03T00: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": {
|
||||||
"archive": {
|
|
||||||
"name": "Archive Extension",
|
|
||||||
"id": "archive",
|
|
||||||
"description": "Archive merged features into main project memory, resolving gaps and conflicts.",
|
|
||||||
"author": "Stanislav Deviatov",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/stn1slv/spec-kit-archive/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/stn1slv/spec-kit-archive",
|
|
||||||
"homepage": "https://github.com/stn1slv/spec-kit-archive",
|
|
||||||
"documentation": "https://github.com/stn1slv/spec-kit-archive/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/stn1slv/spec-kit-archive/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 1,
|
|
||||||
"hooks": 0
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"archive",
|
|
||||||
"memory",
|
|
||||||
"merge",
|
|
||||||
"changelog"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-14T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-14T00:00:00Z"
|
|
||||||
},
|
|
||||||
"azure-devops": {
|
|
||||||
"name": "Azure DevOps Integration",
|
|
||||||
"id": "azure-devops",
|
|
||||||
"description": "Sync user stories and tasks to Azure DevOps work items using OAuth authentication.",
|
|
||||||
"author": "pragya247",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/pragya247/spec-kit-azure-devops/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/pragya247/spec-kit-azure-devops",
|
|
||||||
"homepage": "https://github.com/pragya247/spec-kit-azure-devops",
|
|
||||||
"documentation": "https://github.com/pragya247/spec-kit-azure-devops/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/pragya247/spec-kit-azure-devops/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0",
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "az",
|
|
||||||
"version": ">=2.0.0",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 1,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"azure",
|
|
||||||
"devops",
|
|
||||||
"project-management",
|
|
||||||
"work-items",
|
|
||||||
"issue-tracking"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-03T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-03T00:00:00Z"
|
|
||||||
},
|
|
||||||
"cleanup": {
|
"cleanup": {
|
||||||
"name": "Cleanup Extension",
|
"name": "Cleanup Extension",
|
||||||
"id": "cleanup",
|
"id": "cleanup",
|
||||||
@@ -92,270 +22,13 @@
|
|||||||
"commands": 1,
|
"commands": 1,
|
||||||
"hooks": 1
|
"hooks": 1
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["quality", "tech-debt", "review", "cleanup", "scout-rule"],
|
||||||
"quality",
|
|
||||||
"tech-debt",
|
|
||||||
"review",
|
|
||||||
"cleanup",
|
|
||||||
"scout-rule"
|
|
||||||
],
|
|
||||||
"verified": false,
|
"verified": false,
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"stars": 0,
|
"stars": 0,
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"cognitive-squad": {
|
|
||||||
"name": "Cognitive Squad",
|
|
||||||
"id": "cognitive-squad",
|
|
||||||
"description": "19-function cognitive agent squad for autonomous pre-code analysis — 7 core agents, 7 specialists, 4 learning functions with feedback loop",
|
|
||||||
"author": "Testimonial",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"download_url": "https://github.com/Testimonial/cognitive-squad/archive/refs/tags/v0.1.0.zip",
|
|
||||||
"repository": "https://github.com/Testimonial/cognitive-squad",
|
|
||||||
"homepage": "https://github.com/Testimonial/cognitive-squad",
|
|
||||||
"documentation": "https://github.com/Testimonial/cognitive-squad/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/Testimonial/cognitive-squad/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.3.0",
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "understanding",
|
|
||||||
"version": ">=3.4.0",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "spec-kit-reverse-eng",
|
|
||||||
"version": ">=1.0.0",
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 7,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"ai-agents",
|
|
||||||
"cognitive",
|
|
||||||
"pre-code",
|
|
||||||
"analysis",
|
|
||||||
"multi-agent"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-16T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-16T00:00:00Z"
|
|
||||||
},
|
|
||||||
"docguard": {
|
|
||||||
"name": "DocGuard \u2014 CDD Enforcement",
|
|
||||||
"id": "docguard",
|
|
||||||
"description": "Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero dependencies.",
|
|
||||||
"author": "raccioly",
|
|
||||||
"version": "0.9.8",
|
|
||||||
"download_url": "https://github.com/raccioly/docguard/releases/download/v0.9.8/spec-kit-docguard-v0.9.8.zip",
|
|
||||||
"repository": "https://github.com/raccioly/docguard",
|
|
||||||
"homepage": "https://www.npmjs.com/package/docguard-cli",
|
|
||||||
"documentation": "https://github.com/raccioly/docguard/blob/main/extensions/spec-kit-docguard/README.md",
|
|
||||||
"changelog": "https://github.com/raccioly/docguard/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0",
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "node",
|
|
||||||
"version": ">=18.0.0",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 6,
|
|
||||||
"hooks": 3
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"documentation",
|
|
||||||
"validation",
|
|
||||||
"quality",
|
|
||||||
"cdd",
|
|
||||||
"traceability",
|
|
||||||
"ai-agents",
|
|
||||||
"enforcement",
|
|
||||||
"spec-kit"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-13T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-15T20: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": {
|
|
||||||
"name": "Fleet Orchestrator",
|
|
||||||
"id": "fleet",
|
|
||||||
"description": "Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases.",
|
|
||||||
"author": "sharathsatish",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/sharathsatish/spec-kit-fleet/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/sharathsatish/spec-kit-fleet",
|
|
||||||
"homepage": "https://github.com/sharathsatish/spec-kit-fleet",
|
|
||||||
"documentation": "https://github.com/sharathsatish/spec-kit-fleet/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/sharathsatish/spec-kit-fleet/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 2,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"orchestration",
|
|
||||||
"workflow",
|
|
||||||
"human-in-the-loop",
|
|
||||||
"parallel"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-06T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-06T00:00:00Z"
|
|
||||||
},
|
|
||||||
"jira": {
|
|
||||||
"name": "Jira Integration",
|
|
||||||
"id": "jira",
|
|
||||||
"description": "Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support.",
|
|
||||||
"author": "mbachorik",
|
|
||||||
"version": "2.1.0",
|
|
||||||
"download_url": "https://github.com/mbachorik/spec-kit-jira/archive/refs/tags/v2.1.0.zip",
|
|
||||||
"repository": "https://github.com/mbachorik/spec-kit-jira",
|
|
||||||
"homepage": "https://github.com/mbachorik/spec-kit-jira",
|
|
||||||
"documentation": "https://github.com/mbachorik/spec-kit-jira/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/mbachorik/spec-kit-jira/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 3,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"issue-tracking",
|
|
||||||
"jira",
|
|
||||||
"atlassian",
|
|
||||||
"project-management"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-05T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-05T00:00:00Z"
|
|
||||||
},
|
|
||||||
"ralph": {
|
|
||||||
"name": "Ralph Loop",
|
|
||||||
"id": "ralph",
|
|
||||||
"description": "Autonomous implementation loop using AI agent CLI.",
|
|
||||||
"author": "Rubiss",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/Rubiss/spec-kit-ralph/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/Rubiss/spec-kit-ralph",
|
|
||||||
"homepage": "https://github.com/Rubiss/spec-kit-ralph",
|
|
||||||
"documentation": "https://github.com/Rubiss/spec-kit-ralph/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/Rubiss/spec-kit-ralph/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0",
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "copilot",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "git",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 2,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"implementation",
|
|
||||||
"automation",
|
|
||||||
"loop",
|
|
||||||
"copilot"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-09T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-09T00:00:00Z"
|
|
||||||
},
|
|
||||||
"reconcile": {
|
|
||||||
"name": "Reconcile Extension",
|
|
||||||
"id": "reconcile",
|
|
||||||
"description": "Reconcile implementation drift by surgically updating the feature's own spec, plan, and tasks.",
|
|
||||||
"author": "Stanislav Deviatov",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/stn1slv/spec-kit-reconcile/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/stn1slv/spec-kit-reconcile",
|
|
||||||
"homepage": "https://github.com/stn1slv/spec-kit-reconcile",
|
|
||||||
"documentation": "https://github.com/stn1slv/spec-kit-reconcile/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/stn1slv/spec-kit-reconcile/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 1,
|
|
||||||
"hooks": 0
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"reconcile",
|
|
||||||
"drift",
|
|
||||||
"tasks",
|
|
||||||
"remediation"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-14T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-14T00:00:00Z"
|
|
||||||
},
|
|
||||||
"retrospective": {
|
"retrospective": {
|
||||||
"name": "Retrospective Extension",
|
"name": "Retrospective Extension",
|
||||||
"id": "retrospective",
|
"id": "retrospective",
|
||||||
@@ -375,53 +48,13 @@
|
|||||||
"commands": 1,
|
"commands": 1,
|
||||||
"hooks": 1
|
"hooks": 1
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["retrospective", "spec-drift", "quality", "analysis", "governance"],
|
||||||
"retrospective",
|
|
||||||
"spec-drift",
|
|
||||||
"quality",
|
|
||||||
"analysis",
|
|
||||||
"governance"
|
|
||||||
],
|
|
||||||
"verified": false,
|
"verified": false,
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"stars": 0,
|
"stars": 0,
|
||||||
"created_at": "2026-02-24T00:00:00Z",
|
"created_at": "2026-02-24T00:00:00Z",
|
||||||
"updated_at": "2026-02-24T00:00:00Z"
|
"updated_at": "2026-02-24T00:00:00Z"
|
||||||
},
|
},
|
||||||
"review": {
|
|
||||||
"name": "Review Extension",
|
|
||||||
"id": "review",
|
|
||||||
"description": "Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification.",
|
|
||||||
"author": "ismaelJimenez",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/ismaelJimenez/spec-kit-review",
|
|
||||||
"homepage": "https://github.com/ismaelJimenez/spec-kit-review",
|
|
||||||
"documentation": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 7,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"code-review",
|
|
||||||
"quality",
|
|
||||||
"review",
|
|
||||||
"testing",
|
|
||||||
"error-handling",
|
|
||||||
"type-design",
|
|
||||||
"simplification"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-06T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-06T00:00:00Z"
|
|
||||||
},
|
|
||||||
"sync": {
|
"sync": {
|
||||||
"name": "Spec Sync",
|
"name": "Spec Sync",
|
||||||
"id": "sync",
|
"id": "sync",
|
||||||
@@ -441,92 +74,13 @@
|
|||||||
"commands": 5,
|
"commands": 5,
|
||||||
"hooks": 1
|
"hooks": 1
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["sync", "drift", "validation", "bidirectional", "backfill"],
|
||||||
"sync",
|
|
||||||
"drift",
|
|
||||||
"validation",
|
|
||||||
"bidirectional",
|
|
||||||
"backfill"
|
|
||||||
],
|
|
||||||
"verified": false,
|
"verified": false,
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"stars": 0,
|
"stars": 0,
|
||||||
"created_at": "2026-03-02T00:00:00Z",
|
"created_at": "2026-03-02T00:00:00Z",
|
||||||
"updated_at": "2026-03-02T00:00:00Z"
|
"updated_at": "2026-03-02T00:00:00Z"
|
||||||
},
|
},
|
||||||
"understanding": {
|
|
||||||
"name": "Understanding",
|
|
||||||
"id": "understanding",
|
|
||||||
"description": "Automated requirements quality analysis \u2014 validates specs against IEEE/ISO standards using 31 deterministic metrics. Catches ambiguity, missing testability, and structural issues before they reach implementation. Includes experimental energy-based ambiguity detection using local LM token perplexity.",
|
|
||||||
"author": "Ladislav Bihari",
|
|
||||||
"version": "3.4.0",
|
|
||||||
"download_url": "https://github.com/Testimonial/understanding/archive/refs/tags/v3.4.0.zip",
|
|
||||||
"repository": "https://github.com/Testimonial/understanding",
|
|
||||||
"homepage": "https://github.com/Testimonial/understanding",
|
|
||||||
"documentation": "https://github.com/Testimonial/understanding/blob/main/extension/README.md",
|
|
||||||
"changelog": "https://github.com/Testimonial/understanding/blob/main/extension/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0",
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "understanding",
|
|
||||||
"version": ">=3.4.0",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 3,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"quality",
|
|
||||||
"metrics",
|
|
||||||
"requirements",
|
|
||||||
"validation",
|
|
||||||
"readability",
|
|
||||||
"IEEE-830",
|
|
||||||
"ISO-29148"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-07T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-07T00:00:00Z"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "Project Status",
|
|
||||||
"id": "status",
|
|
||||||
"description": "Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary.",
|
|
||||||
"author": "KhawarHabibKhan",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/KhawarHabibKhan/spec-kit-status/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/KhawarHabibKhan/spec-kit-status",
|
|
||||||
"homepage": "https://github.com/KhawarHabibKhan/spec-kit-status",
|
|
||||||
"documentation": "https://github.com/KhawarHabibKhan/spec-kit-status/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/KhawarHabibKhan/spec-kit-status/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 1,
|
|
||||||
"hooks": 0
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"status",
|
|
||||||
"workflow",
|
|
||||||
"progress",
|
|
||||||
"feature-tracking",
|
|
||||||
"task-progress"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-16T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-16T00:00:00Z"
|
|
||||||
},
|
|
||||||
"v-model": {
|
"v-model": {
|
||||||
"name": "V-Model Extension Pack",
|
"name": "V-Model Extension Pack",
|
||||||
"id": "v-model",
|
"id": "v-model",
|
||||||
@@ -546,50 +100,13 @@
|
|||||||
"commands": 9,
|
"commands": 9,
|
||||||
"hooks": 1
|
"hooks": 1
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["v-model", "traceability", "testing", "compliance", "safety-critical"],
|
||||||
"v-model",
|
|
||||||
"traceability",
|
|
||||||
"testing",
|
|
||||||
"compliance",
|
|
||||||
"safety-critical"
|
|
||||||
],
|
|
||||||
"verified": false,
|
"verified": false,
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"stars": 0,
|
"stars": 0,
|
||||||
"created_at": "2026-02-20T00:00:00Z",
|
"created_at": "2026-02-20T00:00:00Z",
|
||||||
"updated_at": "2026-02-22T00:00:00Z"
|
"updated_at": "2026-02-22T00:00:00Z"
|
||||||
},
|
},
|
||||||
"learn": {
|
|
||||||
"name": "Learning Extension",
|
|
||||||
"id": "learn",
|
|
||||||
"description": "Generate educational guides from implementations and enhance clarifications with mentoring context.",
|
|
||||||
"author": "Vianca Martinez",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/imviancagrace/spec-kit-learn/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/imviancagrace/spec-kit-learn",
|
|
||||||
"homepage": "https://github.com/imviancagrace/spec-kit-learn",
|
|
||||||
"documentation": "https://github.com/imviancagrace/spec-kit-learn/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/imviancagrace/spec-kit-learn/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 2,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"learning",
|
|
||||||
"education",
|
|
||||||
"mentoring",
|
|
||||||
"knowledge-transfer"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-17T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-17T00:00:00Z"
|
|
||||||
},
|
|
||||||
"verify": {
|
"verify": {
|
||||||
"name": "Verify Extension",
|
"name": "Verify Extension",
|
||||||
"id": "verify",
|
"id": "verify",
|
||||||
@@ -609,13 +126,7 @@
|
|||||||
"commands": 1,
|
"commands": 1,
|
||||||
"hooks": 1
|
"hooks": 1
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["verification", "quality-gate", "implementation", "spec-adherence", "compliance"],
|
||||||
"verification",
|
|
||||||
"quality-gate",
|
|
||||||
"implementation",
|
|
||||||
"spec-adherence",
|
|
||||||
"compliance"
|
|
||||||
],
|
|
||||||
"verified": false,
|
"verified": false,
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"stars": 0,
|
"stars": 0,
|
||||||
|
|||||||
@@ -1,21 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-03-10T00:00:00Z",
|
"updated_at": "2026-02-03T00: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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
---
|
|
||||||
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 ... ==============================
|
|
||||||
```
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# 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,157 +0,0 @@
|
|||||||
# Preset System Architecture
|
|
||||||
|
|
||||||
This document describes the internal architecture of the preset system — how template resolution, command registration, and catalog management work under the hood.
|
|
||||||
|
|
||||||
For usage instructions, see [README.md](README.md).
|
|
||||||
|
|
||||||
## Template Resolution
|
|
||||||
|
|
||||||
When Spec Kit needs a template (e.g. `spec-template`), the `PresetResolver` walks a priority stack and returns the first match:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["resolve_template('spec-template')"] --> B{Override exists?}
|
|
||||||
B -- Yes --> C[".specify/templates/overrides/spec-template.md"]
|
|
||||||
B -- No --> D{Preset provides it?}
|
|
||||||
D -- Yes --> E[".specify/presets/‹preset-id›/templates/spec-template.md"]
|
|
||||||
D -- No --> F{Extension provides it?}
|
|
||||||
F -- Yes --> G[".specify/extensions/‹ext-id›/templates/spec-template.md"]
|
|
||||||
F -- No --> H[".specify/templates/spec-template.md"]
|
|
||||||
|
|
||||||
E -- "multiple presets?" --> I["lowest priority number wins"]
|
|
||||||
I --> E
|
|
||||||
|
|
||||||
style C fill:#4caf50,color:#fff
|
|
||||||
style E fill:#2196f3,color:#fff
|
|
||||||
style G fill:#ff9800,color:#fff
|
|
||||||
style H fill:#9e9e9e,color:#fff
|
|
||||||
```
|
|
||||||
|
|
||||||
| Priority | Source | Path | Use case |
|
|
||||||
|----------|--------|------|----------|
|
|
||||||
| 1 (highest) | Override | `.specify/templates/overrides/` | One-off project-local tweaks |
|
|
||||||
| 2 | Preset | `.specify/presets/<id>/templates/` | Shareable, stackable customizations |
|
|
||||||
| 3 | Extension | `.specify/extensions/<id>/templates/` | Extension-provided templates |
|
|
||||||
| 4 (lowest) | Core | `.specify/templates/` | Shipped defaults |
|
|
||||||
|
|
||||||
When multiple presets are installed, they're sorted by their `priority` field (lower number = higher precedence). This is set via `--priority` on `specify preset add`.
|
|
||||||
|
|
||||||
The resolution is implemented three times to ensure consistency:
|
|
||||||
- **Python**: `PresetResolver` in `src/specify_cli/presets.py`
|
|
||||||
- **Bash**: `resolve_template()` in `scripts/bash/common.sh`
|
|
||||||
- **PowerShell**: `Resolve-Template` in `scripts/powershell/common.ps1`
|
|
||||||
|
|
||||||
## Command Registration
|
|
||||||
|
|
||||||
When a preset is installed with `type: "command"` entries, the `PresetManager` registers them into all detected agent directories using the shared `CommandRegistrar` from `src/specify_cli/agents.py`.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["specify preset add my-preset"] --> B{Preset has type: command?}
|
|
||||||
B -- No --> Z["done (templates only)"]
|
|
||||||
B -- Yes --> C{Extension command?}
|
|
||||||
C -- "speckit.myext.cmd\n(3+ dot segments)" --> D{Extension installed?}
|
|
||||||
D -- No --> E["skip (extension not active)"]
|
|
||||||
D -- Yes --> F["register command"]
|
|
||||||
C -- "speckit.specify\n(core command)" --> F
|
|
||||||
F --> G["detect agent directories"]
|
|
||||||
G --> H[".claude/commands/"]
|
|
||||||
G --> I[".gemini/commands/"]
|
|
||||||
G --> J[".github/agents/"]
|
|
||||||
G --> K["... (17+ agents)"]
|
|
||||||
H --> L["write .md (Markdown format)"]
|
|
||||||
I --> M["write .toml (TOML format)"]
|
|
||||||
J --> N["write .agent.md + .prompt.md"]
|
|
||||||
|
|
||||||
style E fill:#ff5722,color:#fff
|
|
||||||
style L fill:#4caf50,color:#fff
|
|
||||||
style M fill:#4caf50,color:#fff
|
|
||||||
style N fill:#4caf50,color:#fff
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extension safety check
|
|
||||||
|
|
||||||
Command names follow the pattern `speckit.<ext-id>.<cmd-name>`. When a command has 3+ dot segments, the system extracts the extension ID and checks if `.specify/extensions/<ext-id>/` exists. If the extension isn't installed, the command is skipped — preventing orphan files referencing non-existent extensions.
|
|
||||||
|
|
||||||
Core commands (e.g. `speckit.specify`, with only 2 segments) are always registered.
|
|
||||||
|
|
||||||
### Agent format rendering
|
|
||||||
|
|
||||||
The `CommandRegistrar` renders commands differently per agent:
|
|
||||||
|
|
||||||
| Agent | Format | Extension | Arg placeholder |
|
|
||||||
|-------|--------|-----------|-----------------|
|
|
||||||
| Claude, Cursor, opencode, Windsurf, etc. | Markdown | `.md` | `$ARGUMENTS` |
|
|
||||||
| Copilot | Markdown | `.agent.md` + `.prompt.md` | `$ARGUMENTS` |
|
|
||||||
| Gemini, Qwen, Tabnine | TOML | `.toml` | `{{args}}` |
|
|
||||||
|
|
||||||
### Cleanup on removal
|
|
||||||
|
|
||||||
When `specify preset remove` is called, the registered commands are read from the registry metadata and the corresponding files are deleted from each agent directory, including Copilot companion `.prompt.md` files.
|
|
||||||
|
|
||||||
## Catalog System
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["specify preset search"] --> B["PresetCatalog.get_active_catalogs()"]
|
|
||||||
B --> C{SPECKIT_PRESET_CATALOG_URL set?}
|
|
||||||
C -- Yes --> D["single custom catalog"]
|
|
||||||
C -- No --> E{.specify/preset-catalogs.yml exists?}
|
|
||||||
E -- Yes --> F["project-level catalog stack"]
|
|
||||||
E -- No --> G{"~/.specify/preset-catalogs.yml exists?"}
|
|
||||||
G -- Yes --> H["user-level catalog stack"]
|
|
||||||
G -- No --> I["built-in defaults"]
|
|
||||||
I --> J["default (install allowed)"]
|
|
||||||
I --> K["community (discovery only)"]
|
|
||||||
|
|
||||||
style D fill:#ff9800,color:#fff
|
|
||||||
style F fill:#2196f3,color:#fff
|
|
||||||
style H fill:#2196f3,color:#fff
|
|
||||||
style J fill:#4caf50,color:#fff
|
|
||||||
style K fill:#9e9e9e,color:#fff
|
|
||||||
```
|
|
||||||
|
|
||||||
Catalogs are fetched with a 1-hour cache (per-URL, SHA256-hashed cache files). Each catalog entry has a `priority` (for merge ordering) and `install_allowed` flag.
|
|
||||||
|
|
||||||
## Repository Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
presets/
|
|
||||||
├── ARCHITECTURE.md # This file
|
|
||||||
├── PUBLISHING.md # Guide for submitting presets to the catalog
|
|
||||||
├── README.md # User guide
|
|
||||||
├── catalog.json # Official preset catalog
|
|
||||||
├── catalog.community.json # Community preset catalog
|
|
||||||
├── scaffold/ # Scaffold for creating new presets
|
|
||||||
│ ├── preset.yml # Example manifest
|
|
||||||
│ ├── README.md # Guide for customizing the scaffold
|
|
||||||
│ ├── commands/
|
|
||||||
│ │ ├── speckit.specify.md # Core command override example
|
|
||||||
│ │ └── speckit.myext.myextcmd.md # Extension command override example
|
|
||||||
│ └── templates/
|
|
||||||
│ ├── spec-template.md # Core template override example
|
|
||||||
│ └── myext-template.md # Extension template override example
|
|
||||||
└── self-test/ # Self-test preset (overrides all core templates)
|
|
||||||
├── preset.yml
|
|
||||||
├── commands/
|
|
||||||
│ └── speckit.specify.md
|
|
||||||
└── templates/
|
|
||||||
├── spec-template.md
|
|
||||||
├── plan-template.md
|
|
||||||
├── tasks-template.md
|
|
||||||
├── checklist-template.md
|
|
||||||
├── constitution-template.md
|
|
||||||
└── agent-file-template.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/specify_cli/
|
|
||||||
├── agents.py # CommandRegistrar — shared infrastructure for writing
|
|
||||||
│ # command files to agent directories
|
|
||||||
├── presets.py # PresetManifest, PresetRegistry, PresetManager,
|
|
||||||
│ # PresetCatalog, PresetCatalogEntry, PresetResolver
|
|
||||||
└── __init__.py # CLI commands: specify preset list/add/remove/search/
|
|
||||||
# resolve/info, specify preset catalog list/add/remove
|
|
||||||
```
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
# Preset Publishing Guide
|
|
||||||
|
|
||||||
This guide explains how to publish your preset to the Spec Kit preset catalog, making it discoverable by `specify preset search`.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
1. [Prerequisites](#prerequisites)
|
|
||||||
2. [Prepare Your Preset](#prepare-your-preset)
|
|
||||||
3. [Submit to Catalog](#submit-to-catalog)
|
|
||||||
4. [Verification Process](#verification-process)
|
|
||||||
5. [Release Workflow](#release-workflow)
|
|
||||||
6. [Best Practices](#best-practices)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before publishing a preset, ensure you have:
|
|
||||||
|
|
||||||
1. **Valid Preset**: A working preset with a valid `preset.yml` manifest
|
|
||||||
2. **Git Repository**: Preset hosted on GitHub (or other public git hosting)
|
|
||||||
3. **Documentation**: README.md with description and usage instructions
|
|
||||||
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
|
|
||||||
5. **Versioning**: Semantic versioning (e.g., 1.0.0)
|
|
||||||
6. **Testing**: Preset tested on real projects with `specify preset add --dev`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prepare Your Preset
|
|
||||||
|
|
||||||
### 1. Preset Structure
|
|
||||||
|
|
||||||
Ensure your preset follows the standard structure:
|
|
||||||
|
|
||||||
```text
|
|
||||||
your-preset/
|
|
||||||
├── preset.yml # Required: Preset manifest
|
|
||||||
├── README.md # Required: Documentation
|
|
||||||
├── LICENSE # Required: License file
|
|
||||||
├── CHANGELOG.md # Recommended: Version history
|
|
||||||
│
|
|
||||||
├── templates/ # Template overrides
|
|
||||||
│ ├── spec-template.md
|
|
||||||
│ ├── plan-template.md
|
|
||||||
│ └── ...
|
|
||||||
│
|
|
||||||
└── commands/ # Command overrides (optional)
|
|
||||||
└── speckit.specify.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Start from the [scaffold](scaffold/) if you're creating a new preset.
|
|
||||||
|
|
||||||
### 2. preset.yml Validation
|
|
||||||
|
|
||||||
Verify your manifest is valid:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
schema_version: "1.0"
|
|
||||||
|
|
||||||
preset:
|
|
||||||
id: "your-preset" # Unique lowercase-hyphenated ID
|
|
||||||
name: "Your Preset Name" # Human-readable name
|
|
||||||
version: "1.0.0" # Semantic version
|
|
||||||
description: "Brief description (one sentence)"
|
|
||||||
author: "Your Name or Organization"
|
|
||||||
repository: "https://github.com/your-org/spec-kit-preset-your-preset"
|
|
||||||
license: "MIT"
|
|
||||||
|
|
||||||
requires:
|
|
||||||
speckit_version: ">=0.1.0" # Required spec-kit version
|
|
||||||
|
|
||||||
provides:
|
|
||||||
templates:
|
|
||||||
- type: "template"
|
|
||||||
name: "spec-template"
|
|
||||||
file: "templates/spec-template.md"
|
|
||||||
description: "Custom spec template"
|
|
||||||
replaces: "spec-template"
|
|
||||||
|
|
||||||
tags: # 2-5 relevant tags
|
|
||||||
- "category"
|
|
||||||
- "workflow"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Validation Checklist**:
|
|
||||||
|
|
||||||
- ✅ `id` is lowercase with hyphens only (no underscores, spaces, or special characters)
|
|
||||||
- ✅ `version` follows semantic versioning (X.Y.Z)
|
|
||||||
- ✅ `description` is concise (under 200 characters)
|
|
||||||
- ✅ `repository` URL is valid and public
|
|
||||||
- ✅ All template and command files exist in the preset directory
|
|
||||||
- ✅ Template names are lowercase with hyphens only
|
|
||||||
- ✅ Command names use dot notation (e.g. `speckit.specify`)
|
|
||||||
- ✅ Tags are lowercase and descriptive
|
|
||||||
|
|
||||||
### 3. Test Locally
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install from local directory
|
|
||||||
specify preset add --dev /path/to/your-preset
|
|
||||||
|
|
||||||
# Verify templates resolve from your preset
|
|
||||||
specify preset resolve spec-template
|
|
||||||
|
|
||||||
# Verify preset info
|
|
||||||
specify preset info your-preset
|
|
||||||
|
|
||||||
# List installed presets
|
|
||||||
specify preset list
|
|
||||||
|
|
||||||
# Remove when done testing
|
|
||||||
specify preset remove your-preset
|
|
||||||
```
|
|
||||||
|
|
||||||
If your preset includes command overrides, verify they appear in the agent directories:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check Claude commands (if using Claude)
|
|
||||||
ls .claude/commands/speckit.*.md
|
|
||||||
|
|
||||||
# Check Copilot commands (if using Copilot)
|
|
||||||
ls .github/agents/speckit.*.agent.md
|
|
||||||
|
|
||||||
# Check Gemini commands (if using Gemini)
|
|
||||||
ls .gemini/commands/speckit.*.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Create GitHub Release
|
|
||||||
|
|
||||||
Create a GitHub release for your preset version:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Tag the release
|
|
||||||
git tag v1.0.0
|
|
||||||
git push origin v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
The release archive URL will be:
|
|
||||||
|
|
||||||
```text
|
|
||||||
https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Test Installation from Archive
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify preset add --from https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Submit to Catalog
|
|
||||||
|
|
||||||
### Understanding the Catalogs
|
|
||||||
|
|
||||||
Spec Kit uses a dual-catalog system:
|
|
||||||
|
|
||||||
- **`catalog.json`** — Official, verified presets (install allowed by default)
|
|
||||||
- **`catalog.community.json`** — Community-contributed presets (discovery only by default)
|
|
||||||
|
|
||||||
All community presets should be submitted to `catalog.community.json`.
|
|
||||||
|
|
||||||
### 1. Fork the spec-kit Repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/YOUR-USERNAME/spec-kit.git
|
|
||||||
cd spec-kit
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Add Preset to Community Catalog
|
|
||||||
|
|
||||||
Edit `presets/catalog.community.json` and add your preset.
|
|
||||||
|
|
||||||
> **⚠️ Entries must be sorted alphabetically by preset ID.** Insert your preset in the correct position within the `"presets"` object.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"schema_version": "1.0",
|
|
||||||
"updated_at": "2026-03-10T00:00:00Z",
|
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
|
|
||||||
"presets": {
|
|
||||||
"your-preset": {
|
|
||||||
"name": "Your Preset Name",
|
|
||||||
"description": "Brief description of what your preset provides",
|
|
||||||
"author": "Your Name",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/your-org/spec-kit-preset-your-preset",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"templates": 3,
|
|
||||||
"commands": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"category",
|
|
||||||
"workflow"
|
|
||||||
],
|
|
||||||
"created_at": "2026-03-10T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-10T00:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Submit Pull Request
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout -b add-your-preset
|
|
||||||
git add presets/catalog.community.json
|
|
||||||
git commit -m "Add your-preset to community catalog
|
|
||||||
|
|
||||||
- Preset ID: your-preset
|
|
||||||
- Version: 1.0.0
|
|
||||||
- Author: Your Name
|
|
||||||
- Description: Brief description
|
|
||||||
"
|
|
||||||
git push origin add-your-preset
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pull Request Checklist**:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Preset Submission
|
|
||||||
|
|
||||||
**Preset Name**: Your Preset Name
|
|
||||||
**Preset ID**: your-preset
|
|
||||||
**Version**: 1.0.0
|
|
||||||
**Repository**: https://github.com/your-org/spec-kit-preset-your-preset
|
|
||||||
|
|
||||||
### Checklist
|
|
||||||
- [ ] Valid preset.yml manifest
|
|
||||||
- [ ] README.md with description and usage
|
|
||||||
- [ ] LICENSE file included
|
|
||||||
- [ ] GitHub release created
|
|
||||||
- [ ] Preset tested with `specify preset add --dev`
|
|
||||||
- [ ] Templates resolve correctly (`specify preset resolve`)
|
|
||||||
- [ ] Commands register to agent directories (if applicable)
|
|
||||||
- [ ] Commands match template sections (command + template are coherent)
|
|
||||||
- [ ] Added to presets/catalog.community.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Process
|
|
||||||
|
|
||||||
After submission, maintainers will review:
|
|
||||||
|
|
||||||
1. **Manifest validation** — valid `preset.yml`, all files exist
|
|
||||||
2. **Template quality** — templates are useful and well-structured
|
|
||||||
3. **Command coherence** — commands reference sections that exist in templates
|
|
||||||
4. **Security** — no malicious content, safe file operations
|
|
||||||
5. **Documentation** — clear README explaining what the preset does
|
|
||||||
|
|
||||||
Once verified, `verified: true` is set and the preset appears in `specify preset search`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Release Workflow
|
|
||||||
|
|
||||||
When releasing a new version:
|
|
||||||
|
|
||||||
1. Update `version` in `preset.yml`
|
|
||||||
2. Update CHANGELOG.md
|
|
||||||
3. Tag and push: `git tag v1.1.0 && git push origin v1.1.0`
|
|
||||||
4. Submit PR to update `version` and `download_url` in `presets/catalog.community.json`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Template Design
|
|
||||||
|
|
||||||
- **Keep sections clear** — use headings and placeholder text the LLM can replace
|
|
||||||
- **Match commands to templates** — if your preset overrides a command, make sure it references the sections in your template
|
|
||||||
- **Document customization points** — use HTML comments to guide users on what to change
|
|
||||||
|
|
||||||
### Naming
|
|
||||||
|
|
||||||
- Preset IDs should be descriptive: `healthcare-compliance`, `enterprise-safe`, `startup-lean`
|
|
||||||
- Avoid generic names: `my-preset`, `custom`, `test`
|
|
||||||
|
|
||||||
### Stacking
|
|
||||||
|
|
||||||
- Design presets to work well when stacked with others
|
|
||||||
- Only override templates you need to change
|
|
||||||
- Document which templates and commands your preset modifies
|
|
||||||
|
|
||||||
### Command Overrides
|
|
||||||
|
|
||||||
- Only override commands when the workflow needs to change, not just the output format
|
|
||||||
- If you only need different template sections, a template override is sufficient
|
|
||||||
- Test command overrides with multiple agents (Claude, Gemini, Copilot)
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
# Presets
|
|
||||||
|
|
||||||
Presets are stackable, priority-ordered collections of template and command overrides for Spec Kit. They let you customize both the artifacts produced by the Spec-Driven Development workflow (specs, plans, tasks, checklists, constitutions) and the commands that guide the LLM in creating them — without forking or modifying core files.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
When Spec Kit needs a template (e.g. `spec-template`), it walks a resolution stack:
|
|
||||||
|
|
||||||
1. `.specify/templates/overrides/` — project-local one-off overrides
|
|
||||||
2. `.specify/presets/<preset-id>/templates/` — installed presets (sorted by priority)
|
|
||||||
3. `.specify/extensions/<ext-id>/templates/` — extension-provided templates
|
|
||||||
4. `.specify/templates/` — core templates shipped with Spec Kit
|
|
||||||
|
|
||||||
If no preset is installed, core templates are used — exactly the same behavior as before presets existed.
|
|
||||||
|
|
||||||
For detailed resolution and command registration flows, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
||||||
|
|
||||||
## Command Overrides
|
|
||||||
|
|
||||||
Presets can also override the commands that guide the SDD workflow. Templates define *what* gets produced (specs, plans, constitutions); commands define *how* the LLM produces them (the step-by-step instructions).
|
|
||||||
|
|
||||||
When a preset includes `type: "command"` entries, the commands are automatically registered into all detected agent directories (`.claude/commands/`, `.gemini/commands/`, etc.) in the correct format (Markdown or TOML with appropriate argument placeholders). When the preset is removed, the registered commands are cleaned up.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Search available presets
|
|
||||||
specify preset search
|
|
||||||
|
|
||||||
# Install a preset from the catalog
|
|
||||||
specify preset add healthcare-compliance
|
|
||||||
|
|
||||||
# Install from a local directory (for development)
|
|
||||||
specify preset add --dev ./my-preset
|
|
||||||
|
|
||||||
# Install with a specific priority (lower = higher precedence)
|
|
||||||
specify preset add healthcare-compliance --priority 5
|
|
||||||
|
|
||||||
# List installed presets
|
|
||||||
specify preset list
|
|
||||||
|
|
||||||
# See which template a name resolves to
|
|
||||||
specify preset resolve spec-template
|
|
||||||
|
|
||||||
# Get detailed info about a preset
|
|
||||||
specify preset info healthcare-compliance
|
|
||||||
|
|
||||||
# Remove a preset
|
|
||||||
specify preset remove healthcare-compliance
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stacking Presets
|
|
||||||
|
|
||||||
Multiple presets can be installed simultaneously. The `--priority` flag controls which one wins when two presets provide the same template (lower number = higher precedence):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify preset add enterprise-safe --priority 10 # base layer
|
|
||||||
specify preset add healthcare-compliance --priority 5 # overrides enterprise-safe
|
|
||||||
specify preset add pm-workflow --priority 1 # overrides everything
|
|
||||||
```
|
|
||||||
|
|
||||||
Presets **override**, they don't merge. If two presets both provide `spec-template`, the one with the lowest priority number wins entirely.
|
|
||||||
|
|
||||||
## Catalog Management
|
|
||||||
|
|
||||||
Presets are discovered through catalogs. By default, Spec Kit uses the official and community catalogs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List active catalogs
|
|
||||||
specify preset catalog list
|
|
||||||
|
|
||||||
# Add a custom catalog
|
|
||||||
specify preset catalog add https://example.com/catalog.json --name my-org --install-allowed
|
|
||||||
|
|
||||||
# Remove a catalog
|
|
||||||
specify preset catalog remove my-org
|
|
||||||
```
|
|
||||||
|
|
||||||
## Creating a Preset
|
|
||||||
|
|
||||||
See [scaffold/](scaffold/) for a scaffold you can copy to create your own preset.
|
|
||||||
|
|
||||||
1. Copy `scaffold/` to a new directory
|
|
||||||
2. Edit `preset.yml` with your preset's metadata
|
|
||||||
3. Add or replace templates in `templates/`
|
|
||||||
4. Test locally with `specify preset add --dev .`
|
|
||||||
5. Verify with `specify preset resolve spec-template`
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `SPECKIT_PRESET_CATALOG_URL` | Override the catalog URL (replaces all defaults) |
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
| File | Scope | Description |
|
|
||||||
|------|-------|-------------|
|
|
||||||
| `.specify/preset-catalogs.yml` | Project | Custom catalog stack for this project |
|
|
||||||
| `~/.specify/preset-catalogs.yml` | User | Custom catalog stack for all projects |
|
|
||||||
|
|
||||||
## Future Considerations
|
|
||||||
|
|
||||||
The following enhancements are under consideration for future releases:
|
|
||||||
|
|
||||||
- **Composition strategies** — Allow presets to declare a `strategy` per template instead of the default `replace`:
|
|
||||||
|
|
||||||
| Type | `replace` | `prepend` | `append` | `wrap` |
|
|
||||||
|------|-----------|-----------|----------|--------|
|
|
||||||
| **template** | ✓ (default) | ✓ | ✓ | ✓ |
|
|
||||||
| **command** | ✓ (default) | ✓ | ✓ | ✓ |
|
|
||||||
| **script** | ✓ (default) | — | — | ✓ |
|
|
||||||
|
|
||||||
For artifacts and commands (which are LLM directives), `wrap` would inject preset content before and after the core template using a `{CORE_TEMPLATE}` placeholder. For scripts, `wrap` would run custom logic before/after the core script via a `$CORE_SCRIPT` variable.
|
|
||||||
- **Script overrides** — Enable presets to provide alternative versions of core scripts (e.g. `create-new-feature.sh`) for workflow customization. A `strategy: "wrap"` option could allow presets to run custom logic before/after the core script without fully replacing it.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": "1.0",
|
|
||||||
"updated_at": "2026-03-09T00:00:00Z",
|
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
|
|
||||||
"presets": {}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": "1.0",
|
|
||||||
"updated_at": "2026-03-10T00:00:00Z",
|
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.json",
|
|
||||||
"presets": {}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# My Preset
|
|
||||||
|
|
||||||
A custom preset for Spec Kit. Copy this directory and customize it to create your own.
|
|
||||||
|
|
||||||
## Templates Included
|
|
||||||
|
|
||||||
| Template | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `spec-template` | template | Custom feature specification template (overrides core and extensions) |
|
|
||||||
| `myext-template` | template | Override of the myext extension's report template |
|
|
||||||
| `speckit.specify` | command | Custom specification command (overrides core) |
|
|
||||||
| `speckit.myext.myextcmd` | command | Override of the myext extension's myextcmd command |
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
1. Copy this directory: `cp -r presets/scaffold my-preset`
|
|
||||||
2. Edit `preset.yml` — set your preset's ID, name, description, and templates
|
|
||||||
3. Add or modify templates in `templates/`
|
|
||||||
4. Test locally: `specify preset add --dev ./my-preset`
|
|
||||||
5. Verify resolution: `specify preset resolve spec-template`
|
|
||||||
6. Remove when done testing: `specify preset remove my-preset`
|
|
||||||
|
|
||||||
## Manifest Reference (`preset.yml`)
|
|
||||||
|
|
||||||
Required fields:
|
|
||||||
- `schema_version` — always `"1.0"`
|
|
||||||
- `preset.id` — lowercase alphanumeric with hyphens
|
|
||||||
- `preset.name` — human-readable name
|
|
||||||
- `preset.version` — semantic version (e.g. `1.0.0`)
|
|
||||||
- `preset.description` — brief description
|
|
||||||
- `requires.speckit_version` — version constraint (e.g. `>=0.1.0`)
|
|
||||||
- `provides.templates` — list of templates with `type`, `name`, and `file`
|
|
||||||
|
|
||||||
## Template Types
|
|
||||||
|
|
||||||
- **template** — Document scaffolds (spec-template.md, plan-template.md, tasks-template.md, etc.)
|
|
||||||
- **command** — AI agent workflow prompts (e.g. speckit.specify, speckit.plan)
|
|
||||||
- **script** — Custom scripts (reserved for future use)
|
|
||||||
|
|
||||||
## Publishing
|
|
||||||
|
|
||||||
See the [Preset Publishing Guide](../PUBLISHING.md) for details on submitting to the catalog.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Override of the myext extension's myextcmd command"
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- Preset override for speckit.myext.myextcmd -->
|
|
||||||
|
|
||||||
You are following a customized version of the myext extension's myextcmd command.
|
|
||||||
|
|
||||||
When executing this command:
|
|
||||||
|
|
||||||
1. Read the user's input from $ARGUMENTS
|
|
||||||
2. Follow the standard myextcmd workflow
|
|
||||||
3. Additionally, apply the following customizations from this preset:
|
|
||||||
- Add compliance checks before proceeding
|
|
||||||
- Include audit trail entries in the output
|
|
||||||
|
|
||||||
> CUSTOMIZE: Replace the instructions above with your own.
|
|
||||||
> This file overrides the command that the "myext" extension provides.
|
|
||||||
> When this preset is installed, all agents (Claude, Gemini, Copilot, etc.)
|
|
||||||
> will use this version instead of the extension's original.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Create a feature specification (preset override)"
|
|
||||||
scripts:
|
|
||||||
sh: scripts/bash/create-new-feature.sh "{ARGS}"
|
|
||||||
ps: scripts/powershell/create-new-feature.ps1 "{ARGS}"
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Input
|
|
||||||
|
|
||||||
```text
|
|
||||||
$ARGUMENTS
|
|
||||||
```
|
|
||||||
|
|
||||||
Given the feature description above:
|
|
||||||
|
|
||||||
1. **Create the feature branch** by running the script:
|
|
||||||
- Bash: `{SCRIPT} --json --short-name "<short-name>" "<description>"`
|
|
||||||
- The JSON output contains BRANCH_NAME and SPEC_FILE paths.
|
|
||||||
|
|
||||||
2. **Read the spec-template** to see the sections you need to fill.
|
|
||||||
|
|
||||||
3. **Write the specification** to SPEC_FILE, replacing the placeholders in each section
|
|
||||||
(Overview, Requirements, Acceptance Criteria) with details from the user's description.
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
schema_version: "1.0"
|
|
||||||
|
|
||||||
preset:
|
|
||||||
# CUSTOMIZE: Change 'my-preset' to your preset ID (lowercase, hyphen-separated)
|
|
||||||
id: "my-preset"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Human-readable name for your preset
|
|
||||||
name: "My Preset"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Update version when releasing (semantic versioning: X.Y.Z)
|
|
||||||
version: "1.0.0"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Brief description (under 200 characters)
|
|
||||||
description: "Brief description of what your preset provides"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Your name or organization name
|
|
||||||
author: "Your Name"
|
|
||||||
|
|
||||||
# CUSTOMIZE: GitHub repository URL (create before publishing)
|
|
||||||
repository: "https://github.com/your-org/spec-kit-preset-my-preset"
|
|
||||||
|
|
||||||
# REVIEW: License (MIT is recommended for open source)
|
|
||||||
license: "MIT"
|
|
||||||
|
|
||||||
# Requirements for this preset
|
|
||||||
requires:
|
|
||||||
# CUSTOMIZE: Minimum spec-kit version required
|
|
||||||
speckit_version: ">=0.1.0"
|
|
||||||
|
|
||||||
# Templates provided by this preset
|
|
||||||
provides:
|
|
||||||
templates:
|
|
||||||
# CUSTOMIZE: Define your template overrides
|
|
||||||
# Templates are document scaffolds (spec-template.md, plan-template.md, etc.)
|
|
||||||
- type: "template"
|
|
||||||
name: "spec-template"
|
|
||||||
file: "templates/spec-template.md"
|
|
||||||
description: "Custom feature specification template"
|
|
||||||
replaces: "spec-template" # Which core template this overrides (optional)
|
|
||||||
|
|
||||||
# ADD MORE TEMPLATES: Copy this block for each template
|
|
||||||
# - type: "template"
|
|
||||||
# name: "plan-template"
|
|
||||||
# file: "templates/plan-template.md"
|
|
||||||
# description: "Custom plan template"
|
|
||||||
# replaces: "plan-template"
|
|
||||||
|
|
||||||
# OVERRIDE EXTENSION TEMPLATES:
|
|
||||||
# Presets sit above extensions in the resolution stack, so you can
|
|
||||||
# override templates provided by any installed extension.
|
|
||||||
# For example, if the "myext" extension provides a spec-template,
|
|
||||||
# the preset's version above will take priority automatically.
|
|
||||||
|
|
||||||
# Override a template provided by the "myext" extension:
|
|
||||||
- type: "template"
|
|
||||||
name: "myext-template"
|
|
||||||
file: "templates/myext-template.md"
|
|
||||||
description: "Override myext's report template"
|
|
||||||
replaces: "myext-template"
|
|
||||||
|
|
||||||
# Command overrides (AI agent workflow prompts)
|
|
||||||
# Presets can override both core and extension commands.
|
|
||||||
# Commands are automatically registered into all detected agent
|
|
||||||
# directories (.claude/commands/, .gemini/commands/, etc.)
|
|
||||||
|
|
||||||
# Override a core command:
|
|
||||||
- type: "command"
|
|
||||||
name: "speckit.specify"
|
|
||||||
file: "commands/speckit.specify.md"
|
|
||||||
description: "Custom specification command"
|
|
||||||
replaces: "speckit.specify"
|
|
||||||
|
|
||||||
# Override an extension command (e.g. from the "myext" extension):
|
|
||||||
- type: "command"
|
|
||||||
name: "speckit.myext.myextcmd"
|
|
||||||
file: "commands/speckit.myext.myextcmd.md"
|
|
||||||
description: "Override myext's myextcmd command with custom workflow"
|
|
||||||
replaces: "speckit.myext.myextcmd"
|
|
||||||
|
|
||||||
# Script templates (reserved for future use)
|
|
||||||
# - type: "script"
|
|
||||||
# name: "create-new-feature"
|
|
||||||
# file: "scripts/bash/create-new-feature.sh"
|
|
||||||
# description: "Custom feature creation script"
|
|
||||||
# replaces: "create-new-feature"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Add relevant tags (2-5 recommended)
|
|
||||||
# Used for discovery in catalog
|
|
||||||
tags:
|
|
||||||
- "example"
|
|
||||||
- "preset"
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# MyExt Report
|
|
||||||
|
|
||||||
> This template overrides the one provided by the "myext" extension.
|
|
||||||
> Customize it to match your needs.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Brief summary of the report.
|
|
||||||
|
|
||||||
## Details
|
|
||||||
|
|
||||||
- Detail 1
|
|
||||||
- Detail 2
|
|
||||||
|
|
||||||
## Actions
|
|
||||||
|
|
||||||
- [ ] Action 1
|
|
||||||
- [ ] Action 2
|
|
||||||
|
|
||||||
<!--
|
|
||||||
CUSTOMIZE: This template takes priority over the myext extension's
|
|
||||||
version of myext-template. The extension's original is still available
|
|
||||||
if you remove this preset.
|
|
||||||
-->
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Feature Specification: [FEATURE NAME]
|
|
||||||
|
|
||||||
**Created**: [DATE]
|
|
||||||
**Status**: Draft
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
[Brief description of the feature]
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- [ ] Requirement 1
|
|
||||||
- [ ] Requirement 2
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [ ] Criterion 1
|
|
||||||
- [ ] Criterion 2
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Self-test override of the specify command"
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
You are following the self-test preset's version of the specify command.
|
|
||||||
|
|
||||||
When creating a specification, follow this process:
|
|
||||||
|
|
||||||
1. Read the user's requirements from $ARGUMENTS
|
|
||||||
2. Create a specification document using the spec-template
|
|
||||||
3. Include all standard sections plus the self-test marker
|
|
||||||
|
|
||||||
> This command is provided by the self-test preset.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
schema_version: "1.0"
|
|
||||||
|
|
||||||
preset:
|
|
||||||
id: "self-test"
|
|
||||||
name: "Self-Test Preset"
|
|
||||||
version: "1.0.0"
|
|
||||||
description: "A preset that overrides all core templates for testing purposes"
|
|
||||||
author: "github"
|
|
||||||
repository: "https://github.com/github/spec-kit"
|
|
||||||
license: "MIT"
|
|
||||||
|
|
||||||
requires:
|
|
||||||
speckit_version: ">=0.1.0"
|
|
||||||
|
|
||||||
provides:
|
|
||||||
templates:
|
|
||||||
- type: "template"
|
|
||||||
name: "spec-template"
|
|
||||||
file: "templates/spec-template.md"
|
|
||||||
description: "Self-test spec template"
|
|
||||||
replaces: "spec-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "plan-template"
|
|
||||||
file: "templates/plan-template.md"
|
|
||||||
description: "Self-test plan template"
|
|
||||||
replaces: "plan-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "tasks-template"
|
|
||||||
file: "templates/tasks-template.md"
|
|
||||||
description: "Self-test tasks template"
|
|
||||||
replaces: "tasks-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "checklist-template"
|
|
||||||
file: "templates/checklist-template.md"
|
|
||||||
description: "Self-test checklist template"
|
|
||||||
replaces: "checklist-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "constitution-template"
|
|
||||||
file: "templates/constitution-template.md"
|
|
||||||
description: "Self-test constitution template"
|
|
||||||
replaces: "constitution-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "agent-file-template"
|
|
||||||
file: "templates/agent-file-template.md"
|
|
||||||
description: "Self-test agent file template"
|
|
||||||
replaces: "agent-file-template"
|
|
||||||
|
|
||||||
- type: "command"
|
|
||||||
name: "speckit.specify"
|
|
||||||
file: "commands/speckit.specify.md"
|
|
||||||
description: "Self-test override of the specify command"
|
|
||||||
replaces: "speckit.specify"
|
|
||||||
|
|
||||||
tags:
|
|
||||||
- "testing"
|
|
||||||
- "self-test"
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Agent File (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Agent Instructions
|
|
||||||
|
|
||||||
Follow these guidelines when working on this project.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Checklist (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Pre-Implementation
|
|
||||||
|
|
||||||
- [ ] Spec reviewed
|
|
||||||
- [ ] Plan approved
|
|
||||||
|
|
||||||
## Post-Implementation
|
|
||||||
|
|
||||||
- [ ] Tests passing
|
|
||||||
- [ ] Documentation updated
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Constitution (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Principles
|
|
||||||
|
|
||||||
1. Principle 1
|
|
||||||
2. Principle 2
|
|
||||||
|
|
||||||
## Guidelines
|
|
||||||
|
|
||||||
- Guideline 1
|
|
||||||
- Guideline 2
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Implementation Plan (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Approach
|
|
||||||
|
|
||||||
Describe the implementation approach.
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
1. Step 1
|
|
||||||
2. Step 2
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Dependency 1
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
|
|
||||||
- Risk 1
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Feature Specification (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Brief description of the feature.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Requirement 1
|
|
||||||
- Requirement 2
|
|
||||||
|
|
||||||
## Design
|
|
||||||
|
|
||||||
Describe the design approach.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [ ] Criterion 1
|
|
||||||
- [ ] Criterion 2
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Tasks (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Task List
|
|
||||||
|
|
||||||
- [ ] Task 1
|
|
||||||
- [ ] Task 2
|
|
||||||
|
|
||||||
## Estimation
|
|
||||||
|
|
||||||
| Task | Estimate |
|
|
||||||
|------|----------|
|
|
||||||
| Task 1 | TBD |
|
|
||||||
| Task 2 | TBD |
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.3.1"
|
version = "0.1.13"
|
||||||
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,8 +13,6 @@ 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",
|
|
||||||
"json5>=0.13.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -79,28 +79,15 @@ 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
|
||||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
eval $(get_feature_paths)
|
||||||
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)
|
||||||
if has_jq; then
|
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
||||||
jq -cn \
|
"$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
|
||||||
--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"
|
||||||
@@ -154,25 +141,14 @@ fi
|
|||||||
# Output results
|
# Output results
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
# Build JSON array of documents
|
# Build JSON array of documents
|
||||||
if has_jq; then
|
if [[ ${#docs[@]} -eq 0 ]]; then
|
||||||
if [[ ${#docs[@]} -eq 0 ]]; then
|
json_docs="[]"
|
||||||
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
|
||||||
if [[ ${#docs[@]} -eq 0 ]]; then
|
json_docs=$(printf '"%s",' "${docs[@]}")
|
||||||
json_docs="[]"
|
json_docs="[${json_docs%,}]"
|
||||||
else
|
|
||||||
json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
|
|
||||||
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
|
||||||
return 1
|
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,130 +134,23 @@ 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
|
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
|
||||||
if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
|
|
||||||
echo "ERROR: Failed to resolve feature directory" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use printf '%q' to safely quote values, preventing shell injection
|
cat <<EOF
|
||||||
# via crafted branch names or paths containing special characters
|
REPO_ROOT='$repo_root'
|
||||||
printf 'REPO_ROOT=%q\n' "$repo_root"
|
CURRENT_BRANCH='$current_branch'
|
||||||
printf 'CURRENT_BRANCH=%q\n' "$current_branch"
|
HAS_GIT='$has_git_repo'
|
||||||
printf 'HAS_GIT=%q\n' "$has_git_repo"
|
FEATURE_DIR='$feature_dir'
|
||||||
printf 'FEATURE_DIR=%q\n' "$feature_dir"
|
FEATURE_SPEC='$feature_dir/spec.md'
|
||||||
printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
|
IMPL_PLAN='$feature_dir/plan.md'
|
||||||
printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
|
TASKS='$feature_dir/tasks.md'
|
||||||
printf 'TASKS=%q\n' "$feature_dir/tasks.md"
|
RESEARCH='$feature_dir/research.md'
|
||||||
printf 'RESEARCH=%q\n' "$feature_dir/research.md"
|
DATA_MODEL='$feature_dir/data-model.md'
|
||||||
printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
|
QUICKSTART='$feature_dir/quickstart.md'
|
||||||
printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
|
CONTRACTS_DIR='$feature_dir/contracts'
|
||||||
printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
|
EOF
|
||||||
}
|
|
||||||
|
|
||||||
# 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 JSON-required control character escapes (RFC 8259).
|
|
||||||
json_escape() {
|
|
||||||
local s="$1"
|
|
||||||
s="${s//\\/\\\\}"
|
|
||||||
s="${s//\"/\\\"}"
|
|
||||||
s="${s//$'\n'/\\n}"
|
|
||||||
s="${s//$'\t'/\\t}"
|
|
||||||
s="${s//$'\r'/\\r}"
|
|
||||||
s="${s//$'\b'/\\b}"
|
|
||||||
s="${s//$'\f'/\\f}"
|
|
||||||
# Strip remaining control characters (U+0000–U+001F) not individually escaped above
|
|
||||||
s=$(printf '%s' "$s" | tr -d '\000-\007\013\016-\037')
|
|
||||||
printf '%s' "$s"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||||
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||||
|
|
||||||
# Resolve a template name to a file path using the priority stack:
|
|
||||||
# 1. .specify/templates/overrides/
|
|
||||||
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
|
|
||||||
# 3. .specify/extensions/<ext-id>/templates/
|
|
||||||
# 4. .specify/templates/ (core)
|
|
||||||
resolve_template() {
|
|
||||||
local template_name="$1"
|
|
||||||
local repo_root="$2"
|
|
||||||
local base="$repo_root/.specify/templates"
|
|
||||||
|
|
||||||
# Priority 1: Project overrides
|
|
||||||
local override="$base/overrides/${template_name}.md"
|
|
||||||
[ -f "$override" ] && echo "$override" && return 0
|
|
||||||
|
|
||||||
# Priority 2: Installed presets (sorted by priority from .registry)
|
|
||||||
local presets_dir="$repo_root/.specify/presets"
|
|
||||||
if [ -d "$presets_dir" ]; then
|
|
||||||
local registry_file="$presets_dir/.registry"
|
|
||||||
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
|
|
||||||
# Read preset IDs sorted by priority (lower number = higher precedence).
|
|
||||||
# The python3 call is wrapped in an if-condition so that set -e does not
|
|
||||||
# abort the function when python3 exits non-zero (e.g. invalid JSON).
|
|
||||||
local sorted_presets=""
|
|
||||||
if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
|
|
||||||
import json, sys, os
|
|
||||||
try:
|
|
||||||
with open(os.environ['SPECKIT_REGISTRY']) as f:
|
|
||||||
data = json.load(f)
|
|
||||||
presets = data.get('presets', {})
|
|
||||||
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
|
|
||||||
print(pid)
|
|
||||||
except Exception:
|
|
||||||
sys.exit(1)
|
|
||||||
" 2>/dev/null); then
|
|
||||||
if [ -n "$sorted_presets" ]; then
|
|
||||||
# python3 succeeded and returned preset IDs — search in priority order
|
|
||||||
while IFS= read -r preset_id; do
|
|
||||||
local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
|
|
||||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
||||||
done <<< "$sorted_presets"
|
|
||||||
fi
|
|
||||||
# python3 succeeded but registry has no presets — nothing to search
|
|
||||||
else
|
|
||||||
# python3 failed (missing, or registry parse error) — fall back to unordered directory scan
|
|
||||||
for preset in "$presets_dir"/*/; do
|
|
||||||
[ -d "$preset" ] || continue
|
|
||||||
local candidate="$preset/templates/${template_name}.md"
|
|
||||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Fallback: alphabetical directory order (no python3 available)
|
|
||||||
for preset in "$presets_dir"/*/; do
|
|
||||||
[ -d "$preset" ] || continue
|
|
||||||
local candidate="$preset/templates/${template_name}.md"
|
|
||||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Priority 3: Extension-provided templates
|
|
||||||
local ext_dir="$repo_root/.specify/extensions"
|
|
||||||
if [ -d "$ext_dir" ]; then
|
|
||||||
for ext in "$ext_dir"/*/; do
|
|
||||||
[ -d "$ext" ] || continue
|
|
||||||
# Skip hidden directories (e.g. .backup, .cache)
|
|
||||||
case "$(basename "$ext")" in .*) continue;; esac
|
|
||||||
local candidate="$ext/templates/${template_name}.md"
|
|
||||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Priority 4: Core templates
|
|
||||||
local core="$base/${template_name}.md"
|
|
||||||
[ -f "$core" ] && echo "$core" && return 0
|
|
||||||
|
|
||||||
# Template not found in any location.
|
|
||||||
# Return 1 so callers can distinguish "not found" from "found".
|
|
||||||
# Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ check_existing_branches() {
|
|||||||
local specs_dir="$1"
|
local specs_dir="$1"
|
||||||
|
|
||||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
git fetch --all --prune >/dev/null 2>&1 || true
|
git fetch --all --prune 2>/dev/null || true
|
||||||
|
|
||||||
# Get highest number from ALL branches (not just matching short name)
|
# Get highest number from ALL branches (not just matching short name)
|
||||||
local highest_branch=$(get_highest_from_branches)
|
local highest_branch=$(get_highest_from_branches)
|
||||||
@@ -166,7 +166,6 @@ clean_branch_name() {
|
|||||||
# to searching for repository markers so the workflow still functions in repositories that
|
# to searching for repository markers so the workflow still functions in repositories that
|
||||||
# were initialised with --no-git.
|
# were initialised with --no-git.
|
||||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
source "$SCRIPT_DIR/common.sh"
|
|
||||||
|
|
||||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
@@ -297,31 +296,18 @@ fi
|
|||||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||||
mkdir -p "$FEATURE_DIR"
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|
||||||
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
|
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
|
||||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||||
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
|
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
||||||
cp "$TEMPLATE" "$SPEC_FILE"
|
|
||||||
else
|
|
||||||
echo "Warning: Spec template not found; created empty spec file" >&2
|
|
||||||
touch "$SPEC_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Inform the user how to persist the feature variable in their own shell
|
# Set the SPECIFY_FEATURE environment variable for the current session
|
||||||
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
|
export SPECIFY_FEATURE="$BRANCH_NAME"
|
||||||
|
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
if command -v jq >/dev/null 2>&1; then
|
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
||||||
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"
|
||||||
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
|
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ 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
|
||||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
eval $(get_feature_paths)
|
||||||
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
|
||||||
@@ -39,30 +37,20 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
|||||||
mkdir -p "$FEATURE_DIR"
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|
||||||
# Copy plan template if it exists
|
# Copy plan template if it exists
|
||||||
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true
|
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
|
||||||
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
|
if [[ -f "$TEMPLATE" ]]; then
|
||||||
cp "$TEMPLATE" "$IMPL_PLAN"
|
cp "$TEMPLATE" "$IMPL_PLAN"
|
||||||
echo "Copied plan template to $IMPL_PLAN"
|
echo "Copied plan template to $IMPL_PLAN"
|
||||||
else
|
else
|
||||||
echo "Warning: Plan template not found"
|
echo "Warning: Plan template not found at $TEMPLATE"
|
||||||
# Create a basic plan file if template doesn't exist
|
# Create a basic plan file if template doesn't exist
|
||||||
touch "$IMPL_PLAN"
|
touch "$IMPL_PLAN"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Output results
|
# Output results
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
if has_jq; then
|
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
||||||
jq -cn \
|
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
|
||||||
--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, Kimi Code, Antigravity or Generic
|
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Kiro CLI, or Antigravity
|
||||||
# - 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|kimi|trae|generic
|
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli
|
||||||
# Leave empty to update all existing agent files
|
# Leave empty to update all existing agent files
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -53,9 +53,7 @@ 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
|
||||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
eval $(get_feature_paths)
|
||||||
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:-}"
|
||||||
@@ -73,17 +71,11 @@ 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, Kiro CLI, and IBM Bob all share AGENTS.md — use AGENTS_FILE to avoid
|
AMP_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
# 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"
|
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="$AGENTS_FILE"
|
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
|
|
||||||
KIMI_FILE="$REPO_ROOT/KIMI.md"
|
|
||||||
TRAE_FILE="$REPO_ROOT/.trae/rules/AGENTS.md"
|
|
||||||
|
|
||||||
# Template file
|
# Template file
|
||||||
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
||||||
@@ -117,8 +109,6 @@ 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
|
||||||
@@ -483,7 +473,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"
|
||||||
@@ -614,144 +604,158 @@ update_specific_agent() {
|
|||||||
|
|
||||||
case "$agent_type" in
|
case "$agent_type" in
|
||||||
claude)
|
claude)
|
||||||
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
|
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||||
;;
|
;;
|
||||||
gemini)
|
gemini)
|
||||||
update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1
|
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
||||||
;;
|
;;
|
||||||
copilot)
|
copilot)
|
||||||
update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1
|
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
||||||
;;
|
;;
|
||||||
cursor-agent)
|
cursor-agent)
|
||||||
update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1
|
update_agent_file "$CURSOR_FILE" "Cursor IDE"
|
||||||
;;
|
;;
|
||||||
qwen)
|
qwen)
|
||||||
update_agent_file "$QWEN_FILE" "Qwen Code" || return 1
|
update_agent_file "$QWEN_FILE" "Qwen Code"
|
||||||
;;
|
;;
|
||||||
opencode)
|
opencode)
|
||||||
update_agent_file "$AGENTS_FILE" "opencode" || return 1
|
update_agent_file "$AGENTS_FILE" "opencode"
|
||||||
;;
|
;;
|
||||||
codex)
|
codex)
|
||||||
update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1
|
update_agent_file "$AGENTS_FILE" "Codex CLI"
|
||||||
;;
|
;;
|
||||||
windsurf)
|
windsurf)
|
||||||
update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1
|
update_agent_file "$WINDSURF_FILE" "Windsurf"
|
||||||
;;
|
;;
|
||||||
kilocode)
|
kilocode)
|
||||||
update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1
|
update_agent_file "$KILOCODE_FILE" "Kilo Code"
|
||||||
;;
|
;;
|
||||||
auggie)
|
auggie)
|
||||||
update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1
|
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
|
||||||
;;
|
;;
|
||||||
roo)
|
roo)
|
||||||
update_agent_file "$ROO_FILE" "Roo Code" || return 1
|
update_agent_file "$ROO_FILE" "Roo Code"
|
||||||
;;
|
;;
|
||||||
codebuddy)
|
codebuddy)
|
||||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1
|
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||||
;;
|
;;
|
||||||
qodercli)
|
qodercli)
|
||||||
update_agent_file "$QODER_FILE" "Qoder CLI" || return 1
|
update_agent_file "$QODER_FILE" "Qoder CLI"
|
||||||
;;
|
;;
|
||||||
amp)
|
amp)
|
||||||
update_agent_file "$AMP_FILE" "Amp" || return 1
|
update_agent_file "$AMP_FILE" "Amp"
|
||||||
;;
|
;;
|
||||||
shai)
|
shai)
|
||||||
update_agent_file "$SHAI_FILE" "SHAI" || return 1
|
update_agent_file "$SHAI_FILE" "SHAI"
|
||||||
;;
|
|
||||||
tabnine)
|
|
||||||
update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1
|
|
||||||
;;
|
;;
|
||||||
kiro-cli)
|
kiro-cli)
|
||||||
update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1
|
update_agent_file "$KIRO_FILE" "Kiro CLI"
|
||||||
;;
|
;;
|
||||||
agy)
|
agy)
|
||||||
update_agent_file "$AGY_FILE" "Antigravity" || return 1
|
update_agent_file "$AGY_FILE" "Antigravity"
|
||||||
;;
|
;;
|
||||||
bob)
|
bob)
|
||||||
update_agent_file "$BOB_FILE" "IBM Bob" || return 1
|
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||||
;;
|
|
||||||
vibe)
|
|
||||||
update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1
|
|
||||||
;;
|
|
||||||
kimi)
|
|
||||||
update_agent_file "$KIMI_FILE" "Kimi Code" || return 1
|
|
||||||
;;
|
|
||||||
trae)
|
|
||||||
update_agent_file "$TRAE_FILE" "Trae" || 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|kimi|trae|generic"
|
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: skip non-existent files and files already updated (dedup by
|
|
||||||
# realpath so that variables pointing to the same file — e.g. AMP_FILE,
|
|
||||||
# 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.
|
|
||||||
# Note: defined at top level because bash 3.2 does not support true
|
|
||||||
# nested/local functions. _updated_paths, _found_agent, and _all_ok are
|
|
||||||
# initialised exclusively inside update_all_existing_agents so that
|
|
||||||
# sourcing this script has no side effects on the caller's environment.
|
|
||||||
|
|
||||||
_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
|
|
||||||
# Record the file as seen before attempting the update so that:
|
|
||||||
# (a) aliases pointing to the same path are not retried on failure
|
|
||||||
# (b) _found_agent reflects file existence, not update success
|
|
||||||
_updated_paths+=("$real_path")
|
|
||||||
_found_agent=true
|
|
||||||
update_agent_file "$file" "$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
update_all_existing_agents() {
|
update_all_existing_agents() {
|
||||||
_found_agent=false
|
local found_agent=false
|
||||||
_updated_paths=()
|
|
||||||
local _all_ok=true
|
# Check each possible agent file and update if it exists
|
||||||
|
if [[ -f "$CLAUDE_FILE" ]]; then
|
||||||
_update_if_new "$CLAUDE_FILE" "Claude Code" || _all_ok=false
|
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||||
_update_if_new "$GEMINI_FILE" "Gemini CLI" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false
|
fi
|
||||||
_update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false
|
|
||||||
_update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false
|
if [[ -f "$GEMINI_FILE" ]]; then
|
||||||
_update_if_new "$AGENTS_FILE" "Codex/opencode" || _all_ok=false
|
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
||||||
_update_if_new "$AMP_FILE" "Amp" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$KIRO_FILE" "Kiro CLI" || _all_ok=false
|
fi
|
||||||
_update_if_new "$BOB_FILE" "IBM Bob" || _all_ok=false
|
|
||||||
_update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false
|
if [[ -f "$COPILOT_FILE" ]]; then
|
||||||
_update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false
|
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
||||||
_update_if_new "$AUGGIE_FILE" "Auggie CLI" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$ROO_FILE" "Roo Code" || _all_ok=false
|
fi
|
||||||
_update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI" || _all_ok=false
|
|
||||||
_update_if_new "$SHAI_FILE" "SHAI" || _all_ok=false
|
if [[ -f "$CURSOR_FILE" ]]; then
|
||||||
_update_if_new "$TABNINE_FILE" "Tabnine CLI" || _all_ok=false
|
update_agent_file "$CURSOR_FILE" "Cursor IDE"
|
||||||
_update_if_new "$QODER_FILE" "Qoder CLI" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$AGY_FILE" "Antigravity" || _all_ok=false
|
fi
|
||||||
_update_if_new "$VIBE_FILE" "Mistral Vibe" || _all_ok=false
|
|
||||||
_update_if_new "$KIMI_FILE" "Kimi Code" || _all_ok=false
|
if [[ -f "$QWEN_FILE" ]]; then
|
||||||
_update_if_new "$TRAE_FILE" "Trae" || _all_ok=false
|
update_agent_file "$QWEN_FILE" "Qwen Code"
|
||||||
|
found_agent=true
|
||||||
# If no agent files exist, create a default Claude file
|
fi
|
||||||
if [[ "$_found_agent" == false ]]; then
|
|
||||||
log_info "No existing agent files found, creating default Claude file..."
|
if [[ -f "$AGENTS_FILE" ]]; then
|
||||||
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
|
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
|
fi
|
||||||
|
|
||||||
[[ "$_all_ok" == true ]]
|
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 "$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 no agent files exist, create a default Claude file
|
||||||
|
if [[ "$found_agent" == false ]]; then
|
||||||
|
log_info "No existing agent files found, creating default Claude file..."
|
||||||
|
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
print_summary() {
|
print_summary() {
|
||||||
echo
|
echo
|
||||||
@@ -770,7 +774,8 @@ 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|kimi|trae|generic]"
|
|
||||||
|
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli]"
|
||||||
}
|
}
|
||||||
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|||||||
@@ -135,70 +135,3 @@ function Test-DirHasFiles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve a template name to a file path using the priority stack:
|
|
||||||
# 1. .specify/templates/overrides/
|
|
||||||
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
|
|
||||||
# 3. .specify/extensions/<ext-id>/templates/
|
|
||||||
# 4. .specify/templates/ (core)
|
|
||||||
function Resolve-Template {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)][string]$TemplateName,
|
|
||||||
[Parameter(Mandatory=$true)][string]$RepoRoot
|
|
||||||
)
|
|
||||||
|
|
||||||
$base = Join-Path $RepoRoot '.specify/templates'
|
|
||||||
|
|
||||||
# Priority 1: Project overrides
|
|
||||||
$override = Join-Path $base "overrides/$TemplateName.md"
|
|
||||||
if (Test-Path $override) { return $override }
|
|
||||||
|
|
||||||
# Priority 2: Installed presets (sorted by priority from .registry)
|
|
||||||
$presetsDir = Join-Path $RepoRoot '.specify/presets'
|
|
||||||
if (Test-Path $presetsDir) {
|
|
||||||
$registryFile = Join-Path $presetsDir '.registry'
|
|
||||||
$sortedPresets = @()
|
|
||||||
if (Test-Path $registryFile) {
|
|
||||||
try {
|
|
||||||
$registryData = Get-Content $registryFile -Raw | ConvertFrom-Json
|
|
||||||
$presets = $registryData.presets
|
|
||||||
if ($presets) {
|
|
||||||
$sortedPresets = $presets.PSObject.Properties |
|
|
||||||
Sort-Object { if ($null -ne $_.Value.priority) { $_.Value.priority } else { 10 } } |
|
|
||||||
ForEach-Object { $_.Name }
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
# Fallback: alphabetical directory order
|
|
||||||
$sortedPresets = @()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sortedPresets.Count -gt 0) {
|
|
||||||
foreach ($presetId in $sortedPresets) {
|
|
||||||
$candidate = Join-Path $presetsDir "$presetId/templates/$TemplateName.md"
|
|
||||||
if (Test-Path $candidate) { return $candidate }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# Fallback: alphabetical directory order
|
|
||||||
foreach ($preset in Get-ChildItem -Path $presetsDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' }) {
|
|
||||||
$candidate = Join-Path $preset.FullName "templates/$TemplateName.md"
|
|
||||||
if (Test-Path $candidate) { return $candidate }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Priority 3: Extension-provided templates
|
|
||||||
$extDir = Join-Path $RepoRoot '.specify/extensions'
|
|
||||||
if (Test-Path $extDir) {
|
|
||||||
foreach ($ext in Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' } | Sort-Object Name) {
|
|
||||||
$candidate = Join-Path $ext.FullName "templates/$TemplateName.md"
|
|
||||||
if (Test-Path $candidate) { return $candidate }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Priority 4: Core templates
|
|
||||||
$core = Join-Path $base "$TemplateName.md"
|
|
||||||
if (Test-Path $core) { return $core }
|
|
||||||
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -141,9 +141,6 @@ if (-not $fallbackRoot) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Load common functions (includes Resolve-Template)
|
|
||||||
. "$PSScriptRoot/common.ps1"
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$repoRoot = git rev-parse --show-toplevel 2>$null
|
$repoRoot = git rev-parse --show-toplevel 2>$null
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
@@ -253,7 +250,7 @@ if ($branchName.Length -gt $maxBranchLength) {
|
|||||||
if ($hasGit) {
|
if ($hasGit) {
|
||||||
$branchCreated = $false
|
$branchCreated = $false
|
||||||
try {
|
try {
|
||||||
git checkout -q -b $branchName 2>$null | Out-Null
|
git checkout -b $branchName 2>$null | Out-Null
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
$branchCreated = $true
|
$branchCreated = $true
|
||||||
}
|
}
|
||||||
@@ -279,9 +276,9 @@ if ($hasGit) {
|
|||||||
$featureDir = Join-Path $specsDir $branchName
|
$featureDir = Join-Path $specsDir $branchName
|
||||||
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
|
||||||
|
|
||||||
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
|
$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
|
||||||
$specFile = Join-Path $featureDir 'spec.md'
|
$specFile = Join-Path $featureDir 'spec.md'
|
||||||
if ($template -and (Test-Path $template)) {
|
if (Test-Path $template) {
|
||||||
Copy-Item $template $specFile -Force
|
Copy-Item $template $specFile -Force
|
||||||
} else {
|
} else {
|
||||||
New-Item -ItemType File -Path $specFile | Out-Null
|
New-Item -ItemType File -Path $specFile | Out-Null
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GI
|
|||||||
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
|
||||||
|
|
||||||
# Copy plan template if it exists, otherwise note it or create empty file
|
# Copy plan template if it exists, otherwise note it or create empty file
|
||||||
$template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT
|
$template = Join-Path $paths.REPO_ROOT '.specify/templates/plan-template.md'
|
||||||
if ($template -and (Test-Path $template)) {
|
if (Test-Path $template) {
|
||||||
Copy-Item $template $paths.IMPL_PLAN -Force
|
Copy-Item $template $paths.IMPL_PLAN -Force
|
||||||
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
|
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
|
||||||
} else {
|
} else {
|
||||||
Write-Warning "Plan template not found"
|
Write-Warning "Plan template not found at $template"
|
||||||
# Create a basic plan file if template doesn't exist
|
# Create a basic plan file if template doesn't exist
|
||||||
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
|
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, kimi, trae, generic)
|
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, kiro-cli, agy, bob, qodercli)
|
||||||
|
|
||||||
.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','kimi','trae','generic')]
|
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','kiro-cli','agy','bob','qodercli','generic')]
|
||||||
[string]$AgentType
|
[string]$AgentType
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,13 +58,9 @@ $CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md'
|
|||||||
$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
|
$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
|
||||||
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
|
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
|
||||||
$TABNINE_FILE = Join-Path $REPO_ROOT 'TABNINE.md'
|
|
||||||
$KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$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'
|
|
||||||
$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md'
|
|
||||||
$TRAE_FILE = Join-Path $REPO_ROOT '.trae/rules/AGENTS.md'
|
|
||||||
|
|
||||||
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
||||||
|
|
||||||
@@ -332,7 +328,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
|
||||||
}
|
}
|
||||||
@@ -403,15 +399,11 @@ function Update-SpecificAgent {
|
|||||||
'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
|
'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
|
||||||
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
|
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
|
||||||
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
|
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
|
||||||
'tabnine' { Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI' }
|
|
||||||
'kiro-cli' { Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI' }
|
'kiro-cli' { Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI' }
|
||||||
'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' }
|
|
||||||
'kimi' { Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code' }
|
|
||||||
'trae' { Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae' }
|
|
||||||
'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|kimi|trae|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|kiro-cli|agy|bob|qodercli|generic'; return $false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,13 +423,9 @@ function Update-AllExistingAgents {
|
|||||||
if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
|
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $TABNINE_FILE) { if (-not (Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI')) { $ok = $false }; $found = $true }
|
|
||||||
if (Test-Path $KIRO_FILE) { if (-not (Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $KIRO_FILE) { if (-not (Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true }
|
if (Test-Path $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 $KIMI_FILE) { if (-not (Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false }; $found = $true }
|
|
||||||
if (Test-Path $TRAE_FILE) { if (-not (Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae')) { $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 }
|
||||||
@@ -452,7 +440,7 @@ function Print-Summary {
|
|||||||
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
|
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
|
||||||
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
|
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic]'
|
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic]'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Main {
|
function Main {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,428 +0,0 @@
|
|||||||
"""
|
|
||||||
Agent Command Registrar for Spec Kit
|
|
||||||
|
|
||||||
Shared infrastructure for registering commands with AI agents.
|
|
||||||
Used by both the extension system and the preset system to write
|
|
||||||
command files into agent-specific directories in the correct format.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
|
|
||||||
class CommandRegistrar:
|
|
||||||
"""Handles registration of commands with AI agents.
|
|
||||||
|
|
||||||
Supports writing command files in Markdown or TOML format to the
|
|
||||||
appropriate agent directory, with correct argument placeholders
|
|
||||||
and companion files (e.g. Copilot .prompt.md).
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Agent configurations with directory, format, and argument placeholder
|
|
||||||
AGENT_CONFIGS = {
|
|
||||||
"claude": {
|
|
||||||
"dir": ".claude/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"gemini": {
|
|
||||||
"dir": ".gemini/commands",
|
|
||||||
"format": "toml",
|
|
||||||
"args": "{{args}}",
|
|
||||||
"extension": ".toml"
|
|
||||||
},
|
|
||||||
"copilot": {
|
|
||||||
"dir": ".github/agents",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".agent.md"
|
|
||||||
},
|
|
||||||
"cursor": {
|
|
||||||
"dir": ".cursor/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"qwen": {
|
|
||||||
"dir": ".qwen/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"opencode": {
|
|
||||||
"dir": ".opencode/command",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"codex": {
|
|
||||||
"dir": ".codex/prompts",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"windsurf": {
|
|
||||||
"dir": ".windsurf/workflows",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"kilocode": {
|
|
||||||
"dir": ".kilocode/workflows",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"auggie": {
|
|
||||||
"dir": ".augment/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"roo": {
|
|
||||||
"dir": ".roo/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"codebuddy": {
|
|
||||||
"dir": ".codebuddy/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"qodercli": {
|
|
||||||
"dir": ".qoder/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"kiro-cli": {
|
|
||||||
"dir": ".kiro/prompts",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"amp": {
|
|
||||||
"dir": ".agents/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"shai": {
|
|
||||||
"dir": ".shai/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"tabnine": {
|
|
||||||
"dir": ".tabnine/agent/commands",
|
|
||||||
"format": "toml",
|
|
||||||
"args": "{{args}}",
|
|
||||||
"extension": ".toml"
|
|
||||||
},
|
|
||||||
"bob": {
|
|
||||||
"dir": ".bob/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"kimi": {
|
|
||||||
"dir": ".kimi/skills",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": "/SKILL.md"
|
|
||||||
},
|
|
||||||
"trae": {
|
|
||||||
"dir": ".trae/rules",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_frontmatter(content: str) -> tuple[dict, str]:
|
|
||||||
"""Parse YAML frontmatter from Markdown content.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: Markdown content with YAML frontmatter
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (frontmatter_dict, body_content)
|
|
||||||
"""
|
|
||||||
if not content.startswith("---"):
|
|
||||||
return {}, content
|
|
||||||
|
|
||||||
# Find second ---
|
|
||||||
end_marker = content.find("---", 3)
|
|
||||||
if end_marker == -1:
|
|
||||||
return {}, content
|
|
||||||
|
|
||||||
frontmatter_str = content[3:end_marker].strip()
|
|
||||||
body = content[end_marker + 3:].strip()
|
|
||||||
|
|
||||||
try:
|
|
||||||
frontmatter = yaml.safe_load(frontmatter_str) or {}
|
|
||||||
except yaml.YAMLError:
|
|
||||||
frontmatter = {}
|
|
||||||
|
|
||||||
return frontmatter, body
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def render_frontmatter(fm: dict) -> str:
|
|
||||||
"""Render frontmatter dictionary as YAML.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
fm: Frontmatter dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
YAML-formatted frontmatter with delimiters
|
|
||||||
"""
|
|
||||||
if not fm:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False)
|
|
||||||
return f"---\n{yaml_str}---\n"
|
|
||||||
|
|
||||||
def _adjust_script_paths(self, frontmatter: dict) -> dict:
|
|
||||||
"""Adjust script paths from extension-relative to repo-relative.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frontmatter: Frontmatter dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Modified frontmatter with adjusted paths
|
|
||||||
"""
|
|
||||||
if "scripts" in frontmatter:
|
|
||||||
for key in frontmatter["scripts"]:
|
|
||||||
script_path = frontmatter["scripts"][key]
|
|
||||||
if script_path.startswith("../../scripts/"):
|
|
||||||
frontmatter["scripts"][key] = f".specify/scripts/{script_path[14:]}"
|
|
||||||
return frontmatter
|
|
||||||
|
|
||||||
def render_markdown_command(
|
|
||||||
self,
|
|
||||||
frontmatter: dict,
|
|
||||||
body: str,
|
|
||||||
source_id: str,
|
|
||||||
context_note: str = None
|
|
||||||
) -> str:
|
|
||||||
"""Render command in Markdown format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frontmatter: Command frontmatter
|
|
||||||
body: Command body content
|
|
||||||
source_id: Source identifier (extension or preset ID)
|
|
||||||
context_note: Custom context comment (default: <!-- Source: {source_id} -->)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Formatted Markdown command file content
|
|
||||||
"""
|
|
||||||
if context_note is None:
|
|
||||||
context_note = f"\n<!-- Source: {source_id} -->\n"
|
|
||||||
return self.render_frontmatter(frontmatter) + "\n" + context_note + body
|
|
||||||
|
|
||||||
def render_toml_command(
|
|
||||||
self,
|
|
||||||
frontmatter: dict,
|
|
||||||
body: str,
|
|
||||||
source_id: str
|
|
||||||
) -> str:
|
|
||||||
"""Render command in TOML format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frontmatter: Command frontmatter
|
|
||||||
body: Command body content
|
|
||||||
source_id: Source identifier (extension or preset ID)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Formatted TOML command file content
|
|
||||||
"""
|
|
||||||
toml_lines = []
|
|
||||||
|
|
||||||
if "description" in frontmatter:
|
|
||||||
desc = frontmatter["description"].replace('"', '\\"')
|
|
||||||
toml_lines.append(f'description = "{desc}"')
|
|
||||||
toml_lines.append("")
|
|
||||||
|
|
||||||
toml_lines.append(f"# Source: {source_id}")
|
|
||||||
toml_lines.append("")
|
|
||||||
|
|
||||||
toml_lines.append('prompt = """')
|
|
||||||
toml_lines.append(body)
|
|
||||||
toml_lines.append('"""')
|
|
||||||
|
|
||||||
return "\n".join(toml_lines)
|
|
||||||
|
|
||||||
def _convert_argument_placeholder(self, content: str, from_placeholder: str, to_placeholder: str) -> str:
|
|
||||||
"""Convert argument placeholder format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: Command content
|
|
||||||
from_placeholder: Source placeholder (e.g., "$ARGUMENTS")
|
|
||||||
to_placeholder: Target placeholder (e.g., "{{args}}")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Content with converted placeholders
|
|
||||||
"""
|
|
||||||
return content.replace(from_placeholder, to_placeholder)
|
|
||||||
|
|
||||||
def register_commands(
|
|
||||||
self,
|
|
||||||
agent_name: str,
|
|
||||||
commands: List[Dict[str, Any]],
|
|
||||||
source_id: str,
|
|
||||||
source_dir: Path,
|
|
||||||
project_root: Path,
|
|
||||||
context_note: str = None
|
|
||||||
) -> List[str]:
|
|
||||||
"""Register commands for a specific agent.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
agent_name: Agent name (claude, gemini, copilot, etc.)
|
|
||||||
commands: List of command info dicts with 'name', 'file', and optional 'aliases'
|
|
||||||
source_id: Identifier of the source (extension or preset ID)
|
|
||||||
source_dir: Directory containing command source files
|
|
||||||
project_root: Path to project root
|
|
||||||
context_note: Custom context comment for markdown output
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of registered command names
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If agent is not supported
|
|
||||||
"""
|
|
||||||
if agent_name not in self.AGENT_CONFIGS:
|
|
||||||
raise ValueError(f"Unsupported agent: {agent_name}")
|
|
||||||
|
|
||||||
agent_config = self.AGENT_CONFIGS[agent_name]
|
|
||||||
commands_dir = project_root / agent_config["dir"]
|
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
registered = []
|
|
||||||
|
|
||||||
for cmd_info in commands:
|
|
||||||
cmd_name = cmd_info["name"]
|
|
||||||
cmd_file = cmd_info["file"]
|
|
||||||
|
|
||||||
source_file = source_dir / cmd_file
|
|
||||||
if not source_file.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
content = source_file.read_text(encoding="utf-8")
|
|
||||||
frontmatter, body = self.parse_frontmatter(content)
|
|
||||||
|
|
||||||
frontmatter = self._adjust_script_paths(frontmatter)
|
|
||||||
|
|
||||||
body = self._convert_argument_placeholder(
|
|
||||||
body, "$ARGUMENTS", agent_config["args"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if agent_config["format"] == "markdown":
|
|
||||||
output = self.render_markdown_command(frontmatter, body, source_id, context_note)
|
|
||||||
elif agent_config["format"] == "toml":
|
|
||||||
output = self.render_toml_command(frontmatter, body, source_id)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported format: {agent_config['format']}")
|
|
||||||
|
|
||||||
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
|
|
||||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
dest_file.write_text(output, encoding="utf-8")
|
|
||||||
|
|
||||||
if agent_name == "copilot":
|
|
||||||
self.write_copilot_prompt(project_root, cmd_name)
|
|
||||||
|
|
||||||
registered.append(cmd_name)
|
|
||||||
|
|
||||||
for alias in cmd_info.get("aliases", []):
|
|
||||||
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
|
|
||||||
alias_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
alias_file.write_text(output, encoding="utf-8")
|
|
||||||
if agent_name == "copilot":
|
|
||||||
self.write_copilot_prompt(project_root, alias)
|
|
||||||
registered.append(alias)
|
|
||||||
|
|
||||||
return registered
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def write_copilot_prompt(project_root: Path, cmd_name: str) -> None:
|
|
||||||
"""Generate a companion .prompt.md file for a Copilot agent command.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_root: Path to project root
|
|
||||||
cmd_name: Command name (e.g. 'speckit.my-ext.example')
|
|
||||||
"""
|
|
||||||
prompts_dir = project_root / ".github" / "prompts"
|
|
||||||
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
|
|
||||||
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n", encoding="utf-8")
|
|
||||||
|
|
||||||
def register_commands_for_all_agents(
|
|
||||||
self,
|
|
||||||
commands: List[Dict[str, Any]],
|
|
||||||
source_id: str,
|
|
||||||
source_dir: Path,
|
|
||||||
project_root: Path,
|
|
||||||
context_note: str = None
|
|
||||||
) -> Dict[str, List[str]]:
|
|
||||||
"""Register commands for all detected agents in the project.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
commands: List of command info dicts
|
|
||||||
source_id: Identifier of the source (extension or preset ID)
|
|
||||||
source_dir: Directory containing command source files
|
|
||||||
project_root: Path to project root
|
|
||||||
context_note: Custom context comment for markdown output
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary mapping agent names to list of registered commands
|
|
||||||
"""
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
for agent_name, agent_config in self.AGENT_CONFIGS.items():
|
|
||||||
agent_dir = project_root / agent_config["dir"].split("/")[0]
|
|
||||||
|
|
||||||
if agent_dir.exists():
|
|
||||||
try:
|
|
||||||
registered = self.register_commands(
|
|
||||||
agent_name, commands, source_id, source_dir, project_root,
|
|
||||||
context_note=context_note
|
|
||||||
)
|
|
||||||
if registered:
|
|
||||||
results[agent_name] = registered
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def unregister_commands(
|
|
||||||
self,
|
|
||||||
registered_commands: Dict[str, List[str]],
|
|
||||||
project_root: Path
|
|
||||||
) -> None:
|
|
||||||
"""Remove previously registered command files from agent directories.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
registered_commands: Dict mapping agent names to command name lists
|
|
||||||
project_root: Path to project root
|
|
||||||
"""
|
|
||||||
for agent_name, cmd_names in registered_commands.items():
|
|
||||||
if agent_name not in self.AGENT_CONFIGS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
agent_config = self.AGENT_CONFIGS[agent_name]
|
|
||||||
commands_dir = project_root / agent_config["dir"]
|
|
||||||
|
|
||||||
for cmd_name in cmd_names:
|
|
||||||
cmd_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
|
|
||||||
if cmd_file.exists():
|
|
||||||
cmd_file.unlink()
|
|
||||||
|
|
||||||
if agent_name == "copilot":
|
|
||||||
prompt_file = project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
|
|
||||||
if prompt_file.exists():
|
|
||||||
prompt_file.unlink()
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,40 +13,6 @@ $ARGUMENTS
|
|||||||
|
|
||||||
You **MUST** consider the user input before proceeding (if not empty).
|
You **MUST** consider the user input before proceeding (if not empty).
|
||||||
|
|
||||||
## Pre-Execution Checks
|
|
||||||
|
|
||||||
**Check for extension hooks (before implementation)**:
|
|
||||||
- Check if `.specify/extensions.yml` exists in the project root.
|
|
||||||
- If it exists, read it and look for entries under the `hooks.before_implement` key
|
|
||||||
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
|
|
||||||
- Filter to only hooks where `enabled: true`
|
|
||||||
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
|
|
||||||
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
|
|
||||||
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
|
|
||||||
- For each executable hook, output the following based on its `optional` flag:
|
|
||||||
- **Optional hook** (`optional: true`):
|
|
||||||
```
|
|
||||||
## Extension Hooks
|
|
||||||
|
|
||||||
**Optional Pre-Hook**: {extension}
|
|
||||||
Command: `/{command}`
|
|
||||||
Description: {description}
|
|
||||||
|
|
||||||
Prompt: {prompt}
|
|
||||||
To execute: `/{command}`
|
|
||||||
```
|
|
||||||
- **Mandatory hook** (`optional: false`):
|
|
||||||
```
|
|
||||||
## Extension Hooks
|
|
||||||
|
|
||||||
**Automatic Pre-Hook**: {extension}
|
|
||||||
Executing: `/{command}`
|
|
||||||
EXECUTE_COMMAND: {command}
|
|
||||||
|
|
||||||
Wait for the result of the hook command before proceeding to the Outline.
|
|
||||||
```
|
|
||||||
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
|
|
||||||
|
|
||||||
## Outline
|
## Outline
|
||||||
|
|
||||||
1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||||
@@ -122,7 +88,7 @@ You **MUST** consider the user input before proceeding (if not empty).
|
|||||||
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
|
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
|
||||||
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
|
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
|
||||||
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
|
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
|
||||||
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `*.dll`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*`
|
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*`
|
||||||
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
|
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
|
||||||
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
|
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
|
||||||
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
|
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
|
||||||
@@ -170,32 +136,3 @@ You **MUST** consider the user input before proceeding (if not empty).
|
|||||||
- Report final status with summary of completed work
|
- Report final status with summary of completed work
|
||||||
|
|
||||||
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
|
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
|
||||||
|
|
||||||
10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root.
|
|
||||||
- If it exists, read it and look for entries under the `hooks.after_implement` key
|
|
||||||
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
|
|
||||||
- Filter to only hooks where `enabled: true`
|
|
||||||
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
|
|
||||||
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
|
|
||||||
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
|
|
||||||
- For each executable hook, output the following based on its `optional` flag:
|
|
||||||
- **Optional hook** (`optional: true`):
|
|
||||||
```
|
|
||||||
## Extension Hooks
|
|
||||||
|
|
||||||
**Optional Hook**: {extension}
|
|
||||||
Command: `/{command}`
|
|
||||||
Description: {description}
|
|
||||||
|
|
||||||
Prompt: {prompt}
|
|
||||||
To execute: `/{command}`
|
|
||||||
```
|
|
||||||
- **Mandatory hook** (`optional: false`):
|
|
||||||
```
|
|
||||||
## Extension Hooks
|
|
||||||
|
|
||||||
**Automatic Hook**: {extension}
|
|
||||||
Executing: `/{command}`
|
|
||||||
EXECUTE_COMMAND: {command}
|
|
||||||
```
|
|
||||||
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ handoffs:
|
|||||||
prompt: Clarify specification requirements
|
prompt: Clarify specification requirements
|
||||||
send: true
|
send: true
|
||||||
scripts:
|
scripts:
|
||||||
sh: scripts/bash/create-new-feature.sh "{ARGS}"
|
sh: scripts/bash/create-new-feature.sh --json "{ARGS}"
|
||||||
ps: scripts/powershell/create-new-feature.ps1 "{ARGS}"
|
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
|
||||||
---
|
---
|
||||||
|
|
||||||
## User Input
|
## User Input
|
||||||
@@ -39,14 +39,33 @@ Given that feature description, do this:
|
|||||||
- "Create a dashboard for analytics" → "analytics-dashboard"
|
- "Create a dashboard for analytics" → "analytics-dashboard"
|
||||||
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
||||||
|
|
||||||
2. **Create the feature branch** by running the script with `--short-name` (and `--json`), and do NOT pass `--number` (the script auto-detects the next globally available number across all branches and spec directories):
|
2. **Check for existing branches before creating new one**:
|
||||||
|
|
||||||
- Bash example: `{SCRIPT} --json --short-name "user-auth" "Add user authentication"`
|
a. First, fetch all remote branches to ensure we have the latest information:
|
||||||
- PowerShell example: `{SCRIPT} -Json -ShortName "user-auth" "Add user authentication"`
|
|
||||||
|
```bash
|
||||||
|
git fetch --all --prune
|
||||||
|
```
|
||||||
|
|
||||||
|
b. Find the highest feature number across all sources for the short-name:
|
||||||
|
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
|
||||||
|
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
|
||||||
|
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
|
||||||
|
|
||||||
|
c. Determine the next available number:
|
||||||
|
- Extract all numbers from all three sources
|
||||||
|
- Find the highest number N
|
||||||
|
- Use N+1 for the new branch number
|
||||||
|
|
||||||
|
d. Run the script `{SCRIPT}` with the calculated number and short-name:
|
||||||
|
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
|
||||||
|
- Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"`
|
||||||
|
- PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
|
||||||
|
|
||||||
**IMPORTANT**:
|
**IMPORTANT**:
|
||||||
- Do NOT pass `--number` — the script determines the correct next number automatically
|
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
|
||||||
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
|
- Only match branches/directories with the exact short-name pattern
|
||||||
|
- If no existing branches/directories found with this short-name, start with number 1
|
||||||
- You must only ever run this script once per feature
|
- You must only ever run this script once per feature
|
||||||
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
|
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
|
||||||
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
|
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
|
||||||
@@ -129,7 +148,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 7
|
- **If all items pass**: Mark checklist complete and proceed to step 6
|
||||||
|
|
||||||
- **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,6 +197,8 @@ 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**.
|
||||||
|
|||||||
@@ -22,40 +22,6 @@ $ARGUMENTS
|
|||||||
|
|
||||||
You **MUST** consider the user input before proceeding (if not empty).
|
You **MUST** consider the user input before proceeding (if not empty).
|
||||||
|
|
||||||
## Pre-Execution Checks
|
|
||||||
|
|
||||||
**Check for extension hooks (before tasks generation)**:
|
|
||||||
- Check if `.specify/extensions.yml` exists in the project root.
|
|
||||||
- If it exists, read it and look for entries under the `hooks.before_tasks` key
|
|
||||||
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
|
|
||||||
- Filter to only hooks where `enabled: true`
|
|
||||||
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
|
|
||||||
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
|
|
||||||
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
|
|
||||||
- For each executable hook, output the following based on its `optional` flag:
|
|
||||||
- **Optional hook** (`optional: true`):
|
|
||||||
```
|
|
||||||
## Extension Hooks
|
|
||||||
|
|
||||||
**Optional Pre-Hook**: {extension}
|
|
||||||
Command: `/{command}`
|
|
||||||
Description: {description}
|
|
||||||
|
|
||||||
Prompt: {prompt}
|
|
||||||
To execute: `/{command}`
|
|
||||||
```
|
|
||||||
- **Mandatory hook** (`optional: false`):
|
|
||||||
```
|
|
||||||
## Extension Hooks
|
|
||||||
|
|
||||||
**Automatic Pre-Hook**: {extension}
|
|
||||||
Executing: `/{command}`
|
|
||||||
EXECUTE_COMMAND: {command}
|
|
||||||
|
|
||||||
Wait for the result of the hook command before proceeding to the Outline.
|
|
||||||
```
|
|
||||||
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
|
|
||||||
|
|
||||||
## Outline
|
## Outline
|
||||||
|
|
||||||
1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||||
@@ -97,35 +63,6 @@ You **MUST** consider the user input before proceeding (if not empty).
|
|||||||
- Suggested MVP scope (typically just User Story 1)
|
- Suggested MVP scope (typically just User Story 1)
|
||||||
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
|
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
|
||||||
|
|
||||||
6. **Check for extension hooks**: After tasks.md is generated, check if `.specify/extensions.yml` exists in the project root.
|
|
||||||
- If it exists, read it and look for entries under the `hooks.after_tasks` key
|
|
||||||
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
|
|
||||||
- Filter to only hooks where `enabled: true`
|
|
||||||
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
|
|
||||||
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
|
|
||||||
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
|
|
||||||
- For each executable hook, output the following based on its `optional` flag:
|
|
||||||
- **Optional hook** (`optional: true`):
|
|
||||||
```
|
|
||||||
## Extension Hooks
|
|
||||||
|
|
||||||
**Optional Hook**: {extension}
|
|
||||||
Command: `/{command}`
|
|
||||||
Description: {description}
|
|
||||||
|
|
||||||
Prompt: {prompt}
|
|
||||||
To execute: `/{command}`
|
|
||||||
```
|
|
||||||
- **Mandatory hook** (`optional: false`):
|
|
||||||
```
|
|
||||||
## Extension Hooks
|
|
||||||
|
|
||||||
**Automatic Hook**: {extension}
|
|
||||||
Executing: `/{command}`
|
|
||||||
EXECUTE_COMMAND: {command}
|
|
||||||
```
|
|
||||||
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
|
|
||||||
|
|
||||||
Context for task generation: {ARGS}
|
Context for task generation: {ARGS}
|
||||||
|
|
||||||
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
|
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
hooks:
|
|
||||||
before_implement:
|
|
||||||
- id: pre_test
|
|
||||||
enabled: true
|
|
||||||
optional: false
|
|
||||||
extension: "test-extension"
|
|
||||||
command: "pre_implement_test"
|
|
||||||
description: "Test before implement hook execution"
|
|
||||||
|
|
||||||
after_implement:
|
|
||||||
- id: post_test
|
|
||||||
enabled: true
|
|
||||||
optional: true
|
|
||||||
extension: "test-extension"
|
|
||||||
command: "post_implement_test"
|
|
||||||
description: "Test after implement hook execution"
|
|
||||||
prompt: "Would you like to run the post-implement test?"
|
|
||||||
|
|
||||||
before_tasks:
|
|
||||||
- id: pre_tasks_test
|
|
||||||
enabled: true
|
|
||||||
optional: false
|
|
||||||
extension: "test-extension"
|
|
||||||
command: "pre_tasks_test"
|
|
||||||
description: "Test before tasks hook execution"
|
|
||||||
|
|
||||||
after_tasks:
|
|
||||||
- id: post_tasks_test
|
|
||||||
enabled: true
|
|
||||||
optional: true
|
|
||||||
extension: "test-extension"
|
|
||||||
command: "post_tasks_test"
|
|
||||||
description: "Test after tasks hook execution"
|
|
||||||
prompt: "Would you like to run the post-tasks test?"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Testing Extension Hooks
|
|
||||||
|
|
||||||
This directory contains a mock project to verify that LLM agents correctly identify and execute hook commands defined in `.specify/extensions.yml`.
|
|
||||||
|
|
||||||
## Test 1: Testing `before_tasks` and `after_tasks`
|
|
||||||
|
|
||||||
1. Open a chat with an LLM (like GitHub Copilot) in this project.
|
|
||||||
2. Ask it to generate tasks for the current directory:
|
|
||||||
> "Please follow `/speckit.tasks` for the `./tests/hooks` directory."
|
|
||||||
3. **Expected Behavior**:
|
|
||||||
- Before doing any generation, the LLM should notice the `AUTOMATIC Pre-Hook` in `.specify/extensions.yml` under `before_tasks`.
|
|
||||||
- It should state it is executing `EXECUTE_COMMAND: pre_tasks_test`.
|
|
||||||
- It should then proceed to read the `.md` docs and produce a `tasks.md`.
|
|
||||||
- After generation, it should output the optional `after_tasks` hook (`post_tasks_test`) block, asking if you want to run it.
|
|
||||||
|
|
||||||
## Test 2: Testing `before_implement` and `after_implement`
|
|
||||||
|
|
||||||
*(Requires `tasks.md` from Test 1 to exist)*
|
|
||||||
|
|
||||||
1. In the same (or new) chat, ask the LLM to implement the tasks:
|
|
||||||
> "Please follow `/speckit.implement` for the `./tests/hooks` directory."
|
|
||||||
2. **Expected Behavior**:
|
|
||||||
- The LLM should first check for `before_implement` hooks.
|
|
||||||
- It should state it is executing `EXECUTE_COMMAND: pre_implement_test` BEFORE doing any actual task execution.
|
|
||||||
- It should evaluate the checklists and execute the code writing tasks.
|
|
||||||
- Upon completion, it should output the optional `after_implement` hook (`post_implement_test`) block.
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
The templates for these commands in `templates/commands/tasks.md` and `templates/commands/implement.md` contains strict ordered lists. The new `before_*` hooks are explicitly formulated in a **Pre-Execution Checks** section prior to the outline to ensure they're evaluated first without breaking template step numbers.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Test Setup for Hooks
|
|
||||||
|
|
||||||
This feature is designed to test if LLMs correctly invoke Spec Kit extensions hooks when generating tasks and implementing code.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- **User Story 1:** I want a test script that prints "Hello hooks!".
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- [ ] T001 [US1] Create script that prints 'Hello hooks!' in hello.py
|
|
||||||
@@ -28,13 +28,6 @@ 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")
|
||||||
@@ -62,14 +55,7 @@ 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/commands", ps_text, re.S) is not None
|
assert re.search(r"'agy'\s*\{.*?\.agent/workflows", 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."""
|
||||||
@@ -111,201 +97,3 @@ class TestAgentConfigConsistency:
|
|||||||
assert "kiro-cli" in pwsh_text
|
assert "kiro-cli" in pwsh_text
|
||||||
assert "Amazon Q Developer CLI" not in bash_text
|
assert "Amazon Q Developer CLI" not in bash_text
|
||||||
assert "Amazon Q Developer CLI" not in pwsh_text
|
assert "Amazon Q Developer CLI" not in pwsh_text
|
||||||
|
|
||||||
# --- Tabnine CLI consistency checks ---
|
|
||||||
|
|
||||||
def test_runtime_config_includes_tabnine(self):
|
|
||||||
"""AGENT_CONFIG should include tabnine with correct folder and subdir."""
|
|
||||||
assert "tabnine" in AGENT_CONFIG
|
|
||||||
assert AGENT_CONFIG["tabnine"]["folder"] == ".tabnine/agent/"
|
|
||||||
assert AGENT_CONFIG["tabnine"]["commands_subdir"] == "commands"
|
|
||||||
assert AGENT_CONFIG["tabnine"]["requires_cli"] is True
|
|
||||||
assert AGENT_CONFIG["tabnine"]["install_url"] is not None
|
|
||||||
|
|
||||||
def test_extension_registrar_includes_tabnine(self):
|
|
||||||
"""CommandRegistrar.AGENT_CONFIGS should include tabnine with correct TOML config."""
|
|
||||||
from specify_cli.extensions import CommandRegistrar
|
|
||||||
|
|
||||||
assert "tabnine" in CommandRegistrar.AGENT_CONFIGS
|
|
||||||
cfg = CommandRegistrar.AGENT_CONFIGS["tabnine"]
|
|
||||||
assert cfg["dir"] == ".tabnine/agent/commands"
|
|
||||||
assert cfg["format"] == "toml"
|
|
||||||
assert cfg["args"] == "{{args}}"
|
|
||||||
assert cfg["extension"] == ".toml"
|
|
||||||
|
|
||||||
def test_release_agent_lists_include_tabnine(self):
|
|
||||||
"""Bash and PowerShell release scripts should include tabnine 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 "tabnine" in sh_agents
|
|
||||||
assert "tabnine" in ps_agents
|
|
||||||
|
|
||||||
def test_release_scripts_generate_tabnine_toml_commands(self):
|
|
||||||
"""Release scripts should generate TOML commands for tabnine in .tabnine/agent/commands."""
|
|
||||||
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")
|
|
||||||
|
|
||||||
assert ".tabnine/agent/commands" in sh_text
|
|
||||||
assert ".tabnine/agent/commands" in ps_text
|
|
||||||
assert re.search(r"'tabnine'\s*\{.*?\.tabnine/agent/commands", ps_text, re.S) is not None
|
|
||||||
|
|
||||||
def test_github_release_includes_tabnine_packages(self):
|
|
||||||
"""GitHub release script should include tabnine template packages."""
|
|
||||||
gh_release_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-github-release.sh").read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
assert "spec-kit-template-tabnine-sh-" in gh_release_text
|
|
||||||
assert "spec-kit-template-tabnine-ps-" in gh_release_text
|
|
||||||
|
|
||||||
def test_agent_context_scripts_include_tabnine(self):
|
|
||||||
"""Agent context scripts should support tabnine agent type."""
|
|
||||||
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
|
|
||||||
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
assert "tabnine" in bash_text
|
|
||||||
assert "TABNINE_FILE" in bash_text
|
|
||||||
assert "tabnine" in pwsh_text
|
|
||||||
assert "TABNINE_FILE" in pwsh_text
|
|
||||||
|
|
||||||
def test_ai_help_includes_tabnine(self):
|
|
||||||
"""CLI help text for --ai should include tabnine."""
|
|
||||||
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
|
|
||||||
|
|
||||||
# --- Trae IDE consistency checks ---
|
|
||||||
|
|
||||||
def test_trae_in_agent_config(self):
|
|
||||||
"""AGENT_CONFIG should include trae with correct folder and commands_subdir."""
|
|
||||||
assert "trae" in AGENT_CONFIG
|
|
||||||
assert AGENT_CONFIG["trae"]["folder"] == ".trae/"
|
|
||||||
assert AGENT_CONFIG["trae"]["commands_subdir"] == "rules"
|
|
||||||
assert AGENT_CONFIG["trae"]["requires_cli"] is False
|
|
||||||
assert AGENT_CONFIG["trae"]["install_url"] is None
|
|
||||||
|
|
||||||
def test_trae_in_extension_registrar(self):
|
|
||||||
"""Extension command registrar should include trae using .trae/rules and markdown, if present."""
|
|
||||||
cfg = CommandRegistrar.AGENT_CONFIGS
|
|
||||||
|
|
||||||
assert "trae" in cfg
|
|
||||||
trae_cfg = cfg["trae"]
|
|
||||||
assert trae_cfg["format"] == "markdown"
|
|
||||||
assert trae_cfg["args"] == "$ARGUMENTS"
|
|
||||||
assert trae_cfg["extension"] == ".md"
|
|
||||||
|
|
||||||
def test_trae_in_release_agent_lists(self):
|
|
||||||
"""Bash and PowerShell release scripts should include trae 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 "trae" in sh_agents
|
|
||||||
assert "trae" in ps_agents
|
|
||||||
|
|
||||||
def test_trae_in_release_scripts_generate_commands(self):
|
|
||||||
"""Release scripts should generate markdown commands for trae in .trae/rules."""
|
|
||||||
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")
|
|
||||||
|
|
||||||
assert ".trae/rules" in sh_text
|
|
||||||
assert ".trae/rules" in ps_text
|
|
||||||
assert re.search(r"'trae'\s*\{.*?\.trae/rules", ps_text, re.S) is not None
|
|
||||||
|
|
||||||
def test_trae_in_github_release_output(self):
|
|
||||||
"""GitHub release script should include trae template packages."""
|
|
||||||
gh_release_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-github-release.sh").read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
assert "spec-kit-template-trae-sh-" in gh_release_text
|
|
||||||
assert "spec-kit-template-trae-ps-" in gh_release_text
|
|
||||||
|
|
||||||
def test_trae_in_agent_context_scripts(self):
|
|
||||||
"""Agent context scripts should support trae agent type."""
|
|
||||||
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
|
|
||||||
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
assert "trae" in bash_text
|
|
||||||
assert "TRAE_FILE" in bash_text
|
|
||||||
assert "trae" in pwsh_text
|
|
||||||
assert "TRAE_FILE" in pwsh_text
|
|
||||||
|
|
||||||
def test_trae_in_powershell_validate_set(self):
|
|
||||||
"""PowerShell update-agent-context script should include 'trae' 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 "trae" in validate_set_values
|
|
||||||
|
|
||||||
def test_ai_help_includes_trae(self):
|
|
||||||
"""CLI help text for --ai should include trae."""
|
|
||||||
assert "trae" in AI_ASSISTANT_HELP
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def templates_dir(project_dir):
|
|||||||
tpl_root.mkdir(parents=True, exist_ok=True)
|
tpl_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Template with valid YAML frontmatter
|
# Template with valid YAML frontmatter
|
||||||
(tpl_root / "speckit.specify.md").write_text(
|
(tpl_root / "specify.md").write_text(
|
||||||
"---\n"
|
"---\n"
|
||||||
"description: Create or update the feature specification.\n"
|
"description: Create or update the feature specification.\n"
|
||||||
"handoffs:\n"
|
"handoffs:\n"
|
||||||
@@ -79,7 +79,7 @@ def templates_dir(project_dir):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Template with minimal frontmatter
|
# Template with minimal frontmatter
|
||||||
(tpl_root / "speckit.plan.md").write_text(
|
(tpl_root / "plan.md").write_text(
|
||||||
"---\n"
|
"---\n"
|
||||||
"description: Generate implementation plan.\n"
|
"description: Generate implementation plan.\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
@@ -91,7 +91,7 @@ def templates_dir(project_dir):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Template with no frontmatter
|
# Template with no frontmatter
|
||||||
(tpl_root / "speckit.tasks.md").write_text(
|
(tpl_root / "tasks.md").write_text(
|
||||||
"# Tasks Command\n"
|
"# Tasks Command\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Body without frontmatter.\n",
|
"Body without frontmatter.\n",
|
||||||
@@ -99,7 +99,7 @@ def templates_dir(project_dir):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Template with empty YAML frontmatter (yaml.safe_load returns None)
|
# Template with empty YAML frontmatter (yaml.safe_load returns None)
|
||||||
(tpl_root / "speckit.empty_fm.md").write_text(
|
(tpl_root / "empty_fm.md").write_text(
|
||||||
"---\n"
|
"---\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -132,16 +132,6 @@ 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:
|
||||||
@@ -157,11 +147,6 @@ class TestGetSkillsDir:
|
|||||||
result = _get_skills_dir(project_dir, "gemini")
|
result = _get_skills_dir(project_dir, "gemini")
|
||||||
assert result == project_dir / ".gemini" / "skills"
|
assert result == project_dir / ".gemini" / "skills"
|
||||||
|
|
||||||
def test_tabnine_skills_dir(self, project_dir):
|
|
||||||
"""Tabnine should use .tabnine/agent/skills/."""
|
|
||||||
result = _get_skills_dir(project_dir, "tabnine")
|
|
||||||
assert result == project_dir / ".tabnine" / "agent" / "skills"
|
|
||||||
|
|
||||||
def test_copilot_skills_dir(self, project_dir):
|
def test_copilot_skills_dir(self, project_dir):
|
||||||
"""Copilot should use .github/skills/."""
|
"""Copilot should use .github/skills/."""
|
||||||
result = _get_skills_dir(project_dir, "copilot")
|
result = _get_skills_dir(project_dir, "copilot")
|
||||||
@@ -337,7 +322,7 @@ class TestInstallAiSkills:
|
|||||||
cmds_dir = project_dir / ".claude" / "commands"
|
cmds_dir = project_dir / ".claude" / "commands"
|
||||||
cmds_dir.mkdir(parents=True)
|
cmds_dir.mkdir(parents=True)
|
||||||
|
|
||||||
(cmds_dir / "speckit.broken.md").write_text(
|
(cmds_dir / "broken.md").write_text(
|
||||||
"---\n"
|
"---\n"
|
||||||
"description: [unclosed bracket\n"
|
"description: [unclosed bracket\n"
|
||||||
" invalid: yaml: content: here\n"
|
" invalid: yaml: content: here\n"
|
||||||
@@ -400,28 +385,6 @@ 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."""
|
||||||
@@ -430,12 +393,9 @@ class TestInstallAiSkills:
|
|||||||
|
|
||||||
# Place .md templates in the agent's commands directory
|
# Place .md templates in the agent's commands directory
|
||||||
agent_folder = AGENT_CONFIG[agent_key]["folder"]
|
agent_folder = AGENT_CONFIG[agent_key]["folder"]
|
||||||
commands_subdir = AGENT_CONFIG[agent_key].get("commands_subdir", "commands")
|
cmds_dir = proj / agent_folder.rstrip("/") / "commands"
|
||||||
cmds_dir = proj / agent_folder.rstrip("/") / commands_subdir
|
|
||||||
cmds_dir.mkdir(parents=True)
|
cmds_dir.mkdir(parents=True)
|
||||||
# Copilot uses speckit.*.agent.md templates; other agents use speckit.*.md
|
(cmds_dir / "specify.md").write_text(
|
||||||
fname = "speckit.specify.agent.md" if agent_key == "copilot" else "speckit.specify.md"
|
|
||||||
(cmds_dir / fname).write_text(
|
|
||||||
"---\ndescription: Test command\n---\n\n# Test\n\nBody.\n"
|
"---\ndescription: Test command\n---\n\n# Test\n\nBody.\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -445,106 +405,10 @@ 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()]
|
||||||
# Kimi uses dot-separator (speckit.specify) to match /skill:speckit.* invocation;
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
def test_copilot_ignores_non_speckit_agents(self, project_dir):
|
|
||||||
"""Non-speckit markdown in .github/agents/ must not produce skills."""
|
|
||||||
agents_dir = project_dir / ".github" / "agents"
|
|
||||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
(agents_dir / "speckit.plan.agent.md").write_text(
|
|
||||||
"---\ndescription: Generate implementation plan.\n---\n\n# Plan\n\nBody.\n"
|
|
||||||
)
|
|
||||||
(agents_dir / "my-custom-agent.agent.md").write_text(
|
|
||||||
"---\ndescription: A user custom agent\n---\n\n# Custom\n\nBody.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
result = install_ai_skills(project_dir, "copilot")
|
|
||||||
|
|
||||||
assert result is True
|
|
||||||
skills_dir = _get_skills_dir(project_dir, "copilot")
|
|
||||||
assert skills_dir.exists()
|
|
||||||
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
|
||||||
assert "speckit-plan" in skill_dirs
|
|
||||||
assert "speckit-my-custom-agent.agent" not in skill_dirs
|
|
||||||
assert "speckit-my-custom-agent" not in skill_dirs
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("agent_key,custom_file", [
|
|
||||||
("claude", "review.md"),
|
|
||||||
("cursor-agent", "deploy.md"),
|
|
||||||
("qwen", "my-workflow.md"),
|
|
||||||
])
|
|
||||||
def test_non_speckit_commands_ignored_for_all_agents(self, temp_dir, agent_key, custom_file):
|
|
||||||
"""User-authored command files must not produce skills for any agent."""
|
|
||||||
proj = temp_dir / f"proj-{agent_key}"
|
|
||||||
proj.mkdir()
|
|
||||||
|
|
||||||
agent_folder = AGENT_CONFIG[agent_key]["folder"]
|
|
||||||
commands_subdir = AGENT_CONFIG[agent_key].get("commands_subdir", "commands")
|
|
||||||
cmds_dir = proj / agent_folder.rstrip("/") / commands_subdir
|
|
||||||
cmds_dir.mkdir(parents=True)
|
|
||||||
(cmds_dir / "speckit.specify.md").write_text(
|
|
||||||
"---\ndescription: Create spec.\n---\n\n# Specify\n\nBody.\n"
|
|
||||||
)
|
|
||||||
(cmds_dir / custom_file).write_text(
|
|
||||||
"---\ndescription: User custom command\n---\n\n# Custom\n\nBody.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
result = install_ai_skills(proj, agent_key)
|
|
||||||
|
|
||||||
assert result is True
|
|
||||||
skills_dir = _get_skills_dir(proj, agent_key)
|
|
||||||
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
|
||||||
assert "speckit-specify" in skill_dirs
|
assert "speckit-specify" in skill_dirs
|
||||||
custom_stem = Path(custom_file).stem
|
assert (skills_dir / "speckit-specify" / "SKILL.md").exists()
|
||||||
assert f"speckit-{custom_stem}" not in skill_dirs
|
|
||||||
|
|
||||||
def test_copilot_fallback_when_only_non_speckit_agents(self, project_dir):
|
|
||||||
"""Fallback to templates/commands/ when .github/agents/ has no speckit.*.md files."""
|
|
||||||
agents_dir = project_dir / ".github" / "agents"
|
|
||||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
# Only a user-authored agent, no speckit.* templates
|
|
||||||
(agents_dir / "my-custom-agent.agent.md").write_text(
|
|
||||||
"---\ndescription: A user custom agent\n---\n\n# Custom\n\nBody.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
result = install_ai_skills(project_dir, "copilot")
|
|
||||||
|
|
||||||
# Should succeed via fallback to templates/commands/
|
|
||||||
assert result is True
|
|
||||||
skills_dir = _get_skills_dir(project_dir, "copilot")
|
|
||||||
assert skills_dir.exists()
|
|
||||||
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
|
||||||
# Should have skills from fallback templates, not from the custom agent
|
|
||||||
assert "speckit-plan" in skill_dirs
|
|
||||||
assert not any("my-custom" in d for d in skill_dirs)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("agent_key", ["claude", "cursor-agent", "qwen"])
|
|
||||||
def test_fallback_when_only_non_speckit_commands(self, temp_dir, agent_key):
|
|
||||||
"""Fallback to templates/commands/ when agent dir has no speckit.*.md files."""
|
|
||||||
proj = temp_dir / f"proj-{agent_key}"
|
|
||||||
proj.mkdir()
|
|
||||||
|
|
||||||
agent_folder = AGENT_CONFIG[agent_key]["folder"]
|
|
||||||
commands_subdir = AGENT_CONFIG[agent_key].get("commands_subdir", "commands")
|
|
||||||
cmds_dir = proj / agent_folder.rstrip("/") / commands_subdir
|
|
||||||
cmds_dir.mkdir(parents=True)
|
|
||||||
# Only a user-authored command, no speckit.* templates
|
|
||||||
(cmds_dir / "my-custom-command.md").write_text(
|
|
||||||
"---\ndescription: User custom command\n---\n\n# Custom\n\nBody.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
result = install_ai_skills(proj, agent_key)
|
|
||||||
|
|
||||||
# Should succeed via fallback to templates/commands/
|
|
||||||
assert result is True
|
|
||||||
skills_dir = _get_skills_dir(proj, agent_key)
|
|
||||||
assert skills_dir.exists()
|
|
||||||
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
|
||||||
assert not any("my-custom" in d for d in skill_dirs)
|
|
||||||
|
|
||||||
class TestCommandCoexistence:
|
class TestCommandCoexistence:
|
||||||
"""Verify install_ai_skills never touches command files.
|
"""Verify install_ai_skills never touches command files.
|
||||||
@@ -556,16 +420,14 @@ class TestCommandCoexistence:
|
|||||||
|
|
||||||
def test_existing_commands_preserved_claude(self, project_dir, templates_dir, commands_dir_claude):
|
def test_existing_commands_preserved_claude(self, project_dir, templates_dir, commands_dir_claude):
|
||||||
"""install_ai_skills must NOT remove pre-existing .claude/commands files."""
|
"""install_ai_skills must NOT remove pre-existing .claude/commands files."""
|
||||||
# Verify commands exist before (templates_dir adds 4 speckit.* files,
|
# Verify commands exist before
|
||||||
# commands_dir_claude overlaps with 3 of them)
|
assert len(list(commands_dir_claude.glob("speckit.*"))) == 3
|
||||||
before = list(commands_dir_claude.glob("speckit.*"))
|
|
||||||
assert len(before) >= 3
|
|
||||||
|
|
||||||
install_ai_skills(project_dir, "claude")
|
install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
# Commands must still be there — install_ai_skills never touches them
|
# Commands must still be there — install_ai_skills never touches them
|
||||||
remaining = list(commands_dir_claude.glob("speckit.*"))
|
remaining = list(commands_dir_claude.glob("speckit.*"))
|
||||||
assert len(remaining) == len(before)
|
assert len(remaining) == 3
|
||||||
|
|
||||||
def test_existing_commands_preserved_gemini(self, project_dir, templates_dir, commands_dir_gemini):
|
def test_existing_commands_preserved_gemini(self, project_dir, templates_dir, commands_dir_gemini):
|
||||||
"""install_ai_skills must NOT remove pre-existing .gemini/commands files."""
|
"""install_ai_skills must NOT remove pre-existing .gemini/commands files."""
|
||||||
@@ -576,15 +438,6 @@ 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")
|
||||||
@@ -800,59 +653,6 @@ 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
@@ -1,190 +0,0 @@
|
|||||||
import stat
|
|
||||||
|
|
||||||
from specify_cli import merge_json_files
|
|
||||||
from specify_cli import handle_vscode_settings
|
|
||||||
|
|
||||||
# --- Dimension 2: Polite Deep Merge Strategy ---
|
|
||||||
|
|
||||||
def test_merge_json_files_type_mismatch_preservation(tmp_path):
|
|
||||||
"""If user has a string but template wants a dict, PRESERVE user's string."""
|
|
||||||
existing_file = tmp_path / "settings.json"
|
|
||||||
# User might have overridden a setting with a simple string or different type
|
|
||||||
existing_file.write_text('{"chat.editor.fontFamily": "CustomFont"}')
|
|
||||||
|
|
||||||
# Template might expect a dict for the same key (hypothetically)
|
|
||||||
new_settings = {
|
|
||||||
"chat.editor.fontFamily": {"font": "TemplateFont"}
|
|
||||||
}
|
|
||||||
|
|
||||||
merged = merge_json_files(existing_file, new_settings)
|
|
||||||
# Result is None because user settings were preserved and nothing else changed
|
|
||||||
assert merged is None
|
|
||||||
|
|
||||||
def test_merge_json_files_deep_nesting(tmp_path):
|
|
||||||
"""Verify deep recursive merging of new keys."""
|
|
||||||
existing_file = tmp_path / "settings.json"
|
|
||||||
existing_file.write_text("""
|
|
||||||
{
|
|
||||||
"a": {
|
|
||||||
"b": {
|
|
||||||
"c": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
new_settings = {
|
|
||||||
"a": {
|
|
||||||
"b": {
|
|
||||||
"d": 2 # New nested key
|
|
||||||
},
|
|
||||||
"e": 3 # New mid-level key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
merged = merge_json_files(existing_file, new_settings)
|
|
||||||
assert merged["a"]["b"]["c"] == 1
|
|
||||||
assert merged["a"]["b"]["d"] == 2
|
|
||||||
assert merged["a"]["e"] == 3
|
|
||||||
|
|
||||||
def test_merge_json_files_empty_existing(tmp_path):
|
|
||||||
"""Merging into an empty/new file."""
|
|
||||||
existing_file = tmp_path / "empty.json"
|
|
||||||
existing_file.write_text("{}")
|
|
||||||
|
|
||||||
new_settings = {"a": 1}
|
|
||||||
merged = merge_json_files(existing_file, new_settings)
|
|
||||||
assert merged == {"a": 1}
|
|
||||||
|
|
||||||
# --- Dimension 3: Real-world Simulation ---
|
|
||||||
|
|
||||||
def test_merge_vscode_realistic_scenario(tmp_path):
|
|
||||||
"""A realistic VSCode settings.json with many existing preferences, comments, and trailing commas."""
|
|
||||||
existing_file = tmp_path / "vscode_settings.json"
|
|
||||||
existing_file.write_text("""
|
|
||||||
{
|
|
||||||
"editor.fontSize": 12,
|
|
||||||
"editor.formatOnSave": true, /* block comment */
|
|
||||||
"files.exclude": {
|
|
||||||
"**/.git": true,
|
|
||||||
"**/node_modules": true,
|
|
||||||
},
|
|
||||||
"chat.promptFilesRecommendations": {
|
|
||||||
"existing.tool": true,
|
|
||||||
} // User comment
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
template_settings = {
|
|
||||||
"chat.promptFilesRecommendations": {
|
|
||||||
"speckit.specify": True,
|
|
||||||
"speckit.plan": True
|
|
||||||
},
|
|
||||||
"chat.tools.terminal.autoApprove": {
|
|
||||||
".specify/scripts/bash/": True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
merged = merge_json_files(existing_file, template_settings)
|
|
||||||
|
|
||||||
# Check preservation
|
|
||||||
assert merged["editor.fontSize"] == 12
|
|
||||||
assert merged["files.exclude"]["**/.git"] is True
|
|
||||||
assert merged["chat.promptFilesRecommendations"]["existing.tool"] is True
|
|
||||||
|
|
||||||
# Check additions
|
|
||||||
assert merged["chat.promptFilesRecommendations"]["speckit.specify"] is True
|
|
||||||
assert merged["chat.tools.terminal.autoApprove"][".specify/scripts/bash/"] is True
|
|
||||||
|
|
||||||
# --- Dimension 4: Error Handling & Robustness ---
|
|
||||||
|
|
||||||
def test_merge_json_files_with_bom(tmp_path):
|
|
||||||
"""Test files with UTF-8 BOM (sometimes created on Windows)."""
|
|
||||||
existing_file = tmp_path / "bom.json"
|
|
||||||
content = '{"a": 1}'
|
|
||||||
# Prepend UTF-8 BOM
|
|
||||||
existing_file.write_bytes(b'\xef\xbb\xbf' + content.encode('utf-8'))
|
|
||||||
|
|
||||||
new_settings = {"b": 2}
|
|
||||||
merged = merge_json_files(existing_file, new_settings)
|
|
||||||
assert merged == {"a": 1, "b": 2}
|
|
||||||
|
|
||||||
def test_merge_json_files_not_a_dictionary_template(tmp_path):
|
|
||||||
"""If for some reason new_content is not a dict, PRESERVE existing settings by returning None."""
|
|
||||||
existing_file = tmp_path / "ok.json"
|
|
||||||
existing_file.write_text('{"a": 1}')
|
|
||||||
|
|
||||||
# Secure fallback: return None to skip writing and avoid clobbering
|
|
||||||
assert merge_json_files(existing_file, ["not", "a", "dict"]) is None
|
|
||||||
|
|
||||||
def test_merge_json_files_unparseable_existing(tmp_path):
|
|
||||||
"""If the existing file is unparseable JSON, return None to avoid overwriting it."""
|
|
||||||
bad_file = tmp_path / "bad.json"
|
|
||||||
bad_file.write_text('{"a": 1, missing_value}') # Invalid JSON
|
|
||||||
|
|
||||||
assert merge_json_files(bad_file, {"b": 2}) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_json_files_list_preservation(tmp_path):
|
|
||||||
"""Verify that existing list values are preserved and NOT merged or overwritten."""
|
|
||||||
existing_file = tmp_path / "list.json"
|
|
||||||
existing_file.write_text('{"my.list": ["user_item"]}')
|
|
||||||
|
|
||||||
template_settings = {
|
|
||||||
"my.list": ["template_item"]
|
|
||||||
}
|
|
||||||
|
|
||||||
merged = merge_json_files(existing_file, template_settings)
|
|
||||||
# The polite merge policy says: keep existing values if they exist and aren't both dicts.
|
|
||||||
# Since nothing changed, it returns None.
|
|
||||||
assert merged is None
|
|
||||||
|
|
||||||
def test_merge_json_files_no_changes(tmp_path):
|
|
||||||
"""If the merge doesn't introduce any new keys or changes, return None to skip rewrite."""
|
|
||||||
existing_file = tmp_path / "no_change.json"
|
|
||||||
existing_file.write_text('{"a": 1, "b": {"c": 2}}')
|
|
||||||
|
|
||||||
template_settings = {
|
|
||||||
"a": 1, # Already exists
|
|
||||||
"b": {"c": 2} # Already exists nested
|
|
||||||
}
|
|
||||||
|
|
||||||
# Should return None because result == existing
|
|
||||||
assert merge_json_files(existing_file, template_settings) is None
|
|
||||||
|
|
||||||
def test_merge_json_files_type_mismatch_no_op(tmp_path):
|
|
||||||
"""If a key exists with different type and we preserve it, it might still result in no change."""
|
|
||||||
existing_file = tmp_path / "mismatch_no_op.json"
|
|
||||||
existing_file.write_text('{"a": "user_string"}')
|
|
||||||
|
|
||||||
template_settings = {
|
|
||||||
"a": {"key": "template_dict"} # Mismatch, will be ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
# Should return None because we preserved the user's string and nothing else changed
|
|
||||||
assert merge_json_files(existing_file, template_settings) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_vscode_settings_preserves_mode_on_atomic_write(tmp_path):
|
|
||||||
"""Atomic rewrite should preserve existing file mode bits."""
|
|
||||||
vscode_dir = tmp_path / ".vscode"
|
|
||||||
vscode_dir.mkdir()
|
|
||||||
dest_file = vscode_dir / "settings.json"
|
|
||||||
template_file = tmp_path / "template_settings.json"
|
|
||||||
|
|
||||||
dest_file.write_text('{"a": 1}\n', encoding="utf-8")
|
|
||||||
dest_file.chmod(0o640)
|
|
||||||
before_mode = stat.S_IMODE(dest_file.stat().st_mode)
|
|
||||||
|
|
||||||
template_file.write_text('{"b": 2}\n', encoding="utf-8")
|
|
||||||
|
|
||||||
handle_vscode_settings(
|
|
||||||
template_file,
|
|
||||||
dest_file,
|
|
||||||
"settings.json",
|
|
||||||
verbose=False,
|
|
||||||
tracker=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
after_mode = stat.S_IMODE(dest_file.stat().st_mode)
|
|
||||||
assert after_mode == before_mode
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user