Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
268e983
feat: add Forgecode (forge) agent support
ericnoam Mar 30, 2026
d83be82
fix: strip handoffs frontmatter and replace $ARGUMENTS for forgecode
ericnoam Mar 30, 2026
8128e60
feat: add name field injection for forgecode agent
ericnoam Mar 31, 2026
c1a93c9
test: update test_argument_token_format for forgecode special case
ericnoam Mar 31, 2026
b46bf65
docs: add forgecode to README documentation
ericnoam Mar 31, 2026
c69893c
fix: show 'forge' binary name in user-facing messages for forgecode
ericnoam Mar 31, 2026
0a477a9
refactor: rename forgecode agent key to forge
ericnoam Mar 31, 2026
635790d
fix: ensure forge alias commands have correct name in frontmatter
ericnoam Mar 31, 2026
506b7ef
feat: add forge to PowerShell script and fix test whitespace
ericnoam Mar 31, 2026
1de7851
fix: use .NET Regex.Replace for count-limited replacement in PowerShell
ericnoam Mar 31, 2026
a7deefc
Merge branch 'main' into feature/add-forgecode-agent-support
ericnoam Apr 1, 2026
d1360ef
Apply suggestion from @Copilot
ericnoam Apr 1, 2026
e3f269b
feat: migrate Forge agent to Python integration system
ericnoam Apr 2, 2026
f5fbfce
Merge branch 'main' into feature/add-forgecode-agent-support
ericnoam Apr 2, 2026
1294d43
fix: replace $ARGUMENTS with {{parameters}} in Forge templates
ericnoam Apr 2, 2026
7d06021
refactor: make ForgeIntegration extend MarkdownIntegration
ericnoam Apr 2, 2026
1009d92
style: remove trailing whitespace from test file
ericnoam Apr 2, 2026
8330f5a
style: remove trailing whitespace from Forge integration
ericnoam Apr 2, 2026
99d7c9a
test: derive expected commands from templates dynamically
ericnoam Apr 2, 2026
494879f
fix: make Forge update-context scripts handle AGENTS.md directly
ericnoam Apr 2, 2026
99e1c3f
feat: add Forge support to shared update-agent-context scripts
ericnoam Apr 2, 2026
90a1845
fix: resolve unbound variable and duplicate file update issues
ericnoam Apr 2, 2026
769a87f
Merge branch 'main' into feature/add-forgecode-agent-support
ericnoam Apr 2, 2026
4a57f79
fix: import timezone from datetime for rate limit header parsing
ericnoam Apr 2, 2026
9f0ef26
fix: correct variable scope in PowerShell deduplication and update docs
ericnoam Apr 2, 2026
ba67ebf
fix: resolve missing scaffold_from_core_pack import in tests
ericnoam Apr 2, 2026
794d421
Merge branch 'github:main' into feature/add-forgecode-agent-support
ericnoam Apr 2, 2026
89b935d
fix: prevent duplicate path prefixes and consolidate shared file updates
ericnoam Apr 2, 2026
053ee8a
Merge branch 'github:main' into feature/add-forgecode-agent-support
ericnoam Apr 2, 2026
59c4212
refactor: remove unused rate-limit helpers and improve PowerShell scr…
ericnoam Apr 2, 2026
8aad4e6
fix: add missing 'forge' to PowerShell usage text and fix agent order
ericnoam Apr 2, 2026
7265539
refactor: remove old architecture files deleted in b1832c9
ericnoam Apr 2, 2026
a077fff
refactor: remove unused timezone import from __init__.py
ericnoam Apr 2, 2026
afbe989
docs: clarify that handoffs is a Claude Code feature, not Forge's
ericnoam Apr 2, 2026
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
24 changes: 18 additions & 6 deletions .github/workflows/scripts/create-release-packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ rewrite_paths() {
}

