Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions .github/workflows/scripts/create-release-packages.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

.PARAMETER Agents
Comma or space separated subset of agents to build (default: all)
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, junie, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, kimi, trae, pi, iflow, generic
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, junie, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, kimi, trae, pi, iflow, goose, generic

.PARAMETER Scripts
Comma or space separated subset of script types to build (default: both)
Expand Down Expand Up @@ -174,6 +174,27 @@ function Generate-Commands {
'agent.md' {
Set-Content -Path $outputFile -Value $body -NoNewline
}
'yaml' {
# Generate Goose recipe format YAML
$title = (Get-Culture).TextInfo.ToTitleCase($name)
$output = @"
version: 1.0.0
title: "$title"
description: "$description"
author:
contact: "spec-kit"
extensions:
- type: builtin
name: developer
activities:
- "Spec-Driven Development"
prompt: |
"@
# Indent each line of body for proper YAML block scalar formatting
$indentedBody = $body -split "`n" | ForEach-Object { " $_" } | Join-String -Separator "`n"
Comment on lines +193 to +194
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the yaml case, ArgFormat isn’t applied to the $ARGUMENTS placeholder that appears in the command bodies (templates use $ARGUMENTS in the “User Input” block). This means the generated Goose .yaml recipes will still contain $ARGUMENTS even though Goose’s config specifies {{args}}. Consider replacing $ARGUMENTS with $ArgFormat when building $body (for YAML and TOML outputs) so packaged templates match the runtime placeholder conversion.

Suggested change
# Indent each line of body for proper YAML block scalar formatting
$indentedBody = $body -split "`n" | ForEach-Object { " $_" } | Join-String -Separator "`n"
# Replace $ARGUMENTS placeholder with configured ArgFormat for Goose YAML recipes
$bodyWithArgs = $body.Replace('$ARGUMENTS', $ArgFormat)
# Indent each line of body for proper YAML block scalar formatting
$indentedBody = $bodyWithArgs -split "`n" | ForEach-Object { " $_" } | Join-String -Separator "`n"

Copilot uses AI. Check for mistakes.
$output += "`n$indentedBody"
Set-Content -Path $outputFile -Value $output -NoNewline
}
}
}
}
Expand Down Expand Up @@ -477,6 +498,10 @@ function Build-Variant {
$cmdDir = Join-Path $baseDir ".iflow/commands"
Generate-Commands -Agent 'iflow' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'goose' {
$cmdDir = Join-Path $baseDir ".goose/recipes"
Generate-Commands -Agent 'goose' -Extension 'yaml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
}
'generic' {
$cmdDir = Join-Path $baseDir ".speckit/commands"
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
Expand All @@ -493,7 +518,7 @@ function Build-Variant {
}

# Define all agents and scripts
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'junie', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'kimi', 'trae', 'pi', 'iflow', 'generic')
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'junie', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'kimi', 'trae', 'pi', 'iflow', 'goose', 'generic')
$AllScripts = @('sh', 'ps')

function Normalize-List {
Expand Down
29 changes: 27 additions & 2 deletions .github/workflows/scripts/create-release-packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ set -euo pipefail
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
# Version argument should include leading 'v'.
# 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 junie codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi trae pi iflow generic (default: all)
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf junie codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi trae pi iflow goose generic (default: all)
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
# Examples:
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
Expand Down Expand Up @@ -116,6 +116,28 @@ generate_commands() {
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
agent.md)
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
yaml)
# Generate Goose recipe format YAML
local title
# Use awk for reliable title casing (sed \b is not portable)
title=$(echo "$name" | tr '_-' ' ' | awk '{for (i=1; i<=NF; i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')
# Indent every line of body for valid YAML block scalar syntax
indented_body=$(printf '%s\n' "$body" | sed 's/^/ /')
cat > "$output_dir/speckit.$name.$ext" <<YAML_EOF
Comment on lines +121 to +126
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indented_body is assigned without local inside generate_commands(). In this function most per-template variables are declared local, so leaving this one global is easy to miss and can lead to accidental reuse/clobbering if the function grows. Declare indented_body as local (and ideally keep variable scoping consistent across the function).

Copilot uses AI. Check for mistakes.
version: 1.0.0
title: "$title"
description: "$description"
author:
contact: "spec-kit"
extensions:
- type: builtin
name: developer
activities:
- "Spec-Driven Development"
prompt: |
${indented_body}
YAML_EOF
;;
esac
done
}
Expand Down Expand Up @@ -330,6 +352,9 @@ build_variant() {
iflow)
mkdir -p "$base_dir/.iflow/commands"
generate_commands iflow md "\$ARGUMENTS" "$base_dir/.iflow/commands" "$script" ;;
goose)
mkdir -p "$base_dir/.goose/recipes"
generate_commands goose yaml "{{args}}" "$base_dir/.goose/recipes" "$script" ;;
generic)
mkdir -p "$base_dir/.speckit/commands"
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
Expand All @@ -339,7 +364,7 @@ build_variant() {
}

