refactor: rename forgecode agent key to forge

Aligns with AGENTS.md design principle: "Use the actual CLI tool
name as the key, not a shortened version" (AGENTS.md:61-83).

The actual CLI executable is 'forge', so the AGENT_CONFIG key should
be 'forge' (not 'forgecode'). This follows the same pattern as other
agents like cursor-agent and kiro-cli.

Changes:
- Renamed AGENT_CONFIG key: "forgecode" → "forge"
- Removed cli_binary field (no longer needed)
- Simplified check_tool() - removed cli_binary lookup logic
- Simplified init() and check() - removed display_key mapping
- Updated all tests: test_forge_name_field_in_frontmatter
- Updated documentation: README.md

Code simplification:
- Removed 6 lines of workaround code
- Removed 1 function parameter (display_key)
- Eliminated all special-case logic for forge

Note: No backward compatibility needed - forge is a new agent
being introduced in this PR.
This commit is contained in:
ericnoam
2026-03-31 19:58:10 +02:00
parent c69893c1f7
commit 0a477a9bc7
5 changed files with 60 additions and 39 deletions

View File

@@ -331,10 +331,10 @@ build_variant() {
iflow)
mkdir -p "$base_dir/.iflow/commands"
generate_commands iflow md "\$ARGUMENTS" "$base_dir/.iflow/commands" "$script" ;;
forgecode)
forge)
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)
generate_commands forge md "{{parameters}}" "$base_dir/.forge/commands" "$script" "handoffs"
# Inject name field into frontmatter (forge requires name + description)
for _cmd_file in "$base_dir/.forge/commands/"*.md; do
[[ -f "$_cmd_file" ]] || continue
_cmd_name=$(basename "$_cmd_file" .md)
@@ -351,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 forgecode 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 forge generic)
ALL_SCRIPTS=(sh ps)
validate_subset() {

View File

@@ -265,7 +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/) | ✅ | |
| [Forge](https://forgecode.dev/) | ✅ | CLI tool: `forge` |
| [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 |
@@ -295,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`, `forgecode`, 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`, `forge`, 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`, `forgecode`, 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`, `forge`, 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 |
@@ -357,8 +357,8 @@ 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 Forge support
specify init my-project --ai forge
# Initialize with an unsupported agent (generic / bring your own agent)
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
@@ -601,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, 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:
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, Forge, 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

View File

@@ -302,13 +302,12 @@ AGENT_CONFIG = {
"install_url": "https://docs.iflow.cn/en/cli/quickstart",
"requires_cli": True,
},
"forgecode": {
"forge": {
"name": "Forge",
"folder": ".forge/",
"commands_subdir": "commands",
"install_url": "https://forgecode.dev/docs/",
"requires_cli": True,
"cli_binary": "forge", # The actual executable users must install
},
"generic": {
"name": "Generic (bring your own agent)",
@@ -604,20 +603,16 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False
raise
return None
def check_tool(tool: str, tracker: StepTracker = None, display_key: str = None) -> bool:
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 (agent key from AGENT_CONFIG)
tool: Name of the tool to check
tracker: Optional StepTracker to update with results
display_key: Optional key to use for tracker display (defaults to tool)
Returns:
True if tool is found, False otherwise
"""
# Use display_key for tracker if provided, otherwise use tool
tracker_key = display_key if display_key else tool
# Special handling for Claude CLI local installs
# See: https://github.com/github/spec-kit/issues/123
# See: https://github.com/github/spec-kit/issues/550
@@ -628,7 +623,7 @@ def check_tool(tool: str, tracker: StepTracker = None, display_key: str = None)
if tool == "claude":
if CLAUDE_LOCAL_PATH.is_file() or CLAUDE_NPM_LOCAL_PATH.is_file():
if tracker:
tracker.complete(tracker_key, "available")
tracker.complete(tool, "available")
return True
if tool == "kiro-cli":
@@ -636,17 +631,13 @@ def check_tool(tool: str, tracker: StepTracker = None, display_key: str = None)
# accept kiro as a compatibility fallback.
found = shutil.which("kiro-cli") is not None or shutil.which("kiro") is not None
else:
# Check if this tool has a custom cli_binary name in AGENT_CONFIG
cli_binary = tool
if tool in AGENT_CONFIG and "cli_binary" in AGENT_CONFIG[tool]:
cli_binary = AGENT_CONFIG[tool]["cli_binary"]
found = shutil.which(cli_binary) is not None
found = shutil.which(tool) is not None
if tracker:
if found:
tracker.complete(tracker_key, "available")
tracker.complete(tool, "available")
else:
tracker.error(tracker_key, "not found")
tracker.error(tool, "not found")
return found
@@ -2015,10 +2006,9 @@ def init(
agent_config = AGENT_CONFIG.get(selected_ai)
if agent_config and agent_config["requires_cli"]:
install_url = agent_config["install_url"]
cli_binary = agent_config.get("cli_binary", selected_ai)
if not check_tool(selected_ai):
error_panel = Panel(
f"[cyan]{cli_binary}[/cyan] not found\n"
f"[cyan]{selected_ai}[/cyan] not found\n"
f"Install from: [cyan]{install_url}[/cyan]\n"
f"{agent_config['name']} is required to continue with this project type.\n\n"
"Tip: Use [cyan]--ignore-agent-tools[/cyan] to skip this check",
@@ -2417,17 +2407,14 @@ def check():
continue # Generic is not a real agent to check
agent_name = agent_config["name"]
requires_cli = agent_config["requires_cli"]
# Use cli_binary for display if specified, otherwise use agent_key
display_key = agent_config.get("cli_binary", agent_key)
tracker.add(display_key, agent_name)
tracker.add(agent_key, agent_name)
if requires_cli:
agent_results[agent_key] = check_tool(agent_key, tracker=tracker, display_key=display_key)
agent_results[agent_key] = check_tool(agent_key, tracker=tracker)
else:
# IDE-based agent - skip CLI check and mark as optional
tracker.skip(display_key, "IDE-based, no CLI check")
tracker.skip(agent_key, "IDE-based, no CLI check")
agent_results[agent_key] = False # Don't count IDE agents as "found"
# Check VS Code variants (not in agent config)

View File

@@ -163,7 +163,7 @@ class CommandRegistrar:
"args": "$ARGUMENTS",
"extension": ".md"
},
"forgecode": {
"forge": {
"dir": ".forge/commands",
"format": "markdown",
"args": "{{parameters}}",

View File

@@ -351,7 +351,7 @@ _TEMPLATES_WITH_ARGS: frozenset[str] = frozenset(
def test_argument_token_format(agent, scaffolded_sh):
"""For templates that carry an {ARGS} token:
- TOML agents must emit {{args}}
- Forgecode must emit {{parameters}}
- Forge must emit {{parameters}}
- Other Markdown agents must emit $ARGUMENTS
Templates without {ARGS} (e.g. implement, plan) are skipped.
"""
@@ -375,10 +375,10 @@ def test_argument_token_format(agent, scaffolded_sh):
assert "{{args}}" in content, (
f"TOML agent '{agent}': expected '{{{{args}}}}' in '{f.name}'"
)
elif agent == "forgecode":
# Forgecode uses {{parameters}} instead of $ARGUMENTS
elif agent == "forge":
# Forge uses {{parameters}} instead of $ARGUMENTS
assert "{{parameters}}" in content, (
f"Forgecode agent: expected '{{{{parameters}}}}' in '{f.name}'"
f"Forge agent: expected '{{{{parameters}}}}' in '{f.name}'"
)
else:
assert "$ARGUMENTS" in content, (
@@ -464,6 +464,40 @@ def test_markdown_has_frontmatter(agent, scaffolded_sh):
)
def test_forge_name_field_in_frontmatter(scaffolded_sh):
"""Forge: every command file must have a 'name' field in frontmatter that matches the filename.
Forge requires both 'name' and 'description' fields in command frontmatter.
This test ensures the release script's name injection is working correctly.
"""
project = scaffolded_sh("forge")
cmd_dir = _expected_cmd_dir(project, "forge")
for f in _list_command_files(cmd_dir, "forge"):
content = f.read_text(encoding="utf-8")
assert content.startswith("---"), (
f"No YAML frontmatter in '{f.name}'"
)
parts = content.split("---", 2)
assert len(parts) >= 3, f"Incomplete frontmatter in '{f.name}'"
fm = yaml.safe_load(parts[1])
assert fm is not None, f"Empty frontmatter in '{f.name}'"
# Check that 'name' field exists
assert "name" in fm, (
f"'name' key missing from frontmatter in '{f.name}' - "
f"Forge requires both 'name' and 'description' fields"
)
# Check that name matches the filename (without extension)
expected_name = f.name.removesuffix(".md")
actual_name = fm["name"]
assert actual_name == expected_name, (
f"Frontmatter 'name' field ({actual_name}) does not match "
f"filename ({expected_name}) in '{f.name}'"
)
# ---------------------------------------------------------------------------
# 6. Copilot-specific: companion .prompt.md files
# ---------------------------------------------------------------------------