generate_commands() {
local agent=$1 ext=$2 arg_format=$3 output_dir=$4 script_variant=$5
local agent=$1 ext=$2 arg_format=$3 output_dir=$4 script_variant=$5 extra_strip_key="${6:-}"
mkdir -p "$output_dir"
for template in templates/commands/*.md; do
[[ -f "$template" ]] || continue
Expand Down Expand Up @@ -95,18 +95,19 @@ generate_commands() {
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
fi

# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
body=$(printf '%s\n' "$body" | awk '
# Remove the scripts:, agent_scripts:, and any extra key sections from frontmatter
body=$(printf '%s\n' "$body" | awk -v extra_key="$extra_strip_key" '
/^---$/ { 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 && extra_key != "" && $0 ~ ("^"extra_key":") { skip_scripts=1; next }
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
{ print }
')

# Apply other substitutions
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
# Apply other substitutions; also replace $ARGUMENTS with the agent-specific placeholder
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed 's/\$ARGUMENTS/'"$arg_format"'/g' | sed "s/__AGENT__/$agent/g" | rewrite_paths)

case $ext in
toml)
Expand Down Expand Up @@ -330,6 +331,17 @@ build_variant() {
iflow)
mkdir -p "$base_dir/.iflow/commands"
generate_commands iflow md "\$ARGUMENTS" "$base_dir/.iflow/commands" "$script" ;;
forgecode)
mkdir -p "$base_dir/.forge/commands"
generate_commands forgecode md "{{parameters}}" "$base_dir/.forge/commands" "$script" "handoffs"
# Inject name field into frontmatter (forgecode requires name + description)
for _cmd_file in "$base_dir/.forge/commands/"*.md; do
[[ -f "$_cmd_file" ]] || continue
_cmd_name=$(basename "$_cmd_file" .md)
_tmp_file="${_cmd_file}.tmp"
awk -v name="$_cmd_name" 'NR==1 && /^---$/ { print; print "name: "name; next } { print }' "$_cmd_file" > "$_tmp_file"
mv "$_tmp_file" "$_cmd_file"
done ;;
generic)
mkdir -p "$base_dir/.speckit/commands"
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
Expand All @@ -339,7 +351,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 forgecode generic)
ALL_SCRIPTS=(sh ps)

validate_subset() {
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ Community projects that extend, visualize, or build on Spec Kit:
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
| [Codex CLI](https://github.com/openai/codex) | ✅ | Requires `--ai-skills`. Codex recommends [skills](https://developers.openai.com/codex/skills) and treats [custom prompts](https://developers.openai.com/codex/custom-prompts) as deprecated. Spec-kit installs Codex skills into `.agents/skills` and invokes them as `$speckit-<command>`. |
| [Cursor](https://cursor.sh/) | ✅ | |
| [Forgecode](https://forgecode.dev/) | ✅ | |
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | |
| [GitHub Copilot](https://code.visualstudio.com/) | ✅ | |
| [IBM Bob](https://www.ibm.com/products/bob) | ✅ | IDE-based agent with slash command support |
Expand Down Expand Up @@ -294,14 +295,14 @@ The `specify` command supports the following options:
| Command | Description |
| ------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `init` | Initialize a new Specify project from the latest template |
| `check` | Check for installed tools: `git` plus all CLI-based agents configured in `AGENT_CONFIG` (for example: `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `junie`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, etc.) |
| `check` | Check for installed tools: `git` plus all CLI-based agents configured in `AGENT_CONFIG` (for example: `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `junie`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, `forgecode`, etc.) |

### `specify init` Arguments & Options

| Argument/Option | Type | Description |
| ---------------------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
| `--ai` | Option | AI assistant to use (see `AGENT_CONFIG` for the full, up-to-date list). Common options include: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `junie`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, or `generic` (requires `--ai-commands-dir`) |
| `--ai` | Option | AI assistant to use (see `AGENT_CONFIG` for the full, up-to-date list). Common options include: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `junie`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, `forgecode`, or `generic` (requires `--ai-commands-dir`) |
| `--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) |
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
Expand Down Expand Up @@ -356,6 +357,9 @@ specify init my-project --ai codex --ai-skills
# Initialize with Antigravity support
specify init my-project --ai agy --ai-skills

# Initialize with Forgecode support
specify init my-project --ai forgecode

# Initialize with an unsupported agent (generic / bring your own agent)
specify init my-project --ai generic --ai-commands-dir .myagent/commands/

Expand Down Expand Up @@ -597,7 +601,7 @@ specify init . --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, Pi, 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, Tabnine CLI, Kiro CLI, Pi, Forgecode, or Mistral Vibe installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:

```bash
specify init <project_name> --ai claude --ignore-agent-tools
Expand Down
72 changes: 41 additions & 31 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def _github_auth_headers(cli_token: str | None = None) -> dict:
def _parse_rate_limit_headers(headers: httpx.Headers) -> dict:
"""Extract and parse GitHub rate-limit headers."""
info = {}

# Standard GitHub rate-limit headers
if "X-RateLimit-Limit" in headers:
info["limit"] = headers.get("X-RateLimit-Limit")
Expand All @@ -84,7 +84,7 @@ def _parse_rate_limit_headers(headers: httpx.Headers) -> dict:
info["reset_epoch"] = reset_epoch
info["reset_time"] = reset_time
info["reset_local"] = reset_time.astimezone()

# Retry-After header (seconds or HTTP-date)
if "Retry-After" in headers:
retry_after = headers.get("Retry-After")
Expand All @@ -93,16 +93,16 @@ def _parse_rate_limit_headers(headers: httpx.Headers) -> dict:
except ValueError:
# HTTP-date format - not implemented, just store as string
info["retry_after"] = retry_after

return info

def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) -> str:
"""Format a user-friendly error message with rate-limit information."""
rate_info = _parse_rate_limit_headers(headers)

lines = [f"GitHub API returned status {status_code} for {url}"]
lines.append("")

if rate_info:
lines.append("[bold]Rate Limit Information:[/bold]")
if "limit" in rate_info:
Expand All @@ -115,14 +115,14 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str)
if "retry_after_seconds" in rate_info:
lines.append(f" • Retry after: {rate_info['retry_after_seconds']} seconds")
lines.append("")

# Add troubleshooting guidance
lines.append("[bold]Troubleshooting Tips:[/bold]")
lines.append(" • If you're on a shared CI or corporate environment, you may be rate-limited.")
lines.append(" • Consider using a GitHub token via --github-token or the GH_TOKEN/GITHUB_TOKEN")
lines.append(" environment variable to increase rate limits.")
lines.append(" • Authenticated requests have a limit of 5,000/hour vs 60/hour for unauthenticated.")

return "\n".join(lines)

# Agent configuration with name, folder, install URL, CLI tool requirement, and commands subdirectory
Expand Down 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,
},
"forgecode": {
"name": "Forge",
"folder": ".forge/",
"commands_subdir": "commands",
"install_url": "https://forgecode.dev/docs/",
"requires_cli": True,
},
"generic": {
"name": "Generic (bring your own agent)",
"folder": None, # Set dynamically via --ai-commands-dir
Expand Down Expand Up @@ -350,10 +357,10 @@ def _build_ai_assistant_help() -> str:
BANNER = """
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝
███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝
╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝
███████║██║ ███████╗╚██████╗██║██║ ██║
╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝
███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝
╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝
███████║██║ ███████╗╚██████╗██║██║ ██║
╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝
"""

TAGLINE = "GitHub Spec Kit - Spec-Driven Development Toolkit"
Expand Down Expand Up @@ -465,12 +472,12 @@ def get_key():
def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str:
"""
Interactive selection using arrow keys with Rich Live display.

Args:
options: Dict with keys as option keys and values as descriptions
prompt_text: Text to show above the options
default_key: Default option key to start with

Returns:
Selected option key
"""
Expand Down Expand Up @@ -598,11 +605,11 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False

def check_tool(tool: str, tracker: StepTracker = None) -> bool:
"""Check if a tool is installed. Optionally update tracker.

Args:
tool: Name of the tool to check
tracker: Optional StepTracker to update with results

Returns:
True if tool is found, False otherwise
"""
Expand All @@ -618,27 +625,30 @@ def check_tool(tool: str, tracker: StepTracker = None) -> bool:
if tracker:
tracker.complete(tool, "available")
return True

if tool == "kiro-cli":
# Kiro currently supports both executable names. Prefer kiro-cli and
# accept kiro as a compatibility fallback.
found = shutil.which("kiro-cli") is not None or shutil.which("kiro") is not None
elif tool == "forgecode":
# The forgecode agent key is 'forgecode' but the CLI binary is 'forge'.
found = shutil.which("forge") is not None
else:
found = shutil.which(tool) is not None

if tracker:
if found:
tracker.complete(tool, "available")
else:
tracker.error(tool, "not found")

return found

def is_git_repo(path: Path = None) -> bool:
"""Check if the specified path is inside a git repository."""
if path is None:
path = Path.cwd()

if not path.is_dir():
return False

Expand All @@ -656,11 +666,11 @@ def is_git_repo(path: Path = None) -> bool:

def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Optional[str]]:
"""Initialize a git repository in the specified path.

Args:
project_path: Path to initialize git repository in
quiet: if True suppress console output (tracker handles status)

Returns:
Tuple of (success: bool, error_message: Optional[str])
"""
Expand All @@ -682,7 +692,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Option
error_msg += f"\nError: {e.stderr.strip()}"
elif e.stdout:
error_msg += f"\nOutput: {e.stdout.strip()}"

if not quiet:
console.print(f"[red]Error initializing git repository:[/red] {e}")
return False, error_msg
Expand Down Expand Up @@ -1879,7 +1889,7 @@ def init(
console.print("[yellow]Example:[/yellow] specify init --ai claude --here")
console.print(f"[yellow]Available agents:[/yellow] {', '.join(AGENT_CONFIG.keys())}")
raise typer.Exit(1)

if ai_commands_dir and ai_commands_dir.startswith("--"):
console.print(f"[red]Error:[/red] Invalid value for --ai-commands-dir: '{ai_commands_dir}'")
console.print("[yellow]Hint:[/yellow] Did you forget to provide a value for --ai-commands-dir?")
Expand Down Expand Up @@ -1949,8 +1959,8 @@ def init(
# Create options dict for selection (agent_key: display_name)
ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()}
selected_ai = select_with_arrows(
ai_choices,
"Choose your AI assistant:",
ai_choices,
"Choose your AI assistant:",
"copilot"
)

Expand Down Expand Up @@ -2297,7 +2307,7 @@ def init(

console.print(tracker.render())
console.print("\n[bold green]Project ready.[/bold green]")

# Show git error details if initialization failed
if git_error_message:
console.print()
Expand Down Expand Up @@ -2432,9 +2442,9 @@ def version():
"""Display version and system information."""
import platform
import importlib.metadata

show_banner()

# Get CLI version from package metadata
cli_version = "unknown"
try:
Expand All @@ -2450,15 +2460,15 @@ def version():
cli_version = data.get("project", {}).get("version", "unknown")
except Exception:
pass

# Fetch latest template release version
repo_owner = "github"
repo_name = "spec-kit"
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"

template_version = "unknown"
release_date = "unknown"

try:
response = client.get(
api_url,
Expand Down
Loading
Loading