# Determine agent list
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf junie codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi trae pi iflow generic)
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf junie codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi trae pi iflow goose generic)
ALL_SCRIPTS=(sh ps)

validate_subset() {
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Specify supports multiple AI agents by generating agent-specific command files a
| **Kimi Code** | `.kimi/skills/` | Markdown | `kimi` | Kimi Code CLI (Moonshot AI) |
| **Pi Coding Agent** | `.pi/prompts/` | Markdown | `pi` | Pi terminal coding agent |
| **iFlow CLI** | `.iflow/commands/` | Markdown | `iflow` | iFlow CLI (iflow-ai) |
| **Goose** | `.goose/recipes/` | YAML | `goose` | Block's open source AI agent with slash command support |
| **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 |
Expand Down Expand Up @@ -328,6 +329,7 @@ Require a command-line tool to be installed:
- **Tabnine CLI**: `tabnine` CLI
- **Kimi Code**: `kimi` CLI
- **Pi Coding Agent**: `pi` CLI
- **Goose**: `goose` CLI

### IDE-Based Agents

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ Community projects that extend, visualize, or build on Spec Kit:
| [Junie](https://junie.jetbrains.com/) | ✅ | |
| [Antigravity (agy)](https://antigravity.google/) | ✅ | Requires `--ai-skills` |
| [Trae](https://www.trae.ai/) | ✅ | |
| [Goose](https://block.github.io/goose/) | ✅ | Uses YAML recipe format in `.goose/recipes/` with slash command support |
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |

## 🔧 Specify CLI Reference
Expand Down
11 changes: 8 additions & 3 deletions scripts/bash/update-agent-context.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
# - Creates default Claude file if no agent files exist
#
# Usage: ./update-agent-context.sh [agent_type]
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|goose|generic
# Leave empty to update all existing agent files

set -e
Expand Down Expand Up @@ -86,6 +86,7 @@ VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
KIMI_FILE="$REPO_ROOT/KIMI.md"
TRAE_FILE="$REPO_ROOT/.trae/rules/AGENTS.md"
IFLOW_FILE="$REPO_ROOT/IFLOW.md"
GOOSE_FILE="$REPO_ROOT/.goose/recipes/AGENTS.md"

# Template file
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
Expand Down Expand Up @@ -690,12 +691,15 @@ update_specific_agent() {
iflow)
update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1
;;
goose)
update_agent_file "$GOOSE_FILE" "Goose" || return 1
;;
generic)
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 "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic"
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|goose|generic"
exit 1
;;
esac
Expand Down Expand Up @@ -757,6 +761,7 @@ update_all_existing_agents() {
_update_if_new "$KIMI_FILE" "Kimi Code" || _all_ok=false
_update_if_new "$TRAE_FILE" "Trae" || _all_ok=false
_update_if_new "$IFLOW_FILE" "iFlow CLI" || _all_ok=false
_update_if_new "$GOOSE_FILE" "Goose" || _all_ok=false

# If no agent files exist, create a default Claude file
if [[ "$_found_agent" == false ]]; then
Expand All @@ -783,7 +788,7 @@ print_summary() {
fi

echo
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]"
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|goose|generic]"
}

#==============================================================================
Expand Down
11 changes: 7 additions & 4 deletions scripts/powershell/update-agent-context.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh:
2. Plan Data Extraction
3. Agent File Management (create from template or update existing)
4. Content Generation (technology stack, recent changes, timestamp)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, junie, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, trae, pi, iflow, generic)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, junie, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, trae, pi, iflow, goose, generic)

.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).
Expand All @@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1
#>
param(
[Parameter(Position=0)]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','junie','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','qodercli','vibe','kimi','trae','pi','iflow','generic')]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','junie','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','qodercli','vibe','kimi','trae','pi','iflow','goose','generic')]
[string]$AgentType
)

Expand Down Expand Up @@ -67,6 +67,7 @@ $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'
$IFLOW_FILE = Join-Path $REPO_ROOT 'IFLOW.md'
$GOOSE_FILE = Join-Path $REPO_ROOT '.goose/recipes/AGENTS.md'

$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'

Expand Down Expand Up @@ -415,8 +416,9 @@ function Update-SpecificAgent {
'trae' { Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae' }
'pi' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Pi Coding Agent' }
'iflow' { Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI' }
'goose' { Update-AgentFile -TargetFile $GOOSE_FILE -AgentName 'Goose' }
'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|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic'; return $false }
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|goose|generic'; return $false }
}
}

