From 0a477a9bc72be6a274959466ecfb15a7c9ad5fc3 Mon Sep 17 00:00:00 2001 From: ericnoam Date: Tue, 31 Mar 2026 19:58:10 +0200 Subject: [PATCH] refactor: rename forgecode agent key to forge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../scripts/create-release-packages.sh | 8 ++-- README.md | 12 +++--- src/specify_cli/__init__.py | 35 +++++----------- src/specify_cli/agents.py | 2 +- tests/test_core_pack_scaffold.py | 42 +++++++++++++++++-- 5 files changed, 60 insertions(+), 39 deletions(-) diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 204afca49..b6a5e6234 100755 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -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() { diff --git a/README.md b/README.md index 017b60ab2..4308d332c 100644 --- a/README.md +++ b/README.md @@ -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-`. | | [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 | | ---------------------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `` | 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 --ai claude --ignore-agent-tools diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 36b7c5748..b10a72d05 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -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) diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 5a2d3de86..102274b11 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -163,7 +163,7 @@ class CommandRegistrar: "args": "$ARGUMENTS", "extension": ".md" }, - "forgecode": { + "forge": { "dir": ".forge/commands", "format": "markdown", "args": "{{parameters}}", diff --git a/tests/test_core_pack_scaffold.py b/tests/test_core_pack_scaffold.py index 4959042fa..af2da13d5 100644 --- a/tests/test_core_pack_scaffold.py +++ b/tests/test_core_pack_scaffold.py @@ -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 # ---------------------------------------------------------------------------