Compare commits

..

5 Commits

Author SHA1 Message Date
dependabot[bot]
6fe3ff7890 chore(deps): bump actions/configure-pages from 5 to 6
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 5 to 6.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 21:53:17 +00:00
Manfred Riem
cb508d7a36 chore: release 0.5.0, begin 0.5.1.dev0 development (#2070)
* chore: bump version to 0.5.0

* chore: begin 0.5.1.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-02 16:42:43 -05:00
Eric Rodriguez Suazo
b8e7851234 feat: add Forgecode agent support (#2034)
* feat: add Forgecode (forge) agent support

- Add 'forgecode' to AGENT_CONFIGS in agents.py with .forge/commands
  directory, markdown format, and {{parameters}} argument placeholder
- Add 'forgecode' to AGENT_CONFIG in __init__.py with .forge/ folder,
  install URL, and requires_cli=True
- Add forgecode binary check in check_tool() mapping agent key
  'forgecode' to the actual 'forge' CLI binary
- Add forgecode case to build_variant() in create-release-packages.sh
  generating commands into .forge/commands/ with {{parameters}}
- Add forgecode to ALL_AGENTS in create-release-packages.sh

* fix: strip handoffs frontmatter and replace $ARGUMENTS for forgecode

The forgecode agent hangs when listing commands because the 'handoffs'
frontmatter field (a Claude Code-specific feature) contains 'send: true'
entries that forge tries to act on when indexing .forge/commands/ files.

Additionally, $ARGUMENTS in command bodies was never replaced with
{{parameters}}, so user input was not passed through to commands.

Python path (agents.py):
- Add strip_frontmatter_keys: [handoffs] to the forgecode AGENT_CONFIG
  entry so register_commands drops the key before rendering

Bash path (create-release-packages.sh):
- Add extra_strip_key parameter to generate_commands; pass 'handoffs'
  for the forgecode case in build_variant
- Use regex prefix match (~ "^"extra_key":") instead of exact
  equality to handle trailing whitespace after the YAML key
- Add sed replacement of $ARGUMENTS -> $arg_format in the body
  pipeline so {{parameters}} is substituted in forgecode command files

* feat: add name field injection for forgecode agent

Forgecode requires both 'name' and 'description' fields in command
frontmatter. This commit adds automatic injection of the 'name' field
during command generation for forgecode.

Changes:
- Python (agents.py): Add inject_name: True to forgecode config and
  implement name injection logic in register_commands
- Bash (create-release-packages.sh): Add post-processing step to inject
  name field into frontmatter after command generation

This complements the existing handoffs stripping fix (d83be82) to fully
support forgecode command requirements.

* test: update test_argument_token_format for forgecode special case

Forgecode uses {{parameters}} instead of the standard $ARGUMENTS
placeholder. Updated test to check for the correct placeholder format
for forgecode agent.

- Added special case handling for forgecode in test_argument_token_format
- Updated docstring to document forgecode's {{parameters}} format
- Test now passes for all 26 agents including forgecode

* docs: add forgecode to README documentation

Added forgecode agent to all relevant sections:
- Added to Supported AI Agents table
- Added to --ai option description
- Added to specify check command examples
- Added initialization example
- Added to CLI tools check list in detailed walkthrough

Forgecode is now fully documented alongside other supported agents.

* fix: show 'forge' binary name in user-facing messages for forgecode

Addresses Copilot PR feedback: Users should see the actual executable
name 'forge' in status and error messages, not the agent key 'forgecode'.

Changes:
- Added 'cli_binary' field to forgecode AGENT_CONFIG (set to 'forge')
- Updated check_tool() to accept optional display_key parameter
- Updated check_tool() to use cli_binary from AGENT_CONFIG when available
- Updated check() command to display cli_binary in StepTracker
- Updated init() error message to show cli_binary instead of agent key

UX improvements:
- 'specify check' now shows: '● forge (available/not found)'
- 'specify init --ai forgecode' error shows: 'forge not found'
  (instead of confusing 'forgecode not found')

This makes it clear to users that they need to install the 'forge'
binary, even though they selected the 'forgecode' agent.

* 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.

* fix: ensure forge alias commands have correct name in frontmatter

When inject_name is enabled (for forge), alias command files must
have their own name field in frontmatter, not reuse the primary
command's name. This is critical for Forge's command discovery
and dispatch system.

Changes:
- For agents with inject_name, create a deepcopy of frontmatter
  for each alias and set the name to the alias name
- Re-render the command content with the alias-specific frontmatter
- Ensures each alias file has the correct name field matching its
  filename

This fixes command discovery issues where forge would try to invoke
aliases using the primary command's name.

* feat: add forge to PowerShell script and fix test whitespace

1. PowerShell script (create-release-packages.ps1):
   - Added forge agent support for Windows users
   - Enables `specify init --ai forge --offline` on Windows
   - Enhanced Generate-Commands with ExtraStripKey parameter
   - Added frontmatter stripping for handoffs key
   - Added $ARGUMENTS replacement for {{parameters}}
   - Implemented forge case with name field injection
   - Complete parity with bash script

2. Test file (test_core_pack_scaffold.py):
   - Removed trailing whitespace from blank lines
   - Cleaner diffs and no linter warnings

Addresses Copilot PR feedback on both issues.

* fix: use .NET Regex.Replace for count-limited replacement in PowerShell

Addresses Copilot feedback: PowerShell's -replace operator does not
support a third argument for replacement count. Using it causes an
error or mis-parsing that would break forge package generation on
Windows.

Changed from:
  $content -replace '(?m)^---$', "---`nname: $cmdName", 1

To:
  $regex = [regex]'(?m)^---$'
  $content = $regex.Replace($content, "---`nname: $cmdName", 1)

The .NET Regex.Replace() method properly supports the count parameter,
ensuring the name field is injected only after the first frontmatter
delimiter (not the closing one).

This fix is critical for Windows users running:
  specify init --ai forge --offline

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: migrate Forge agent to Python integration system

- Create ForgeIntegration class with custom processing for {{parameters}}, handoffs stripping, and name injection
- Add update-context scripts (bash and PowerShell) for Forge
- Register Forge in integration registry
- Update AGENTS.md with Forge documentation and special processing requirements section
- Add comprehensive test suite (11 tests, all passing)

Closes migration from release packaging to Python-based scaffolding for Forge agent.

* fix: replace $ARGUMENTS with {{parameters}} in Forge templates

- Add replacement of $ARGUMENTS to {{parameters}} after template processing
- Use arg_placeholder from config (Copilot's cleaner approach)
- Remove unused 'import re' from _apply_forge_transformations()
- Enhance tests to verify $ARGUMENTS replacement works correctly
- All 11 tests pass

Fixes template processing to ensure Forge receives user-supplied parameters correctly.

* refactor: make ForgeIntegration extend MarkdownIntegration

- Change base class from IntegrationBase to MarkdownIntegration
- Eliminates ~30 lines of duplicated validation/setup boilerplate
- Aligns with the pattern used by 20+ other markdown agents (Bob, Claude, Windsurf, etc.)
- Update AGENTS.md to reflect new inheritance hierarchy
- All Forge-specific processing retained ({{parameters}}, handoffs stripping, name injection)
- All 535 integration tests pass

This addresses reviewer feedback about using the MarkdownIntegration convenience base class.

* style: remove trailing whitespace from test file

- Strip trailing spaces from blank lines in test_integration_forge.py
- Fixes W291 linting warnings
- No functional changes

* style: remove trailing whitespace from Forge integration

- Strip trailing spaces from blank lines in __init__.py
- Fixes whitespace on lines 20, 86, 90, 93, 139, 143
- Verified other files in forge/ directory have no trailing whitespace
- No functional changes, all tests pass

* test: derive expected commands from templates dynamically

- Remove hard-coded command count (9) and command set from test_directory_structure
- Use forge.list_command_templates() to derive expected commands
- Test now auto-syncs when core command templates are added/removed
- Prevents test breakage when template set changes
- All 11 tests pass

* fix: make Forge update-context scripts handle AGENTS.md directly

- Add fallback logic to update/create AGENTS.md when shared script doesn't support forge yet
- Check if shared dispatcher knows about 'forge' before delegating
- If shared script doesn't support forge, handle AGENTS.md updates directly:
  - Add Forge section to existing AGENTS.md if not present
  - Create new AGENTS.md with Forge section if file doesn't exist
- Both bash and PowerShell scripts implement same logic
- Prevents 'Unknown agent type' errors until shared scripts add forge support
- Future-compatible: automatically delegates when shared script supports forge

Addresses reviewer feedback about update-context scripts failing without forge support.

* feat: add Forge support to shared update-agent-context scripts

- Add forge case to bash and PowerShell update-agent-context scripts
- Add FORGE_FILE variable mapping to AGENTS.md (like opencode/codex/pi)
- Add forge to all usage/help text and ValidateSet parameters
- Include forge in update_all_existing_agents functions

Wrapper script improvements:
- Simplify Forge wrapper scripts to unconditionally delegate to shared script
- Remove complex fallback logic that created stub AGENTS.md files
- Add clear error messages if shared script is missing/not executable
- Align with pattern used by other integrations (opencode, bob, etc.)

Benefits:
- Plan command's {AGENT_SCRIPT} now works for Forge users
- No more incomplete/stub context files masking missing support
- Cleaner, more maintainable code (-39 lines in wrappers)
- Consistent architecture across all integrations

Update AGENTS.md to document that Forge integration ensures shared scripts
include forge support for context updates.

Addresses reviewer feedback about Forge support being incomplete for
workflow steps that run {AGENT_SCRIPT}.

* fix: resolve unbound variable and duplicate file update issues

- Fix undefined FORGE_FILE variable in bash update-agent-context.sh
  - Add missing FORGE_FILE definition pointing to AGENTS.md
  - Update comment to include Forge in list of agents sharing AGENTS.md
  - Prevents crash with 'set -u' when running without explicit agent type

- Add deduplication logic to PowerShell update-agent-context.ps1
  - Implement Update-IfNew helper to track processed files by real path
  - Prevents AGENTS.md from being rewritten multiple times
  - Matches existing deduplication behavior in bash script

- Prevent duplicate YAML keys in Forge frontmatter injection
  - Check for existing 'name:' field before injection in both scripts
  - PowerShell: Parse frontmatter to detect existing name field
  - Bash: Enhanced awk script to check frontmatter state
  - Future-proofs against template changes that add name fields

All scripts now have consistent behavior and proper error handling.

* fix: import timezone from datetime for rate limit header parsing

The _parse_rate_limit_headers() function uses timezone.utc on line 82
but timezone was never imported from datetime. This would raise a
NameError the first time GitHub API rate-limit headers are parsed.

Import timezone alongside datetime to fix the missing import.

* fix: correct variable scope in PowerShell deduplication and update docs

- Fix Update-IfNew in PowerShell update-agent-context.ps1
  - Changed from $script: scope to Set-Variable -Scope 1
  - Properly mutates parent function's local variables
  - Fixes deduplication tracking for shared AGENTS.md file
  - Prevents incorrect default Claude file creation

- Update create-release-packages.sh documentation
  - Add missing 'forge' to AGENTS list in header comment
  - Documentation now matches actual ALL_AGENTS array

Without this fix, AGENTS.md would be updated multiple times (once
for each agent sharing it: opencode, codex, amp, kiro, bob, pi, forge)
and the script would always create a default Claude file even when
agent files exist.

* fix: resolve missing scaffold_from_core_pack import in tests

The test_core_pack_scaffold.py imports scaffold_from_core_pack from
specify_cli, but that symbol does not exist in the current codebase.
This causes an ImportError when the test module is loaded.

Implement a resilient resolver that:
- Tries scaffold_from_core_pack first (expected name)
- Falls back to alternative names (scaffold_from_release_pack, etc.)
- Gracefully skips tests if no compatible entrypoint exists

This prevents import-time failures and makes the test future-proof
for when the actual scaffolding function is added or restored.

* fix: prevent duplicate path prefixes and consolidate shared file updates

PowerShell release script:
- Add deduplication pass to Rewrite-Paths function
- Prevents .specify.specify/ double prefixes in generated commands
- Matches bash script behavior with regex '(?:\.specify/){2,}' -> '.specify/'

Bash update-agent-context script:
- Consolidate AGENTS.md updates to single call
- Remove redundant calls for $AMP_FILE, $KIRO_FILE, $BOB_FILE, $FORGE_FILE
- Update label to 'Codex/opencode/Amp/Kiro/Bob/Pi/Forge' to reflect all agents
- Prevents always-deduped $FORGE_FILE call that never executed

Both fixes improve efficiency and correctness while maintaining parity
between bash and PowerShell implementations.

* refactor: remove unused rate-limit helpers and improve PowerShell scripts

- Remove unused _parse_rate_limit_headers() and _format_rate_limit_error()
  from src/specify_cli/__init__.py (56 lines of dead code)
- Add GENRELEASES_DIR override support to PowerShell release script with
  comprehensive safety checks (parity with bash script)
- Remove redundant shared-file update calls from PowerShell agent context
  script (AMP_FILE, KIRO_FILE, BOB_FILE, FORGE_FILE all resolve to AGENTS.md)
- Update test docstring to accurately reflect Forge's {{parameters}} token

Changes align PowerShell scripts with bash equivalents and reduce maintenance
burden by removing dead code.

* fix: add missing 'forge' to PowerShell usage text and fix agent order

- Add 'forge' to usage message in Print-Summary (was missing from list)
- Reorder ValidateSet to match bash script order (vibe before qodercli)

This ensures PowerShell script documentation matches bash script and includes
all supported agents consistently.

* refactor: remove old architecture files deleted in b1832c9

Remove files that were deleted in b1832c9 (Stage 6 migration) but remained
on this branch due to merge conflicts:

- Remove .github/workflows/scripts/create-release-packages.{sh,ps1}
  (replaced by inline release.yml + uv tool install)
- Remove tests/test_core_pack_scaffold.py
  (scaffold system removed, tests no longer relevant)

These files existed on the feature branch because they were modified before
b1832c9 landed. The merge kept our versions, but they should be deleted to
align with the new integration-only architecture.

This PR now focuses purely on adding NEW Forge integration support, not
restoring old architecture.

* refactor: remove unused timezone import from __init__.py

Remove unused timezone import that was added in 4a57f79 for rate-limit
header parsing but became obsolete when rate-limit helper functions were
removed in 59c4212 (and also removed in upstream b1832c9).

No functional changes - purely cleanup of unused import.

* docs: clarify that handoffs is a Claude Code feature, not Forge's

Update docstrings to accurately explain that the 'handoffs' frontmatter key
is from Claude Code (for multi-agent collaboration) and is stripped because
it causes Forge to hang, not because it's a Forge-specific feature.

Changes:
- Module docstring: 'Forge-specific collaboration feature' → 'Claude Code feature that causes Forge to hang'
- Class docstring: Add '(incompatible with Forge)' clarification
- Method docstring: Add '(from Claude Code templates; incompatible with Forge)' context

This avoids implying that handoffs belongs to Forge when it actually comes
from spec-kit templates designed for Claude Code compatibility.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-02 16:40:43 -05:00
PChemGuy
08f69e3d3e Introduces DEVELOPMENT.md (#2069)
* Create DEVELOPMENT.md outline

* AI-generated DEVELOPMENT.md draft

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Create Untitled 2.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Compact DEVELOPMENT.md

* Create DEVELOPMENT.md outline

* AI-generated DEVELOPMENT.md draft

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Create Untitled 2.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Update DEVELOPMENT.md

* Compact DEVELOPMENT.md

* Update DEVELOPMENT.md
2026-04-02 15:01:48 -05:00
Roland Huß
c8ccb0609d Update cc-sdd reference to cc-spex in Community Friends (#2007)
The cc-sdd project has been renamed to cc-spex (v3.0.0).

Assisted-By: 🤖 Claude Code
2026-04-02 13:52:17 -05:00
15 changed files with 923 additions and 298 deletions

View File

@@ -47,7 +47,7 @@ jobs:
docfx docfx.json
- name: Setup Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v3

588
AGENTS.md
View File

@@ -10,278 +10,277 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their
---
## Integration Architecture
## Adding New Agent Support
Each AI agent is a self-contained **integration subpackage** under `src/specify_cli/integrations/<key>/`. The package exposes a single class that declares all metadata, inherits setup/teardown logic from a base class, and registers itself in the global `INTEGRATION_REGISTRY` at import time.
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.
```
src/specify_cli/integrations/
├── __init__.py # INTEGRATION_REGISTRY + _register_builtins()
├── base.py # IntegrationBase, MarkdownIntegration, TomlIntegration, SkillsIntegration
├── manifest.py # IntegrationManifest (file tracking)
├── claude/ # Example: SkillsIntegration subclass
│ ├── __init__.py # ClaudeIntegration class
│ └── scripts/ # Thin wrapper scripts
│ ├── update-context.sh
│ └── update-context.ps1
├── gemini/ # Example: TomlIntegration subclass
│ ├── __init__.py
│ └── scripts/
├── windsurf/ # Example: MarkdownIntegration subclass
│ ├── __init__.py
│ └── scripts/
├── copilot/ # Example: IntegrationBase subclass (custom setup)
│ ├── __init__.py
│ └── scripts/
└── ... # One subpackage per supported agent
```
### Overview
The registry is the **single source of truth**. Supported agents, their directories, formats, and capabilities are all derived from the integration classes — no separate tables or config dicts to maintain.
Specify supports multiple AI agents by generating agent-specific command files and directory structures when initializing projects. Each agent has its own conventions for:
---
- **Command file formats** (Markdown, TOML, etc.)
- **Directory structures** (`.claude/commands/`, `.windsurf/workflows/`, etc.)
- **Command invocation patterns** (slash commands, CLI tools, etc.)
- **Argument passing conventions** (`$ARGUMENTS`, `{{args}}`, etc.)
## Adding a New Integration
### Current Supported Agents
### 1. Choose a base class
| Agent | Directory | Format | CLI Tool | Description |
| -------------------------- | ---------------------- | -------- | --------------- | --------------------------- |
| **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code 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 |
| **Cursor** | `.cursor/commands/` | Markdown | N/A (IDE-based) | Cursor IDE (`--ai cursor-agent`) |
| **Qwen Code** | `.qwen/commands/` | Markdown | `qwen` | Alibaba's Qwen Code CLI |
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
| **Codex CLI** | `.agents/skills/` | Markdown | `codex` | Codex CLI (`--ai codex --ai-skills`) |
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
| **Junie** | `.junie/commands/` | Markdown | `junie` | Junie by JetBrains |
| **Kilo Code** | `.kilocode/workflows/` | Markdown | N/A (IDE-based) | Kilo Code IDE |
| **Auggie CLI** | `.augment/commands/` | Markdown | `auggie` | Auggie CLI |
| **Roo Code** | `.roo/commands/` | Markdown | N/A (IDE-based) | Roo Code IDE |
| **CodeBuddy CLI** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy CLI |
| **Qoder CLI** | `.qoder/commands/` | Markdown | `qodercli` | Qoder CLI |
| **Kiro CLI** | `.kiro/prompts/` | Markdown | `kiro-cli` | Kiro CLI |
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp 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) |
| **Pi Coding Agent** | `.pi/prompts/` | Markdown | `pi` | Pi terminal coding agent |
| **iFlow CLI** | `.iflow/commands/` | Markdown | `iflow` | iFlow CLI (iflow-ai) |
| **Forge** | `.forge/commands/` | Markdown | `forge` | Forge CLI (forgecode.dev) |
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
| **Trae** | `.trae/rules/` | Markdown | N/A (IDE-based) | Trae IDE |
| **Antigravity** | `.agent/commands/` | Markdown | N/A (IDE-based) | Antigravity IDE (`--ai agy --ai-skills`) |
| **Mistral Vibe** | `.vibe/prompts/` | Markdown | `vibe` | Mistral Vibe CLI |
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
| Your agent needs… | Subclass |
|---|---|
| Standard markdown commands (`.md`) | `MarkdownIntegration` |
| TOML-format commands (`.toml`) | `TomlIntegration` |
| Skill directories (`speckit-<name>/SKILL.md`) | `SkillsIntegration` |
| Fully custom output (companion files, settings merge, etc.) | `IntegrationBase` directly |
### Step-by-Step Integration Guide
Most agents only need `MarkdownIntegration` — a minimal subclass with zero method overrides.
Follow these steps to add a new agent (using a hypothetical new agent as an example):
### 2. Create the subpackage
#### 1. Add to AGENT_CONFIG
Create `src/specify_cli/integrations/<key>/__init__.py`. The `key` **must match the actual CLI tool name** (the executable users install and run). Use a Python-safe directory name if the key contains hyphens (e.g., `kiro_cli/` for key `"kiro-cli"`).
**IMPORTANT**: Use the actual CLI tool name as the key, not a shortened version.
**Minimal example — Markdown agent (Windsurf):**
Add the new agent to the `AGENT_CONFIG` dictionary in `src/specify_cli/__init__.py`. This is the **single source of truth** for all agent metadata:
```python
"""Windsurf IDE integration."""
from ..base import MarkdownIntegration
class WindsurfIntegration(MarkdownIntegration):
key = "windsurf"
config = {
"name": "Windsurf",
"folder": ".windsurf/",
"commands_subdir": "workflows",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".windsurf/workflows",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = ".windsurf/rules/specify-rules.md"
AGENT_CONFIG = {
# ... existing agents ...
"new-agent-cli": { # Use the ACTUAL CLI tool name (what users type in terminal)
"name": "New Agent Display Name",
"folder": ".newagent/", # Directory for agent files
"commands_subdir": "commands", # Subdirectory name for command files (default: "commands")
"install_url": "https://example.com/install", # URL for installation docs (or None if IDE-based)
"requires_cli": True, # True if CLI tool required, False for IDE-based agents
},
}
```
**TOML agent (Gemini):**
**Key Design Principle**: The dictionary key should match the actual executable name that users install. For example:
- ✅ Use `"cursor-agent"` because the CLI tool is literally called `cursor-agent`
- ❌ Don't use `"cursor"` as a shortcut if the tool is `cursor-agent`
This eliminates the need for special-case mappings throughout the codebase.
**Field Explanations**:
- `name`: Human-readable display name shown to users
- `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"`)
- Most agents use `"commands"` (e.g., `.claude/commands/`)
- Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode), `"prompts"` (codex, kiro-cli, pi), `"command"` (opencode - singular)
- 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)
- `requires_cli`: Whether the agent requires a CLI tool check during initialization
#### 2. Update CLI Help Text
Update the `--ai` parameter help text in the `init()` command to include the new agent:
```python
"""Gemini CLI integration."""
from ..base import TomlIntegration
class GeminiIntegration(TomlIntegration):
key = "gemini"
config = {
"name": "Gemini CLI",
"folder": ".gemini/",
"commands_subdir": "commands",
"install_url": "https://github.com/google-gemini/gemini-cli",
"requires_cli": True,
}
registrar_config = {
"dir": ".gemini/commands",
"format": "toml",
"args": "{{args}}",
"extension": ".toml",
}
context_file = "GEMINI.md"
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, new-agent-cli, or kiro-cli"),
```
**Skills agent (Codex):**
Also update any function docstrings, examples, and error messages that list available agents.
```python
"""Codex CLI integration — skills-based agent."""
#### 3. Update README Documentation
from __future__ import annotations
Update the **Supported AI Agents** section in `README.md` to include the new agent:
from ..base import IntegrationOption, SkillsIntegration
- Add the new agent to the table with appropriate support level (Full/Partial)
- Include the agent's official website link
- Add any relevant notes about the agent's implementation
- Ensure the table formatting remains aligned and consistent
#### 4. Update Release Package Script
class CodexIntegration(SkillsIntegration):
key = "codex"
config = {
"name": "Codex CLI",
"folder": ".agents/",
"commands_subdir": "skills",
"install_url": "https://github.com/openai/codex",
"requires_cli": True,
}
registrar_config = {
"dir": ".agents/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = "AGENTS.md"
Modify `.github/workflows/scripts/create-release-packages.sh`:
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills (default for Codex)",
),
]
```
#### Required fields
| Field | Location | Purpose |
|---|---|---|
| `key` | Class attribute | Unique identifier; must match the CLI executable name |
| `config` | Class attribute (dict) | Agent metadata: `name`, `folder`, `commands_subdir`, `install_url`, `requires_cli` |
| `registrar_config` | Class attribute (dict) | Command output config: `dir`, `format`, `args` placeholder, file `extension` |
| `context_file` | Class attribute (str or None) | Path to agent context/instructions file (e.g., `"CLAUDE.md"`, `".github/copilot-instructions.md"`) |
**Key design rule:** `key` must be the actual executable name (e.g., `"cursor-agent"` not `"cursor"`). This ensures `shutil.which(key)` works for CLI-tool checks without special-case mappings.
### 3. Register it
In `src/specify_cli/integrations/__init__.py`, add one import and one `_register()` call inside `_register_builtins()`. Both lists are alphabetical:
```python
def _register_builtins() -> None:
# -- Imports (alphabetical) -------------------------------------------
from .claude import ClaudeIntegration
# ...
from .newagent import NewAgentIntegration # ← add import
# ...
# -- Registration (alphabetical) --------------------------------------
_register(ClaudeIntegration())
# ...
_register(NewAgentIntegration()) # ← add registration
# ...
```
### 4. Add scripts
Create two thin wrapper scripts in `src/specify_cli/integrations/<key>/scripts/` that delegate to the shared context-update scripts. Each is ~25 lines of boilerplate.
**`update-context.sh`:**
##### Add to ALL_AGENTS array
```bash
#!/usr/bin/env bash
# update-context.sh — <Agent Name> integration: create/update <context_file>
set -euo pipefail
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" <key>
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf kiro-cli)
```
**`update-context.ps1`:**
##### Add case statement for directory structure
```bash
case $agent in
# ... existing cases ...
windsurf)
mkdir -p "$base_dir/.windsurf/workflows"
generate_commands windsurf md "\$ARGUMENTS" "$base_dir/.windsurf/workflows" "$script" ;;
esac
```
#### 4. Update GitHub Release Script
Modify `.github/workflows/scripts/create-github-release.sh` to include the new agent's packages:
```bash
gh release create "$VERSION" \
# ... existing packages ...
.genreleases/spec-kit-template-windsurf-sh-"$VERSION".zip \
.genreleases/spec-kit-template-windsurf-ps-"$VERSION".zip \
# Add new agent packages here
```
#### 5. Update Agent Context Scripts
##### Bash script (`scripts/bash/update-agent-context.sh`)
Add file variable:
```bash
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
```
Add to case statement:
```bash
case "$AGENT_TYPE" in
# ... existing cases ...
windsurf) update_agent_file "$WINDSURF_FILE" "Windsurf" ;;
"")
# ... existing checks ...
[ -f "$WINDSURF_FILE" ] && update_agent_file "$WINDSURF_FILE" "Windsurf";
# Update default creation condition
;;
esac
```
##### PowerShell script (`scripts/powershell/update-agent-context.ps1`)
Add file variable:
```powershell
# update-context.ps1 — <Agent Name> integration: create/update <context_file>
$ErrorActionPreference = 'Stop'
$windsurfFile = Join-Path $repoRoot '.windsurf/rules/specify-rules.md'
```
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
Add to switch statement:
```powershell
switch ($AgentType) {
# ... existing cases ...
'windsurf' { Update-AgentFile $windsurfFile 'Windsurf' }
'' {
foreach ($pair in @(
# ... existing pairs ...
@{file=$windsurfFile; name='Windsurf'}
)) {
if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name }
}
# Update default creation condition
}
}
```
#### 6. Update CLI Tool Checks (Optional)
For agents that require CLI tools, add checks in the `check()` command and agent validation:
```python
# In check() command
tracker.add("windsurf", "Windsurf IDE (optional)")
windsurf_ok = check_tool_for_tracker("windsurf", "https://windsurf.com/", tracker)
# In init validation (only if CLI tool required)
elif selected_ai == "windsurf":
if not check_tool("windsurf", "Install from: https://windsurf.com/"):
console.print("[red]Error:[/red] Windsurf CLI is required for Windsurf projects")
agent_tool_missing = True
```
**Note**: CLI tool checks are now handled automatically based on the `requires_cli` field in AGENT_CONFIG. No additional code changes needed in the `check()` or `init()` commands - they automatically loop through AGENT_CONFIG and check tools as needed.
## Important Design Decisions
### Using Actual CLI Tool Names as Keys
**CRITICAL**: When adding a new agent to AGENT_CONFIG, always use the **actual executable name** as the dictionary key, not a shortened or convenient version.
**Why this matters:**
- The `check_tool()` function uses `shutil.which(tool)` to find executables in the system PATH
- If the key doesn't match the actual CLI tool name, you'll need special-case mappings throughout the codebase
- This creates unnecessary complexity and maintenance burden
**Example - The Cursor Lesson:**
**Wrong approach** (requires special-case mapping):
```python
AGENT_CONFIG = {
"cursor": { # Shorthand that doesn't match the actual tool
"name": "Cursor",
# ...
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType <key>
# Then you need special cases everywhere:
cli_tool = agent_key
if agent_key == "cursor":
cli_tool = "cursor-agent" # Map to the real tool name
```
Replace `<key>` with your integration key and `<Agent Name>` / `<context_file>` with the appropriate values.
**Correct approach** (no mapping needed):
You must also add the agent to the shared context-update scripts so the shared dispatcher recognises the new key:
```python
AGENT_CONFIG = {
"cursor-agent": { # Matches the actual executable name
"name": "Cursor",
# ...
}
}
- **`scripts/bash/update-agent-context.sh`** — add a file-path variable and a case in `update_specific_agent()`.
- **`scripts/powershell/update-agent-context.ps1`** — add a file-path variable, a switch case in `Update-SpecificAgent`, and an entry in `Update-AllExistingAgents`.
### 5. Test it
```bash
# Install into a test project
specify init my-project --integration <key>
# Verify files were created
ls -R my-project/<commands_dir>/
# Uninstall cleanly
cd my-project && specify integration uninstall <key>
# No special cases needed - just use agent_key directly!
```
Each integration also has a dedicated test file at `tests/integrations/test_integration_<key>.py`. Run it with:
**Benefits of this approach:**
```bash
pytest tests/integrations/test_integration_<key>.py -v
```
- Eliminates special-case logic scattered throughout the codebase
- Makes the code more maintainable and easier to understand
- Reduces the chance of bugs when adding new agents
- Tool checking "just works" without additional mappings
### 6. Optional overrides
The base classes handle most work automatically. Override only when the agent deviates from standard patterns:
| Override | When to use | Example |
|---|---|---|
| `command_filename(template_name)` | Custom file naming or extension | Copilot → `speckit.{name}.agent.md` |
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag |
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `.agent.md` + `.prompt.md` + `.vscode/settings.json` |
| `teardown()` | Custom uninstall logic | Rarely needed; base handles manifest-tracked files |
**Example — Copilot (fully custom `setup`):**
Copilot extends `IntegrationBase` directly because it creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.
### 7. Update Devcontainer files (Optional)
#### 7. Update Devcontainer files (Optional)
For agents that have VS Code extensions or require CLI installation, update the devcontainer configuration files:
#### VS Code Extension-based Agents
##### VS Code Extension-based Agents
For agents available as VS Code extensions, add them to `.devcontainer/devcontainer.json`:
```jsonc
```json
{
"customizations": {
"vscode": {
"extensions": [
// ... existing extensions ...
// [New Agent Name]
"[New Agent Extension ID]"
]
}
@@ -289,7 +288,7 @@ For agents available as VS Code extensions, add them to `.devcontainer/devcontai
}
```
#### CLI-based Agents
##### CLI-based Agents
For agents that require CLI tools, add installation commands to `.devcontainer/post-create.sh`:
@@ -299,16 +298,63 @@ For agents that require CLI tools, add installation commands to `.devcontainer/p
# Existing installations...
echo -e "\n🤖 Installing [New Agent Name] CLI..."
# run_command "npm install -g [agent-cli-package]@latest"
# run_command "npm install -g [agent-cli-package]@latest" # Example for node-based CLI
# or other installation instructions (must be non-interactive and compatible with Linux Debian "Trixie" or later)...
echo "✅ Done"
```
---
**Quick Tips:**
- **Extension-based agents**: Add to the `extensions` array in `devcontainer.json`
- **CLI-based agents**: Add installation scripts to `post-create.sh`
- **Hybrid agents**: May require both extension and CLI installation
- **Test thoroughly**: Ensure installations work in the devcontainer environment
## Agent Categories
### CLI-Based Agents
Require a command-line tool to be installed:
- **Claude Code**: `claude` CLI
- **Gemini CLI**: `gemini` CLI
- **Qwen Code**: `qwen` CLI
- **opencode**: `opencode` CLI
- **Codex CLI**: `codex` CLI (requires `--ai-skills`)
- **Junie**: `junie` CLI
- **Auggie CLI**: `auggie` CLI
- **CodeBuddy CLI**: `codebuddy` CLI
- **Qoder CLI**: `qodercli` CLI
- **Kiro CLI**: `kiro-cli` CLI
- **Amp**: `amp` CLI
- **SHAI**: `shai` CLI
- **Tabnine CLI**: `tabnine` CLI
- **Kimi Code**: `kimi` CLI
- **Mistral Vibe**: `vibe` CLI
- **Pi Coding Agent**: `pi` CLI
- **iFlow CLI**: `iflow` CLI
- **Forge**: `forge` CLI
### IDE-Based Agents
Work within integrated development environments:
- **GitHub Copilot**: Built into VS Code/compatible editors
- **Cursor**: Built into Cursor IDE (`--ai cursor-agent`)
- **Windsurf**: Built into Windsurf IDE
- **Kilo Code**: Built into Kilo Code IDE
- **Roo Code**: Built into Roo Code IDE
- **IBM Bob**: Built into IBM Bob IDE
- **Trae**: Built into Trae IDE
- **Antigravity**: Built into Antigravity IDE (`--ai agy --ai-skills`)
## Command File Formats
### Markdown Format
Used by: Claude, Cursor, GitHub Copilot, opencode, Windsurf, Junie, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code, Qwen, Pi, Codex, Auggie, CodeBuddy, Qoder, Roo Code, Kilo Code, Trae, Antigravity, Mistral Vibe, iFlow, Forge
**Standard format:**
```markdown
@@ -332,6 +378,8 @@ Command content with {SCRIPT} and $ARGUMENTS placeholders.
### TOML Format
Used by: Gemini, Tabnine
```toml
description = "Command description"
@@ -340,23 +388,109 @@ Command content with {SCRIPT} and {{args}} placeholders.
"""
```
## Directory Conventions
- **CLI agents**: Usually `.<agent-name>/commands/`
- **Singular command exception**:
- opencode: `.opencode/command/` (singular `command`, not `commands`)
- **Nested path exception**:
- Tabnine: `.tabnine/agent/commands/` (extra `agent/` segment)
- **Shared `.agents/` folder**:
- Amp: `.agents/commands/` (shared folder, not `.amp/`)
- Codex: `.agents/skills/` (shared folder; requires `--ai-skills`; invoked as `$speckit-<command>`)
- **Skills-based exceptions**:
- Kimi Code: `.kimi/skills/` (skills, invoked as `/skill:speckit-<command>`)
- **Prompt-based exceptions**:
- Kiro CLI: `.kiro/prompts/`
- Pi: `.pi/prompts/`
- Mistral Vibe: `.vibe/prompts/`
- **Rules-based exceptions**:
- Trae: `.trae/rules/`
- **IDE agents**: Follow IDE-specific patterns:
- Copilot: `.github/agents/`
- Cursor: `.cursor/commands/`
- Windsurf: `.windsurf/workflows/`
- Kilo Code: `.kilocode/workflows/`
- Roo Code: `.roo/commands/`
- IBM Bob: `.bob/commands/`
- Antigravity: `.agent/skills/` (`--ai-skills` required; `.agent/commands/` is deprecated)
## Argument Patterns
Different agents use different argument placeholders:
- **Markdown/prompt-based**: `$ARGUMENTS`
- **TOML-based**: `{{args}}`
- **Forge-specific**: `{{parameters}}` (uses custom parameter syntax)
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
- **Agent placeholders**: `__AGENT__` (replaced with agent name)
## Special Processing Requirements
Some agents require custom processing beyond the standard template transformations:
### Copilot Integration
GitHub Copilot has unique requirements:
- Commands use `.agent.md` extension (not `.md`)
- Each command gets a companion `.prompt.md` file in `.github/prompts/`
- Installs `.vscode/settings.json` with prompt file recommendations
- Context file lives at `.github/copilot-instructions.md`
Implementation: Extends `IntegrationBase` with custom `setup()` method that:
1. Processes templates with `process_template()`
2. Generates companion `.prompt.md` files
3. Merges VS Code settings
### Forge Integration
Forge has special frontmatter and argument requirements:
- Uses `{{parameters}}` instead of `$ARGUMENTS`
- Strips `handoffs` frontmatter key (Forge-specific collaboration feature)
- Injects `name` field into frontmatter when missing
Implementation: Extends `MarkdownIntegration` with custom `setup()` method that:
1. Inherits standard template processing from `MarkdownIntegration`
2. Adds extra `$ARGUMENTS``{{parameters}}` replacement after template processing
3. Applies Forge-specific transformations via `_apply_forge_transformations()`
4. Strips `handoffs` frontmatter key
5. Injects missing `name` fields
6. Ensures the shared `update-agent-context.*` scripts include a `forge` case that maps context updates to `AGENTS.md` (similar to `opencode`/`codex`/`pi`) and lists `forge` in their usage/help text
### Standard Markdown Agents
Most agents (Bob, Claude, Windsurf, etc.) use `MarkdownIntegration`:
- Simple subclass with just `key`, `config`, `registrar_config` set
- Inherits standard processing from `MarkdownIntegration.setup()`
- No custom processing needed
## Testing New Agent Integration
1. **Build test**: Run package creation script locally
2. **CLI test**: Test `specify init --ai <agent>` command
3. **File generation**: Verify correct directory structure and files
4. **Command validation**: Ensure generated commands work with the agent
5. **Context update**: Test agent context update scripts
## Common Pitfalls
1. **Using shorthand keys instead of actual CLI tool names**: The integration `key` must match the executable name (e.g., `"cursor-agent"` not `"cursor"`). `shutil.which(key)` is used for CLI tool checks — mismatches require special-case mappings.
2. **Forgetting update scripts**: Both bash and PowerShell thin wrappers and the shared context-update scripts must be updated.
3. **Incorrect `requires_cli` value**: Set to `True` only for agents that have a CLI tool; set to `False` for IDE-based agents.
4. **Wrong argument format**: Use `$ARGUMENTS` for Markdown agents, `{{args}}` for TOML agents.
5. **Skipping registration**: The import and `_register()` call in `_register_builtins()` must both be added.
1. **Using shorthand keys instead of actual CLI tool names**: Always use the actual executable name as the AGENT_CONFIG key (e.g., `"cursor-agent"` not `"cursor"`). This prevents the need for special-case mappings throughout the codebase.
2. **Forgetting update scripts**: Both bash and PowerShell scripts must be updated when adding new agents.
3. **Incorrect `requires_cli` value**: Set to `True` only for agents that actually have CLI tools to check; set to `False` for IDE-based agents.
4. **Wrong argument format**: Use correct placeholder format for each agent type (`$ARGUMENTS` for Markdown, `{{args}}` for TOML).
5. **Directory naming**: Follow agent-specific conventions exactly (check existing agents for patterns).
6. **Help text inconsistency**: Update all user-facing text consistently (help strings, docstrings, README, error messages).
## Future Considerations
When adding new agents:
- Consider the agent's native command/workflow patterns
- Ensure compatibility with the Spec-Driven Development process
- Document any special requirements or limitations
- Update this guide with lessons learned
- Verify the actual CLI tool name before adding to AGENT_CONFIG
---
*This documentation should be updated whenever new integrations are added to maintain accuracy and completeness.*
*This documentation should be updated whenever new agents are added to maintain accuracy and completeness.*