Expand Down Expand Up @@ -445,6 +447,7 @@ function Update-AllExistingAgents {
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 (Test-Path $IFLOW_FILE) { if (-not (Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false }; $found = $true }
if (Test-Path $GOOSE_FILE) { if (-not (Update-AgentFile -TargetFile $GOOSE_FILE -AgentName 'Goose')) { $ok = $false }; $found = $true }
if (-not $found) {
Write-Info 'No existing agent files found, creating default Claude file...'
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
Expand All @@ -459,7 +462,7 @@ function Print-Summary {
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
Write-Host ''
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]'
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|goose|generic]'
}

function Main {
Expand Down
7 changes: 7 additions & 0 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,13 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str)
"install_url": "https://docs.iflow.cn/en/cli/quickstart",
"requires_cli": True,
},
"goose": {
"name": "Goose",
"folder": ".goose/",
"commands_subdir": "recipes",
"install_url": "https://block.github.io/goose/docs/getting-started/installation",
"requires_cli": True,
},
"generic": {
"name": "Generic (bring your own agent)",
"folder": None, # Set dynamically via --ai-commands-dir
Expand Down
64 changes: 64 additions & 0 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ class CommandRegistrar:
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"goose": {
"dir": ".goose/recipes",
"format": "yaml",
"args": "{{args}}",
"extension": ".yaml"
}
}

Expand Down Expand Up @@ -329,6 +335,62 @@ def render_toml_command(

return "\n".join(toml_lines)

def render_yaml_command(
self,
frontmatter: dict,
body: str,
source_id: str
) -> str:
"""Render command in YAML format for Goose recipes.

Args:
frontmatter: Command frontmatter
body: Command body content
source_id: Source identifier (extension or preset ID)

Returns:
Formatted YAML recipe file content
"""
# Get title from frontmatter or generate from command name
title = frontmatter.get("title", "")
if not title and "name" in frontmatter:
# Generate title from command name
title = frontmatter["name"].replace("_", " ").replace("-", " ").title()

Comment on lines +354 to +359
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render_yaml_command() derives title only from frontmatter (title or name). The core command templates’ frontmatter doesn’t include either key (they only have description/scripts), so Goose recipes generated via CommandRegistrar.register_commands() will end up with an empty title, which is likely not valid/usable for Goose recipes. Consider passing cmd_name (or output_name) into render_yaml_command() and generating a fallback title from it (similar to the release packaging scripts’ filename-based title generation).

Suggested change
# Get title from frontmatter or generate from command name
title = frontmatter.get("title", "")
if not title and "name" in frontmatter:
# Generate title from command name
title = frontmatter["name"].replace("_", " ").replace("-", " ").title()
# Get title from frontmatter or generate from available identifiers
title = frontmatter.get("title", "")
# Prefer explicit name if title is missing
if not title and "name" in frontmatter and frontmatter["name"]:
title = (
str(frontmatter["name"])
.replace("_", " ")
.replace("-", " ")
.title()
)
# Fallback to other likely identifier fields in frontmatter
if not title:
for key in ("id", "command"):
value = frontmatter.get(key)
if value:
title = (
str(value)
.replace("_", " ")
.replace("-", " ")
.title()
)
break
# Final fallback: derive a title from the source_id (e.g., filename or path)
if not title and source_id:
source_str = str(source_id)
source_name = Path(source_str).name
source_stem = Path(source_name).stem
title = (
source_stem.replace("_", " ").replace("-", " ").title()
if source_stem
else source_str
)
# Absolute last resort to avoid empty titles
if not title:
title = "Command"

Copilot uses AI. Check for mistakes.
description = frontmatter.get("description", "")

# Build YAML structure following Goose recipe schema
# Use yaml.safe_dump() for proper escaping of title and description
header_dict = {
"version": "1.0.0",
"title": title,
"description": description,
"author": {"contact": "spec-kit"},
"extensions": [{"type": "builtin", "name": "developer"}],
"activities": ["Spec-Driven Development"],
}

# Dump header with proper escaping and consistent formatting
header_yaml = yaml.safe_dump(
header_dict,
sort_keys=False,
allow_unicode=True,
default_flow_style=False,
).strip()

# Build the final YAML with literal block scalar for prompt
lines = [header_yaml, "prompt: |"]

# Indent each line of body for proper YAML block scalar formatting
for line in body.split("\n"):
lines.append(f" {line}")

# Add source comment at the end
lines.append("")
lines.append(f"# Source: {source_id}")

return "\n".join(lines)

def render_skill_command(
self,
agent_name: str,
Expand Down Expand Up @@ -511,6 +573,8 @@ def register_commands(
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)
elif agent_config["format"] == "yaml":
output = self.render_yaml_command(frontmatter, body, source_id)
else:
raise ValueError(f"Unsupported format: {agent_config['format']}")

Expand Down
Loading
Loading