View File

@@ -2,6 +2,14 @@
<!-- insert new changelog below this comment -->
## [0.5.0] - 2026-04-02
### Changed
- Introduces DEVELOPMENT.md (#2069)
- Update cc-sdd reference to cc-spex in Community Friends (#2007)
- chore: release 0.4.5, begin 0.4.6.dev0 development (#2064)
## [0.4.5] - 2026-04-02
### Changed

25
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,25 @@
# Development Notes
Spec Kit is a toolkit for spec-driven development. At its core, it is a coordinated set of prompts, templates, scripts, and CLI/integration assets that define and deliver a spec-driven workflow for AI coding agents. This document is a starting point for people modifying Spec Kit itself, with a compact orientation to the key project documents and repository organization.
**Essential project documents:**
| Document | Role |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| [README.md](README.md) | Primary user-facing overview of Spec Kit and its workflow. |
| [DEVELOPMENT.md](DEVELOPMENT.md) | This document. |
| [spec-driven.md](spec-driven.md) | End-to-end explanation of the Spec-Driven Development workflow supported by Spec Kit. |
| [RELEASE-PROCESS.md](.github/workflows/RELEASE-PROCESS.md) | Release workflow, versioning rules, and changelog generation process. |
| [docs/index.md](docs/index.md) | Entry point to the `docs/` documentation set. |
| [CONTRIBUTING.md](CONTRIBUTING.md) | Contribution process, review expectations, and required development practices. |
| [TESTING.md](TESTING.md) | Validation strategy and testing procedures. |
**Main repository components:**
| Directory | Role |
| ------------------ | ------------------------------------------------------------------------------------------- |
| `templates/` | Prompt assets and templates that define the core workflow behavior and generated artifacts. |
| `scripts/` | Supporting scripts used by the workflow, setup, and repository tooling. |
| `src/specify_cli/` | Python source for the `specify` CLI, including agent-specific assets. |
| `extensions/` | Extension-related docs, catalogs, and supporting assets. |
| `presets/` | Preset-related docs, catalogs, and supporting assets. |

View File

@@ -269,7 +269,7 @@ See Spec-Driven Development in action across different scenarios with these comm
Community projects that extend, visualize, or build on Spec Kit:
- **[cc-sdd](https://github.com/rhuss/cc-sdd)** - A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
- **[cc-spex](https://github.com/rhuss/cc-spex)** - A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
@@ -285,6 +285,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/) | ✅ | |
| [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 |
@@ -314,14 +315,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`, `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`, 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 |
@@ -376,6 +377,9 @@ specify init my-project --ai codex --ai-skills
# Initialize with Antigravity support
specify init my-project --ai agy --ai-skills
# 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/
@@ -621,7 +625,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, 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

@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.4.6.dev0"
version = "0.5.1.dev0"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [

View File

@@ -30,12 +30,12 @@
#
# 5. Multi-Agent Support
# - Handles agent-specific file paths and naming conventions
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Antigravity or Generic
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Forge, Antigravity or Generic
# - Can update single agents or all existing agent files
# - 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|forge|generic
# Leave empty to update all existing agent files
set -e
@@ -74,7 +74,7 @@ AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
QODER_FILE="$REPO_ROOT/QODER.md"
# Amp, Kiro CLI, IBM Bob, and Pi all share AGENTS.md — use AGENTS_FILE to avoid
# Amp, Kiro CLI, IBM Bob, Pi, and Forge all share AGENTS.md — use AGENTS_FILE to avoid
# updating the same file multiple times.
AMP_FILE="$AGENTS_FILE"
SHAI_FILE="$REPO_ROOT/SHAI.md"
@@ -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"
FORGE_FILE="$AGENTS_FILE"
# Template file
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
@@ -690,12 +691,15 @@ update_specific_agent() {
iflow)
update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1
;;
forge)
update_agent_file "$AGENTS_FILE" "Forge" || 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|forge|generic"
exit 1
;;
esac
@@ -739,10 +743,7 @@ update_all_existing_agents() {
_update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false
_update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false
_update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false
_update_if_new "$AGENTS_FILE" "Codex/opencode" || _all_ok=false
_update_if_new "$AMP_FILE" "Amp" || _all_ok=false
_update_if_new "$KIRO_FILE" "Kiro CLI" || _all_ok=false
_update_if_new "$BOB_FILE" "IBM Bob" || _all_ok=false
_update_if_new "$AGENTS_FILE" "Codex/opencode/Amp/Kiro/Bob/Pi/Forge" || _all_ok=false
_update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false
_update_if_new "$JUNIE_FILE" "Junie" || _all_ok=false
_update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false
@@ -783,7 +784,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|forge|generic]"
}
#==============================================================================

View File

@@ -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, forge, 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).
@@ -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','vibe','qodercli','kimi','trae','pi','iflow','forge','generic')]
[string]$AgentType
)
@@ -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'
$FORGE_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
@@ -415,36 +416,66 @@ 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' }
'forge' { Update-AgentFile -TargetFile $FORGE_FILE -AgentName 'Forge' }
'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|forge|generic'; return $false }
}
}
function Update-AllExistingAgents {
$found = $false
$ok = $true
if (Test-Path $CLAUDE_FILE) { if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }; $found = $true }
if (Test-Path $GEMINI_FILE) { if (-not (Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }; $found = $true }
if (Test-Path $COPILOT_FILE) { if (-not (Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }; $found = $true }
if (Test-Path $CURSOR_FILE) { if (-not (Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false }; $found = $true }
if (Test-Path $QWEN_FILE) { if (-not (Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }; $found = $true }
if (Test-Path $AGENTS_FILE) { if (-not (Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex/opencode')) { $ok = $false }; $found = $true }
if (Test-Path $WINDSURF_FILE) { if (-not (Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false }; $found = $true }
if (Test-Path $JUNIE_FILE) { if (-not (Update-AgentFile -TargetFile $JUNIE_FILE -AgentName 'Junie')) { $ok = $false }; $found = $true }
if (Test-Path $KILOCODE_FILE) { if (-not (Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }; $found = $true }
if (Test-Path $AUGGIE_FILE) { if (-not (Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }; $found = $true }
if (Test-Path $ROO_FILE) { if (-not (Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code')) { $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 $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 $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 $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 (Test-Path $IFLOW_FILE) { if (-not (Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false }; $found = $true }
$updatedPaths = @()
# Helper function to update only if file exists and hasn't been updated yet
function Update-IfNew {
param(
[Parameter(Mandatory=$true)]
[string]$FilePath,
[Parameter(Mandatory=$true)]
[string]$AgentName
)
if (-not (Test-Path $FilePath)) { return $true }
# Get the real path to detect duplicates (e.g., AMP_FILE, KIRO_FILE, BOB_FILE all point to AGENTS.md)
$realPath = (Get-Item -LiteralPath $FilePath).FullName
# Check if we've already updated this file
if ($updatedPaths -contains $realPath) {
return $true
}
# Record the file as seen before attempting the update
# Use parent scope (1) to modify Update-AllExistingAgents' local variables
Set-Variable -Name updatedPaths -Value ($updatedPaths + $realPath) -Scope 1
Set-Variable -Name found -Value $true -Scope 1
# Perform the update
return (Update-AgentFile -TargetFile $FilePath -AgentName $AgentName)
}
if (-not (Update-IfNew -FilePath $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }
if (-not (Update-IfNew -FilePath $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false }
if (-not (Update-IfNew -FilePath $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $AGENTS_FILE -AgentName 'Codex/opencode/Amp/Kiro/Bob/Pi/Forge')) { $ok = $false }
if (-not (Update-IfNew -FilePath $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false }
if (-not (Update-IfNew -FilePath $JUNIE_FILE -AgentName 'Junie')) { $ok = $false }
if (-not (Update-IfNew -FilePath $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $TABNINE_FILE -AgentName 'Tabnine CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }
if (-not (Update-IfNew -FilePath $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }
if (-not (Update-IfNew -FilePath $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $TRAE_FILE -AgentName 'Trae')) { $ok = $false }
if (-not (Update-IfNew -FilePath $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false }
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 }
@@ -459,7 +490,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|forge|generic]'
}
function Main {

View File

@@ -117,10 +117,10 @@ CLAUDE_NPM_LOCAL_PATH = Path.home() / ".claude" / "local" / "node_modules" / ".b
BANNER = """
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝
███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝
╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝
███████║██║ ███████╗╚██████╗██║██║ ██║
╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝
███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝
╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝
███████║██║ ███████╗╚██████╗██║██║ ██║
╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝
"""
TAGLINE = "GitHub Spec Kit - Spec-Driven Development Toolkit"
@@ -232,12 +232,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
"""
@@ -365,11 +365,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
"""
@@ -385,27 +385,27 @@ 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
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
@@ -423,11 +423,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])
"""
@@ -449,7 +449,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
@@ -911,7 +911,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?")
@@ -1023,8 +1023,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"
)
@@ -1262,7 +1262,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()
@@ -1410,9 +1410,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:
@@ -1428,15 +1428,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,

View File

@@ -408,6 +408,12 @@ class CommandRegistrar:
frontmatter = self._adjust_script_paths(frontmatter)
for key in agent_config.get("strip_frontmatter_keys", []):
frontmatter.pop(key, None)
if agent_config.get("inject_name") and not frontmatter.get("name"):
frontmatter["name"] = cmd_name
body = self._convert_argument_placeholder(
body, "$ARGUMENTS", agent_config["args"]
)
@@ -436,11 +442,30 @@ class CommandRegistrar:
for alias in cmd_info.get("aliases", []):
alias_output_name = self._compute_output_name(agent_name, alias, agent_config)
alias_output = output
if agent_config["extension"] == "/SKILL.md":
alias_output = self.render_skill_command(
agent_name, alias_output_name, frontmatter, body, source_id, cmd_file, project_root
)
# For agents with inject_name, render with alias-specific frontmatter
if agent_config.get("inject_name"):
alias_frontmatter = deepcopy(frontmatter)
alias_frontmatter["name"] = alias
if agent_config["extension"] == "/SKILL.md":
alias_output = self.render_skill_command(
agent_name, alias_output_name, alias_frontmatter, body, source_id, cmd_file, project_root
)
elif agent_config["format"] == "markdown":
alias_output = self.render_markdown_command(alias_frontmatter, body, source_id, context_note)
elif agent_config["format"] == "toml":
alias_output = self.render_toml_command(alias_frontmatter, body, source_id)
else:
raise ValueError(f"Unsupported format: {agent_config['format']}")
else:
# For other agents, reuse the primary output
alias_output = output
if agent_config["extension"] == "/SKILL.md":
alias_output = self.render_skill_command(
agent_name, alias_output_name, frontmatter, body, source_id, cmd_file, project_root
)
alias_file = commands_dir / f"{alias_output_name}{agent_config['extension']}"
alias_file.parent.mkdir(parents=True, exist_ok=True)
alias_file.write_text(alias_output, encoding="utf-8")
@@ -540,4 +565,3 @@ try:
CommandRegistrar._ensure_configs()
except ImportError:
pass

View File

@@ -55,6 +55,7 @@ def _register_builtins() -> None:
from .codebuddy import CodebuddyIntegration
from .copilot import CopilotIntegration
from .cursor_agent import CursorAgentIntegration
from .forge import ForgeIntegration
from .gemini import GeminiIntegration
from .generic import GenericIntegration
from .iflow import IflowIntegration
@@ -83,6 +84,7 @@ def _register_builtins() -> None:
_register(CodebuddyIntegration())
_register(CopilotIntegration())
_register(CursorAgentIntegration())
_register(ForgeIntegration())
_register(GeminiIntegration())
_register(GenericIntegration())
_register(IflowIntegration())

View File

@@ -0,0 +1,155 @@
"""Forge integration — forgecode.dev AI coding agent.
Forge has several unique behaviors compared to standard markdown agents:
- Uses `{{parameters}}` instead of `$ARGUMENTS` for argument passing
- Strips `handoffs` frontmatter key (Claude Code feature that causes Forge to hang)
- Injects `name` field into frontmatter when missing
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from ..base import MarkdownIntegration
from ..manifest import IntegrationManifest
class ForgeIntegration(MarkdownIntegration):
"""Integration for Forge (forgecode.dev).
Extends MarkdownIntegration to add Forge-specific processing:
- Replaces $ARGUMENTS with {{parameters}}
- Strips 'handoffs' frontmatter key (incompatible with Forge)
- Injects 'name' field into frontmatter when missing
"""
key = "forge"
config = {
"name": "Forge",
"folder": ".forge/",
"commands_subdir": "commands",
"install_url": "https://forgecode.dev/docs/",
"requires_cli": True,
}
registrar_config = {
"dir": ".forge/commands",
"format": "markdown",
"args": "{{parameters}}",
"extension": ".md",
"strip_frontmatter_keys": ["handoffs"],
"inject_name": True,
}
context_file = "AGENTS.md"
def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Install Forge commands with custom processing.
Extends MarkdownIntegration.setup() to inject Forge-specific transformations
after standard template processing.
"""
templates = self.list_command_templates()
if not templates:
return []
project_root_resolved = project_root.resolve()
if manifest.project_root != project_root_resolved:
raise ValueError(
f"manifest.project_root ({manifest.project_root}) does not match "
f"project_root ({project_root_resolved})"
)
dest = self.commands_dest(project_root).resolve()
try:
dest.relative_to(project_root_resolved)
except ValueError as exc:
raise ValueError(
f"Integration destination {dest} escapes "
f"project root {project_root_resolved}"
) from exc
dest.mkdir(parents=True, exist_ok=True)
script_type = opts.get("script_type", "sh")
arg_placeholder = self.registrar_config.get("args", "{{parameters}}")
created: list[Path] = []
for src_file in templates:
raw = src_file.read_text(encoding="utf-8")
# Process template with standard MarkdownIntegration logic
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
# FORGE-SPECIFIC: Ensure any remaining $ARGUMENTS placeholders are
# converted to {{parameters}}
processed = processed.replace("$ARGUMENTS", arg_placeholder)
# FORGE-SPECIFIC: Apply frontmatter transformations
processed = self._apply_forge_transformations(processed, src_file.stem)
dst_name = self.command_filename(src_file.stem)
dst_file = self.write_file_and_record(
processed, dest / dst_name, project_root, manifest
)
created.append(dst_file)
# Install integration-specific update-context scripts
created.extend(self.install_scripts(project_root, manifest))
return created
def _apply_forge_transformations(self, content: str, template_name: str) -> str:
"""Apply Forge-specific transformations to processed content.
1. Strip 'handoffs' frontmatter key (from Claude Code templates; incompatible with Forge)
2. Inject 'name' field if missing
"""
# Parse frontmatter
lines = content.split('\n')
if not lines or lines[0].strip() != '---':
return content
# Find end of frontmatter
frontmatter_end = -1
for i in range(1, len(lines)):
if lines[i].strip() == '---':
frontmatter_end = i
break
if frontmatter_end == -1:
return content
frontmatter_lines = lines[1:frontmatter_end]
body_lines = lines[frontmatter_end + 1:]
# 1. Strip 'handoffs' key
filtered_frontmatter = []
skip_until_outdent = False
for line in frontmatter_lines:
if skip_until_outdent:
# Skip indented lines under handoffs:
if line and (line[0] == ' ' or line[0] == '\t'):
continue
else:
skip_until_outdent = False
if line.strip().startswith('handoffs:'):
skip_until_outdent = True
continue
filtered_frontmatter.append(line)
# 2. Inject 'name' field if missing
has_name = any(line.strip().startswith('name:') for line in filtered_frontmatter)
if not has_name:
# Use the template name as the command name (e.g., "plan" -> "speckit.plan")
cmd_name = f"speckit.{template_name}"
filtered_frontmatter.insert(0, f'name: {cmd_name}')
# Reconstruct content
result = ['---'] + filtered_frontmatter + ['---'] + body_lines
return '\n'.join(result)

View File

@@ -0,0 +1,33 @@
# update-context.ps1 — Forge integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
$sharedScript = "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1"
# Always delegate to the shared updater; fail clearly if it is unavailable.
if (-not (Test-Path $sharedScript)) {
Write-Error "Error: shared agent context updater not found: $sharedScript"
Write-Error "Forge integration requires support in scripts/powershell/update-agent-context.ps1."
exit 1
}
& $sharedScript -AgentType forge
exit $LASTEXITCODE

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# update-context.sh — Forge integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
shared_script="$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh"
# Always delegate to the shared updater; fail clearly if it is unavailable.
if [ ! -x "$shared_script" ]; then
echo "Error: shared agent context updater not found or not executable:" >&2
echo " $shared_script" >&2
echo "Forge integration requires support in scripts/bash/update-agent-context.sh." >&2
exit 1
fi
exec "$shared_script" forge

View File

@@ -0,0 +1,170 @@
"""Tests for ForgeIntegration."""
from specify_cli.integrations import get_integration
from specify_cli.integrations.manifest import IntegrationManifest
class TestForgeIntegration:
def test_forge_key_and_config(self):
forge = get_integration("forge")
assert forge is not None
assert forge.key == "forge"
assert forge.config["folder"] == ".forge/"
assert forge.config["commands_subdir"] == "commands"
assert forge.config["requires_cli"] is True
assert forge.registrar_config["args"] == "{{parameters}}"
assert forge.registrar_config["extension"] == ".md"
assert forge.context_file == "AGENTS.md"
def test_command_filename_md(self):
forge = get_integration("forge")
assert forge.command_filename("plan") == "speckit.plan.md"
def test_setup_creates_md_files(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
created = forge.setup(tmp_path, m)
assert len(created) > 0
# Separate command files from scripts
command_files = [f for f in created if f.parent == tmp_path / ".forge" / "commands"]
assert len(command_files) > 0
for f in command_files:
assert f.name.endswith(".md")
def test_setup_installs_update_scripts(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
created = forge.setup(tmp_path, m)
script_files = [f for f in created if "scripts" in f.parts]
assert len(script_files) > 0
sh_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.sh"
ps_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.ps1"
assert sh_script in created
assert ps_script in created
assert sh_script.exists()
assert ps_script.exists()
def test_all_created_files_tracked_in_manifest(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
created = forge.setup(tmp_path, m)
for f in created:
rel = f.resolve().relative_to(tmp_path.resolve()).as_posix()
assert rel in m.files, f"Created file {rel} not tracked in manifest"
def test_install_uninstall_roundtrip(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
created = forge.install(tmp_path, m)
assert len(created) > 0
m.save()
for f in created:
assert f.exists()
removed, skipped = forge.uninstall(tmp_path, m)
assert len(removed) == len(created)
assert skipped == []
def test_modified_file_survives_uninstall(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
created = forge.install(tmp_path, m)
m.save()
# Modify a command file (not a script)
command_files = [f for f in created if f.parent == tmp_path / ".forge" / "commands"]
modified_file = command_files[0]
modified_file.write_text("user modified this", encoding="utf-8")
removed, skipped = forge.uninstall(tmp_path, m)
assert modified_file.exists()
assert modified_file in skipped
def test_directory_structure(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
forge.setup(tmp_path, m)
commands_dir = tmp_path / ".forge" / "commands"
assert commands_dir.is_dir()
# Derive expected command names from the Forge command templates so the test
# stays in sync if templates are added/removed.
templates = forge.list_command_templates()
expected_commands = {t.stem for t in templates}
assert len(expected_commands) > 0, "No command templates found"
# Check generated files match templates
command_files = sorted(commands_dir.glob("speckit.*.md"))
assert len(command_files) == len(expected_commands)
actual_commands = {f.name.removeprefix("speckit.").removesuffix(".md") for f in command_files}
assert actual_commands == expected_commands
def test_templates_are_processed(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
forge.setup(tmp_path, m)
commands_dir = tmp_path / ".forge" / "commands"
for cmd_file in commands_dir.glob("speckit.*.md"):
content = cmd_file.read_text(encoding="utf-8")
# Check standard replacements
assert "{SCRIPT}" not in content, f"{cmd_file.name} has unprocessed {{SCRIPT}}"
assert "__AGENT__" not in content, f"{cmd_file.name} has unprocessed __AGENT__"
assert "{ARGS}" not in content, f"{cmd_file.name} has unprocessed {{ARGS}}"
# Check Forge-specific: $ARGUMENTS should be replaced with {{parameters}}
assert "$ARGUMENTS" not in content, f"{cmd_file.name} has unprocessed $ARGUMENTS"
# Frontmatter sections should be stripped
assert "\nscripts:\n" not in content
assert "\nagent_scripts:\n" not in content
def test_forge_specific_transformations(self, tmp_path):
"""Test Forge-specific processing: name injection and handoffs stripping."""
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
forge.setup(tmp_path, m)
commands_dir = tmp_path / ".forge" / "commands"
for cmd_file in commands_dir.glob("speckit.*.md"):
content = cmd_file.read_text(encoding="utf-8")
# Check that name field is injected in frontmatter
assert "\nname: " in content, f"{cmd_file.name} missing injected 'name' field"
# Check that handoffs frontmatter key is stripped
assert "\nhandoffs:" not in content, f"{cmd_file.name} has unstripped 'handoffs' key"
def test_uses_parameters_placeholder(self, tmp_path):
"""Verify Forge replaces $ARGUMENTS with {{parameters}} in generated files."""
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
# The registrar_config should specify {{parameters}}
assert forge.registrar_config["args"] == "{{parameters}}"
# Generate files and verify $ARGUMENTS is replaced with {{parameters}}
from specify_cli.integrations.manifest import IntegrationManifest
m = IntegrationManifest("forge", tmp_path)
forge.setup(tmp_path, m)
commands_dir = tmp_path / ".forge" / "commands"
# Check all generated command files
for cmd_file in commands_dir.glob("speckit.*.md"):
content = cmd_file.read_text(encoding="utf-8")
# $ARGUMENTS should be replaced with {{parameters}}
assert "$ARGUMENTS" not in content, (
f"{cmd_file.name} still contains $ARGUMENTS - it should be replaced with {{{{parameters}}}}"
)
# At least some files should have {{parameters}} (those with user input sections)
# We'll check the checklist file specifically as it has a User Input section
# Verify checklist specifically has {{parameters}} in the User Input section
checklist = commands_dir / "speckit.checklist.md"
if checklist.exists():
content = checklist.read_text(encoding="utf-8")
assert "{{parameters}}" in content, (
"checklist should contain {{parameters}} in User Input section"
)