mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 20:36:23 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3f0edd142 |
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -26,7 +26,6 @@ concurrency:
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
if: github.repository == 'github/spec-kit'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -57,7 +56,6 @@ jobs:
|
||||
|
||||
# Deploy job
|
||||
deploy:
|
||||
if: github.repository == 'github/spec-kit'
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
593
AGENTS.md
593
AGENTS.md
@@ -10,282 +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 subpackage exposes a single class that declares all metadata and inherits setup/teardown logic from a base class. Built-in integrations are then instantiated and added to the global `INTEGRATION_REGISTRY` by `src/specify_cli/integrations/__init__.py` via `_register_builtins()`.
|
||||
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, YamlIntegration, 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 for Python integration metadata**. Supported agents, their directories, formats, and capabilities are derived from the integration classes for the Python integration layer. However, context-update behavior still requires explicit cases in the shared dispatcher scripts (`scripts/bash/update-agent-context.sh` and `scripts/powershell/update-agent-context.ps1`), which currently maintain their own supported-agent lists and agent-key→context-file mappings until they are migrated to registry-based dispatch.
|
||||
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` |
|
||||
| YAML recipe files (`.yaml`) | `YamlIntegration` |
|
||||
| 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/<package_dir>/__init__.py`, where `<package_dir>` is the Python-safe directory name derived from `<key>`: use the key as-is when it contains no hyphens (e.g., key `"gemini"` → `gemini/`), or replace hyphens with underscores when it does (e.g., key `"kiro-cli"` → `kiro_cli/`). The `IntegrationBase.key` class attribute always retains the original hyphenated value, since that is what the CLI and registry use. For CLI-based integrations (`requires_cli: True`), the `key` should match the actual CLI tool name (the executable users install and run) so CLI checks can resolve it correctly. For IDE-based integrations (`requires_cli: False`), use the canonical integration identifier instead.
|
||||
**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; for CLI-based integrations (`requires_cli: True`), 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:** For CLI-based integrations (`requires_cli: True`), `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. IDE-based integrations (`requires_cli: False`) should use their canonical identifier (e.g., `"windsurf"`, `"copilot"`).
|
||||
|
||||
### 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/<package_dir>/scripts/` that delegate to the shared context-update scripts. Each is ~25 lines of boilerplate.
|
||||
|
||||
> **Note on `<package_dir>` vs `<key>`:** `<package_dir>` is the Python-safe directory name for your integration — it matches `<key>` exactly when the key contains no hyphens (e.g., key `"gemini"` → `gemini/`), but uses underscores when it does (e.g., key `"kiro-cli"` → `kiro_cli/`). The `IntegrationBase.key` class attribute always retains the original hyphenated value (e.g., `key = "kiro-cli"`), since that is what the CLI and registry use.
|
||||
|
||||
**`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, add the new key to the `AgentType` parameter's `[ValidateSet(...)]`, add a switch case in `Update-SpecificAgent`, and add an entry in `Update-AllExistingAgents`.
|
||||
|
||||
### 5. Test it
|
||||
|
||||
```bash
|
||||
# Install into a test project
|
||||
specify init my-project --integration <key>
|
||||
|
||||
# Verify files were created in the commands directory configured by
|
||||
# config["folder"] + config["commands_subdir"] (for example, .windsurf/workflows/)
|
||||
ls -R my-project/.windsurf/workflows/
|
||||
|
||||
# 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`. Note that hyphens in the key are replaced with underscores in the filename (e.g., key `cursor-agent` → `test_integration_cursor_agent.py`, key `kiro-cli` → `test_integration_kiro_cli.py`). Run it with:
|
||||
**Benefits of this approach:**
|
||||
|
||||
```bash
|
||||
pytest tests/integrations/test_integration_<key_with_underscores>.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]"
|
||||
]
|
||||
}
|
||||
@@ -293,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`:
|
||||
|
||||
@@ -303,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
|
||||
@@ -336,6 +378,8 @@ Command content with {SCRIPT} and $ARGUMENTS placeholders.
|
||||
|
||||
### TOML Format
|
||||
|
||||
Used by: Gemini, Tabnine
|
||||
|
||||
```toml
|
||||
description = "Command description"
|
||||
|
||||
@@ -344,33 +388,40 @@ Command content with {SCRIPT} and {{args}} placeholders.
|
||||
"""
|
||||
```
|
||||
|
||||
### YAML Format
|
||||
## Directory Conventions
|
||||
|
||||
Used by: Goose
|
||||
|
||||
```yaml
|
||||
version: 1.0.0
|
||||
title: "Command Title"
|
||||
description: "Command description"
|
||||
author:
|
||||
contact: spec-kit
|
||||
extensions:
|
||||
- type: builtin
|
||||
name: developer
|
||||
activities:
|
||||
- Spec-Driven Development
|
||||
prompt: |
|
||||
Command content with {SCRIPT} and {{args}} placeholders.
|
||||
```
|
||||
- **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. The placeholder used in command files is always taken from `registrar_config["args"]` for each integration — check there first when in doubt:
|
||||
Different agents use different argument placeholders:
|
||||
|
||||
- **Markdown/prompt-based**: `$ARGUMENTS` (default for most markdown agents)
|
||||
- **TOML-based**: `{{args}}` (e.g., Gemini)
|
||||
- **YAML-based**: `{{args}}` (e.g., Goose)
|
||||
- **Custom**: some agents override the default (e.g., Forge uses `{{parameters}}`)
|
||||
- **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)
|
||||
|
||||
@@ -404,30 +455,42 @@ Implementation: Extends `MarkdownIntegration` with custom `setup()` method that:
|
||||
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` and lists `forge` in their usage/help text
|
||||
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
|
||||
|
||||
### Goose Integration
|
||||
### Standard Markdown Agents
|
||||
|
||||
Goose is a YAML-format agent using Block's recipe system:
|
||||
- Uses `.goose/recipes/` directory for YAML recipe files
|
||||
- Uses `{{args}}` argument placeholder
|
||||
- Produces YAML with `prompt: |` block scalar for command content
|
||||
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
|
||||
|
||||
Implementation: Extends `YamlIntegration` (parallel to `TomlIntegration`):
|
||||
1. Processes templates through the standard placeholder pipeline
|
||||
2. Extracts title and description from frontmatter
|
||||
3. Renders output as Goose recipe YAML (version, title, description, author, extensions, activities, prompt)
|
||||
4. Uses `yaml.safe_dump()` for header fields to ensure proper escaping
|
||||
5. Context updates map to `AGENTS.md` (shared with opencode/codex/pi/forge)
|
||||
## 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 for CLI-based integrations**: For CLI-based integrations (`requires_cli: True`), the `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. IDE-based integrations (`requires_cli: False`) are not subject to this constraint.
|
||||
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.*
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -2,46 +2,6 @@
|
||||
|
||||
<!-- insert new changelog below this comment -->
|
||||
|
||||
## [0.6.2] - 2026-04-13
|
||||
|
||||
### Changed
|
||||
|
||||
- feat: Register "What-if Analysis" community extension (#2182)
|
||||
- feat: add GitHub Issues Integration to community catalog (#2188)
|
||||
- feat(agents): add Goose AI agent support (#2015)
|
||||
- Update ralph extension to v1.0.1 in community catalog (#2192)
|
||||
- fix: skip docs deployment workflow on forks (#2171)
|
||||
- chore: release 0.6.1, begin 0.6.2.dev0 development (#2162)
|
||||
|
||||
## [0.6.1] - 2026-04-10
|
||||
|
||||
### Changed
|
||||
|
||||
- feat: add bundled lean preset with minimal workflow commands (#2161)
|
||||
- Add Brownfield Bootstrap extension to community catalog (#2145)
|
||||
- Add CI Guard extension to community catalog (#2157)
|
||||
- Add SpecTest extension to community catalog (#2159)
|
||||
- fix: bundled extensions should not have download URLs (#2155)
|
||||
- Add PR Bridge extension to community catalog (#2148)
|
||||
- feat(cursor-agent): migrate from .cursor/commands to .cursor/skills (#2156)
|
||||
- Add TinySpec extension to community catalog (#2147)
|
||||
- chore: bump spec-kit-verify to 1.0.3 and spec-kit-review to 1.0.1 (#2146)
|
||||
- Add Status Report extension to community catalog (#2123)
|
||||
- chore: release 0.6.0, begin 0.6.1.dev0 development (#2144)
|
||||
|
||||
## [0.6.0] - 2026-04-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Add Bugfix Workflow community extension to catalog and README (#2135)
|
||||
- Add Worktree Isolation extension to community catalog (#2143)
|
||||
- Add multi-repo-branching preset to community catalog (#2139)
|
||||
- Readme clarity (#2013)
|
||||
- Rewrite AGENTS.md for integration architecture (#2119)
|
||||
- docs: add SpecKit Companion to Community Friends section (#2140)
|
||||
- feat: add memorylint extension to community catalog (#2138)
|
||||
- chore: release 0.5.1, begin 0.5.2.dev0 development (#2137)
|
||||
|
||||
## [0.5.1] - 2026-04-08
|
||||
|
||||
### Changed
|
||||
|
||||
130
README.md
130
README.md
@@ -186,10 +186,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
|
||||
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
||||
| Branch Convention | Configurable branch and folder naming conventions for /specify with presets and custom patterns | `process` | Read+Write | [spec-kit-branch-convention](https://github.com/Quratulain-bilal/spec-kit-branch-convention) |
|
||||
| Brownfield Bootstrap | Bootstrap spec-kit for existing codebases — auto-discover architecture and adopt SDD incrementally | `process` | Read+Write | [spec-kit-brownfield](https://github.com/Quratulain-bilal/spec-kit-brownfield) |
|
||||
| Bugfix Workflow | Structured bugfix workflow — capture bugs, trace to spec artifacts, and patch specs surgically | `process` | Read+Write | [spec-kit-bugfix](https://github.com/Quratulain-bilal/spec-kit-bugfix) |
|
||||
| Canon | Adds canon-driven (baseline-driven) workflows: spec-first, code-first, spec-drift. Requires Canon Core preset installation. | `process` | Read+Write | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon/tree/master/extension) |
|
||||
| CI Guard | Spec compliance gates for CI/CD — verify specs exist, check drift, and block merges on gaps | `process` | Read-only | [spec-kit-ci-guard](https://github.com/Quratulain-bilal/spec-kit-ci-guard) |
|
||||
| Checkpoint Extension | Commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end | `code` | Read+Write | [spec-kit-checkpoint](https://github.com/aaronrsun/spec-kit-checkpoint) |
|
||||
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | `code` | Read+Write | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
||||
| Conduct Extension | Orchestrates spec-kit phases via sub-agent delegation to reduce context pollution. | `process` | Read+Write | [spec-kit-conduct-ext](https://github.com/twbrandon7/spec-kit-conduct-ext) |
|
||||
@@ -199,7 +196,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Fix Findings | Automated analyze-fix-reanalyze loop that resolves spec findings until clean | `code` | Read+Write | [spec-kit-fix-findings](https://github.com/Quratulain-bilal/spec-kit-fix-findings) |
|
||||
| FixIt Extension | Spec-aware bug fixing — maps bugs to spec artifacts, proposes a plan, applies minimal changes | `code` | Read+Write | [spec-kit-fixit](https://github.com/speckit-community/spec-kit-fixit) |
|
||||
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
|
||||
| GitHub Issues Integration | Generate spec artifacts from GitHub Issues - import issues, sync updates, and maintain bidirectional traceability | `integration` | Read+Write | [spec-kit-github-issues](https://github.com/Fatima367/spec-kit-github-issues) |
|
||||
| Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) |
|
||||
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
|
||||
| Learning Extension | Generate educational guides from implementations and enhance clarifications with mentoring context | `docs` | Read+Write | [spec-kit-learn](https://github.com/imviancagrace/spec-kit-learn) |
|
||||
@@ -210,11 +206,9 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| MAQA Jira Integration | Jira integration for MAQA — syncs Stories and Subtasks as features progress through the board | `integration` | Read+Write | [spec-kit-maqa-jira](https://github.com/GenieRobot/spec-kit-maqa-jira) |
|
||||
| MAQA Linear Integration | Linear integration for MAQA — syncs issues and sub-issues across workflow states as features progress | `integration` | Read+Write | [spec-kit-maqa-linear](https://github.com/GenieRobot/spec-kit-maqa-linear) |
|
||||
| MAQA Trello Integration | Trello board integration for MAQA — populates board from specs, moves cards, real-time checklist ticking | `integration` | Read+Write | [spec-kit-maqa-trello](https://github.com/GenieRobot/spec-kit-maqa-trello) |
|
||||
| MemoryLint | Agent memory governance tool: Automatically audits and fixes boundary conflicts between AGENTS.md and the constitution. | `process` | Read+Write | [memorylint](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/memorylint) |
|
||||
| Onboard | Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step | `process` | Read+Write | [spec-kit-onboard](https://github.com/dmux/spec-kit-onboard) |
|
||||
| Optimize | Audit and optimize AI governance for context efficiency — token budgets, rule health, interpretability, compression, coherence, and echo detection | `process` | Read+Write | [spec-kit-optimize](https://github.com/sakitA/spec-kit-optimize) |
|
||||
| Plan Review Gate | Require spec.md and plan.md to be merged via MR/PR before allowing task generation | `process` | Read-only | [spec-kit-plan-review-gate](https://github.com/luno/spec-kit-plan-review-gate) |
|
||||
| PR Bridge | Auto-generate pull request descriptions, checklists, and summaries from spec artifacts | `process` | Read-only | [spec-kit-pr-bridge-](https://github.com/Quratulain-bilal/spec-kit-pr-bridge-) |
|
||||
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
|
||||
| Product Forge | Full product lifecycle: research → product spec → SpecKit → implement → verify → test | `process` | Read+Write | [speckit-product-forge](https://github.com/VaiYav/speckit-product-forge) |
|
||||
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
|
||||
@@ -233,16 +227,11 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Spec Diagram | Auto-generate Mermaid diagrams of SDD workflow state, feature progress, and task dependencies | `visibility` | Read-only | [spec-kit-diagram-](https://github.com/Quratulain-bilal/spec-kit-diagram-) |
|
||||
| Spec Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
|
||||
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
||||
| SpecTest | Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements | `code` | Read+Write | [spec-kit-spectest](https://github.com/Quratulain-bilal/spec-kit-spectest) |
|
||||
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
|
||||
| Status Report | Project status, feature progress, and next-action recommendations for spec-driven workflows | `visibility` | Read-only | [Open-Agent-Tools/spec-kit-status](https://github.com/Open-Agent-Tools/spec-kit-status) |
|
||||
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
|
||||
| TinySpec | Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process | `process` | Read+Write | [spec-kit-tinyspec](https://github.com/Quratulain-bilal/spec-kit-tinyspec) |
|
||||
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
||||
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
||||
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
|
||||
| What-if Analysis | Preview the downstream impact (complexity, effort, tasks, risks) of requirement changes before committing to them | `visibility` | Read-only | [spec-kit-whatif](https://github.com/DevAbdullah90/spec-kit-whatif) |
|
||||
| Worktree Isolation | Spawn isolated git worktrees for parallel feature development without checkout switching | `process` | Read+Write | [spec-kit-worktree](https://github.com/Quratulain-bilal/spec-kit-worktree) |
|
||||
|
||||
To submit your own extension, see the [Extension Publishing Guide](extensions/EXTENSION-PUBLISHING-GUIDE.md).
|
||||
|
||||
@@ -258,7 +247,6 @@ The following community-contributed presets customize how Spec Kit behaves — o
|
||||
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
|
||||
| Canon Core | Adapts original Spec Kit workflow to work together with Canon extension | 2 templates, 8 commands | — | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon) |
|
||||
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
|
||||
| Multi-Repo Branching | Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases | 2 commands | — | [spec-kit-preset-multi-repo-branching](https://github.com/sakitA/spec-kit-preset-multi-repo-branching) |
|
||||
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
|
||||
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
|
||||
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |
|
||||
@@ -297,9 +285,8 @@ Community projects that extend, visualize, or build on Spec Kit:
|
||||
|
||||
- **[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.
|
||||
|
||||
- **[SpecKit Companion](https://marketplace.visualstudio.com/items?itemName=alfredoperez.speckit-companion)** — A VS Code extension that brings a visual GUI to Spec Kit. Browse specs in a rich markdown viewer with clickable file references, create specifications with image attachments, comment and refine each step inline (GitHub-style review), track your progress through the SDD workflow with a visual phase stepper, and manage steering documents like constitutions and templates.
|
||||
|
||||
## 🤖 Supported AI Agents
|
||||
|
||||
| Agent | Support | Notes |
|
||||
| ------------------------------------------------------------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Qoder CLI](https://qoder.com/cli) | ✅ | |
|
||||
@@ -312,7 +299,6 @@ Community projects that extend, visualize, or build on Spec Kit:
|
||||
| [Cursor](https://cursor.sh/) | ✅ | |
|
||||
| [Forge](https://forgecode.dev/) | ✅ | CLI tool: `forge` |
|
||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | |
|
||||
| [Goose](https://block.github.io/goose/) | ✅ | Uses YAML recipe format in `.goose/recipes/` with slash command support |
|
||||
| [GitHub Copilot](https://code.visualstudio.com/) | ✅ | |
|
||||
| [IBM Bob](https://www.ibm.com/products/bob) | ✅ | IDE-based agent with slash command support |
|
||||
| [Jules](https://jules.google.com/) | ✅ | |
|
||||
@@ -332,63 +318,22 @@ Community projects that extend, visualize, or build on Spec Kit:
|
||||
| [Trae](https://www.trae.ai/) | ✅ | |
|
||||
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
|
||||
|
||||
## Available Slash Commands
|
||||
|
||||
After running `specify init`, your AI coding agent will have access to these slash commands for structured development. If you pass `--ai <agent> --ai-skills`, Spec Kit installs agent skills instead of slash-command prompt files; `--ai-skills` requires `--ai`.
|
||||
|
||||
#### Core Commands
|
||||
|
||||
Essential commands for the Spec-Driven Development workflow:
|
||||
|
||||
| Command | Agent Skill | Description |
|
||||
| ------------------------ | ---------------------- | -------------------------------------------------------------------------- |
|
||||
| `/speckit.constitution` | `speckit-constitution` | Create or update project governing principles and development guidelines |
|
||||
| `/speckit.specify` | `speckit-specify` | Define what you want to build (requirements and user stories) |
|
||||
| `/speckit.plan` | `speckit-plan` | Create technical implementation plans with your chosen tech stack |
|
||||
| `/speckit.tasks` | `speckit-tasks` | Generate actionable task lists for implementation |
|
||||
| `/speckit.taskstoissues` | `speckit-taskstoissues`| Convert generated task lists into GitHub issues for tracking and execution |
|
||||
| `/speckit.implement` | `speckit-implement` | Execute all tasks to build the feature according to the plan |
|
||||
|
||||
#### Optional Commands
|
||||
|
||||
Additional commands for enhanced quality and validation:
|
||||
|
||||
| Command | Agent Skill | Description |
|
||||
| -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `/speckit.clarify` | `speckit-clarify` | Clarify underspecified areas (recommended before `/speckit.plan`; formerly `/quizme`) |
|
||||
| `/speckit.analyze` | `speckit-analyze` | Cross-artifact consistency & coverage analysis (run after `/speckit.tasks`, before `/speckit.implement`) |
|
||||
| `/speckit.checklist` | `speckit-checklist` | Generate custom quality checklists that validate requirements completeness, clarity, and consistency (like "unit tests for English") |
|
||||
|
||||
## 🔧 Specify CLI Reference
|
||||
|
||||
The `specify` tool is invoked as
|
||||
|
||||
```text
|
||||
specify <COMMAND> [SUBCOMMAND] [OPTIONS]
|
||||
```
|
||||
|
||||
and supports the following commands:
|
||||
The `specify` command supports the following options:
|
||||
|
||||
### Commands
|
||||
|
||||
| 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`, `forge`, etc.) |
|
||||
| `version` | Show the currently installed Spec Kit version. |
|
||||
| `extension` | Manage extensions |
|
||||
| `preset` | Manage presets |
|
||||
| `integration` | Manage integrations |
|
||||
| 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`, `forge`, etc.) |
|
||||
|
||||
### `specify init` Arguments & Options
|
||||
|
||||
```bash
|
||||
specify init [PROJECT_NAME] <OPTIONS>
|
||||
```
|
||||
|
||||
| Argument/Option | Type | Description |
|
||||
| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `<PROJECT_NAME>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
|
||||
| ---------------------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `<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`, `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) |
|
||||
@@ -485,6 +430,38 @@ specify init my-project --ai claude --branch-numbering timestamp
|
||||
specify check
|
||||
```
|
||||
|
||||
### Available Slash Commands
|
||||
|
||||
After running `specify init`, your AI coding agent will have access to these structured development commands.
|
||||
|
||||
Most agents expose the traditional dotted slash commands shown below, like `/speckit.plan`.
|
||||
|
||||
Claude Code installs spec-kit as skills and invokes them as `/speckit-constitution`, `/speckit-specify`, `/speckit-plan`, `/speckit-tasks`, and `/speckit-implement`.
|
||||
|
||||
For Codex CLI, `--ai-skills` installs spec-kit as agent skills instead of slash-command prompt files. In Codex skills mode, invoke spec-kit as `$speckit-constitution`, `$speckit-specify`, `$speckit-plan`, `$speckit-tasks`, and `$speckit-implement`.
|
||||
|
||||
#### Core Commands
|
||||
|
||||
Essential commands for the Spec-Driven Development workflow:
|
||||
|
||||
| Command | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------ |
|
||||
| `/speckit.constitution` | Create or update project governing principles and development guidelines |
|
||||
| `/speckit.specify` | Define what you want to build (requirements and user stories) |
|
||||
| `/speckit.plan` | Create technical implementation plans with your chosen tech stack |
|
||||
| `/speckit.tasks` | Generate actionable task lists for implementation |
|
||||
| `/speckit.implement` | Execute all tasks to build the feature according to the plan |
|
||||
|
||||
#### Optional Commands
|
||||
|
||||
Additional commands for enhanced quality and validation:
|
||||
|
||||
| Command | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `/speckit.clarify` | Clarify underspecified areas (recommended before `/speckit.plan`; formerly `/quizme`) |
|
||||
| `/speckit.analyze` | Cross-artifact consistency & coverage analysis (run after `/speckit.tasks`, before `/speckit.implement`) |
|
||||
| `/speckit.checklist` | Generate custom quality checklists that validate requirements completeness, clarity, and consistency (like "unit tests for English") |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
@@ -495,18 +472,21 @@ specify check
|
||||
|
||||
Spec Kit can be tailored to your needs through two complementary systems — **extensions** and **presets** — plus project-local overrides for one-off adjustments:
|
||||
|
||||
| Priority | Component Type | Location |
|
||||
| -------: | ------------------------------------------------- | -------------------------------- |
|
||||
| ⬆ 1 | Project-Local Overrides | `.specify/templates/overrides/` |
|
||||
| 2 | Presets — Customize core & extensions | `.specify/presets/templates/` |
|
||||
| 3 | Extensions — Add new capabilities | `.specify/extensions/templates/` |
|
||||
| ⬇ 4 | Spec Kit Core — Built-in SDD commands & templates | `.specify/templates/` |
|
||||
```mermaid
|
||||
block-beta
|
||||
columns 1
|
||||
overrides["⬆ Highest priority\nProject-Local Overrides\n.specify/templates/overrides/"]
|
||||
presets["Presets — Customize core & extensions\n.specify/presets/<preset-id>/templates/"]
|
||||
extensions["Extensions — Add new capabilities\n.specify/extensions/<ext-id>/templates/"]
|
||||
core["Spec Kit Core — Built-in SDD commands & templates\n.specify/templates/\n⬇ Lowest priority"]
|
||||
|
||||
- **Templates** are resolved at **runtime** — Spec Kit walks the stack top-down and uses the first match.
|
||||
- Project-local overrides (`.specify/templates/overrides/`) let you make one-off adjustments for a single project without creating a full preset.
|
||||
- **Extension/preset commands** are applied at **install time** — when you run `specify extension add` or `specify preset add`, command files are written into agent directories (e.g., `.claude/commands/`).
|
||||
- If multiple presets or extensions provide the same command, the highest-priority version wins. On removal, the next-highest-priority version is restored automatically.
|
||||
- If no overrides or customizations exist, Spec Kit uses its core defaults.
|
||||
style overrides fill:transparent,stroke:#999
|
||||
style presets fill:transparent,stroke:#4a9eda
|
||||
style extensions fill:transparent,stroke:#4a9e4a
|
||||
style core fill:transparent,stroke:#e6a817
|
||||
```
|
||||
|
||||
**Templates** are resolved at **runtime** — Spec Kit walks the stack top-down and uses the first match. Project-local overrides (`.specify/templates/overrides/`) let you make one-off adjustments for a single project without creating a full preset. **Commands** are applied at **install time** — when you run `specify extension add` or `specify preset add`, command files are written into agent directories (e.g., `.claude/commands/`). If multiple presets or extensions provide the same command, the highest-priority version wins. On removal, the next-highest-priority version is restored automatically. If no overrides or customizations exist, Spec Kit uses its core defaults.
|
||||
|
||||
### Extensions — Add New Capabilities
|
||||
|
||||
@@ -657,7 +637,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, Forge, Goose, 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
|
||||
|
||||
@@ -292,7 +292,7 @@ This tells Spec Kit which feature directory to use when creating specs, plans, a
|
||||
```bash
|
||||
ls -la .claude/commands/ # Claude Code
|
||||
ls -la .gemini/commands/ # Gemini
|
||||
ls -la .cursor/skills/ # Cursor
|
||||
ls -la .cursor/commands/ # Cursor
|
||||
ls -la .pi/prompts/ # Pi Coding Agent
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-13T14:39:00Z",
|
||||
"updated_at": "2026-04-08T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||
"extensions": {
|
||||
"aide": {
|
||||
@@ -138,70 +138,6 @@
|
||||
"created_at": "2026-04-08T00:00:00Z",
|
||||
"updated_at": "2026-04-08T00:00:00Z"
|
||||
},
|
||||
"brownfield": {
|
||||
"name": "Brownfield Bootstrap",
|
||||
"id": "brownfield",
|
||||
"description": "Bootstrap spec-kit for existing codebases — auto-discover architecture and adopt SDD incrementally.",
|
||||
"author": "Quratulain-bilal",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Quratulain-bilal/spec-kit-brownfield/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Quratulain-bilal/spec-kit-brownfield",
|
||||
"homepage": "https://github.com/Quratulain-bilal/spec-kit-brownfield",
|
||||
"documentation": "https://github.com/Quratulain-bilal/spec-kit-brownfield/blob/main/README.md",
|
||||
"changelog": "https://github.com/Quratulain-bilal/spec-kit-brownfield/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.4.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 4,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"brownfield",
|
||||
"bootstrap",
|
||||
"existing-project",
|
||||
"migration",
|
||||
"onboarding"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-10T00:00:00Z",
|
||||
"updated_at": "2026-04-10T00:00:00Z"
|
||||
},
|
||||
"bugfix": {
|
||||
"name": "Bugfix Workflow",
|
||||
"id": "bugfix",
|
||||
"description": "Structured bugfix workflow — capture bugs, trace to spec artifacts, and patch specs surgically.",
|
||||
"author": "Quratulain-bilal",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Quratulain-bilal/spec-kit-bugfix/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Quratulain-bilal/spec-kit-bugfix",
|
||||
"homepage": "https://github.com/Quratulain-bilal/spec-kit-bugfix",
|
||||
"documentation": "https://github.com/Quratulain-bilal/spec-kit-bugfix/blob/main/README.md",
|
||||
"changelog": "https://github.com/Quratulain-bilal/spec-kit-bugfix/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.4.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"bugfix",
|
||||
"debugging",
|
||||
"workflow",
|
||||
"traceability",
|
||||
"maintenance"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-09T00:00:00Z",
|
||||
"updated_at": "2026-04-09T00:00:00Z"
|
||||
},
|
||||
"canon": {
|
||||
"name": "Canon",
|
||||
"id": "canon",
|
||||
@@ -237,39 +173,6 @@
|
||||
"created_at": "2026-03-29T00:00:00Z",
|
||||
"updated_at": "2026-03-29T00:00:00Z"
|
||||
},
|
||||
"ci-guard": {
|
||||
"name": "CI Guard",
|
||||
"id": "ci-guard",
|
||||
"description": "Spec compliance gates for CI/CD — verify specs exist, check drift, and block merges on gaps.",
|
||||
"author": "Quratulain-bilal",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Quratulain-bilal/spec-kit-ci-guard",
|
||||
"homepage": "https://github.com/Quratulain-bilal/spec-kit-ci-guard",
|
||||
"documentation": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/blob/main/README.md",
|
||||
"changelog": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.4.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 5,
|
||||
"hooks": 2
|
||||
},
|
||||
"tags": [
|
||||
"ci-cd",
|
||||
"compliance",
|
||||
"governance",
|
||||
"quality-gate",
|
||||
"drift-detection",
|
||||
"automation"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-10T17:00:00Z",
|
||||
"updated_at": "2026-04-10T17:00:00Z"
|
||||
},
|
||||
"checkpoint": {
|
||||
"name": "Checkpoint Extension",
|
||||
"id": "checkpoint",
|
||||
@@ -649,46 +552,6 @@
|
||||
"created_at": "2026-03-06T00:00:00Z",
|
||||
"updated_at": "2026-03-31T00:00:00Z"
|
||||
},
|
||||
"github-issues": {
|
||||
"name": "GitHub Issues Integration",
|
||||
"id": "github-issues",
|
||||
"description": "Generate spec artifacts from GitHub Issues - import issues, sync updates, and maintain bidirectional traceability",
|
||||
"author": "Fatima367",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Fatima367/spec-kit-github-issues/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Fatima367/spec-kit-github-issues",
|
||||
"homepage": "https://github.com/Fatima367/spec-kit-github-issues",
|
||||
"documentation": "https://github.com/Fatima367/spec-kit-github-issues/blob/main/README.md",
|
||||
"changelog": "https://github.com/Fatima367/spec-kit-github-issues/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.1.0",
|
||||
"tools": [
|
||||
{
|
||||
"name": "gh",
|
||||
"version": ">=2.0.0",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"integration",
|
||||
"github",
|
||||
"issues",
|
||||
"import",
|
||||
"sync",
|
||||
"traceability"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-12T15:30:00Z",
|
||||
"updated_at": "2026-04-13T14:39:00Z"
|
||||
},
|
||||
"iterate": {
|
||||
"name": "Iterate",
|
||||
"id": "iterate",
|
||||
@@ -1007,38 +870,6 @@
|
||||
"created_at": "2026-03-26T00:00:00Z",
|
||||
"updated_at": "2026-03-26T00:00:00Z"
|
||||
},
|
||||
"memorylint": {
|
||||
"name": "MemoryLint",
|
||||
"id": "memorylint",
|
||||
"description": "Agent memory governance tool: Automatically audits and fixes boundary conflicts between AGENTS.md and the constitution.",
|
||||
"author": "RbBtSn0w",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/RbBtSn0w/spec-kit-extensions/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/RbBtSn0w/spec-kit-extensions",
|
||||
"homepage": "https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/memorylint",
|
||||
"documentation": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/memorylint/README.md",
|
||||
"changelog": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/memorylint/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.5.1"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"memory",
|
||||
"governance",
|
||||
"constitution",
|
||||
"agents-md",
|
||||
"process"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-09T00:00:00Z",
|
||||
"updated_at": "2026-04-09T00:00:00Z"
|
||||
},
|
||||
"onboard": {
|
||||
"name": "Onboard",
|
||||
"id": "onboard",
|
||||
@@ -1135,38 +966,6 @@
|
||||
"created_at": "2026-03-27T08:22:30Z",
|
||||
"updated_at": "2026-03-27T08:22:30Z"
|
||||
},
|
||||
"pr-bridge": {
|
||||
"name": "PR Bridge",
|
||||
"id": "pr-bridge",
|
||||
"description": "Auto-generate pull request descriptions, checklists, and summaries from spec artifacts.",
|
||||
"author": "Quratulain-bilal",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-",
|
||||
"homepage": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-",
|
||||
"documentation": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/blob/main/README.md",
|
||||
"changelog": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.4.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"pull-request",
|
||||
"automation",
|
||||
"traceability",
|
||||
"workflow",
|
||||
"review"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-10T00:00:00Z",
|
||||
"updated_at": "2026-04-10T00:00:00Z"
|
||||
},
|
||||
"presetify": {
|
||||
"name": "Presetify",
|
||||
"id": "presetify",
|
||||
@@ -1265,8 +1064,8 @@
|
||||
"id": "ralph",
|
||||
"description": "Autonomous implementation loop using AI agent CLI.",
|
||||
"author": "Rubiss",
|
||||
"version": "1.0.1",
|
||||
"download_url": "https://github.com/Rubiss/spec-kit-ralph/archive/refs/tags/v1.0.1.zip",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Rubiss/spec-kit-ralph/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Rubiss/spec-kit-ralph",
|
||||
"homepage": "https://github.com/Rubiss/spec-kit-ralph",
|
||||
"documentation": "https://github.com/Rubiss/spec-kit-ralph/blob/main/README.md",
|
||||
@@ -1299,7 +1098,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-03-09T00:00:00Z",
|
||||
"updated_at": "2026-04-12T19:00:00Z"
|
||||
"updated_at": "2026-03-09T00:00:00Z"
|
||||
},
|
||||
"reconcile": {
|
||||
"name": "Reconcile Extension",
|
||||
@@ -1468,8 +1267,8 @@
|
||||
"id": "review",
|
||||
"description": "Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification.",
|
||||
"author": "ismaelJimenez",
|
||||
"version": "1.0.1",
|
||||
"download_url": "https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.1.zip",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/ismaelJimenez/spec-kit-review",
|
||||
"homepage": "https://github.com/ismaelJimenez/spec-kit-review",
|
||||
"documentation": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/README.md",
|
||||
@@ -1495,7 +1294,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-03-06T00:00:00Z",
|
||||
"updated_at": "2026-04-09T00:00:00Z"
|
||||
"updated_at": "2026-03-06T00:00:00Z"
|
||||
},
|
||||
"security-review": {
|
||||
"name": "Security Review",
|
||||
@@ -1591,39 +1390,6 @@
|
||||
"created_at": "2026-03-18T00:00:00Z",
|
||||
"updated_at": "2026-03-18T00:00:00Z"
|
||||
},
|
||||
"spectest": {
|
||||
"name": "SpecTest",
|
||||
"id": "spectest",
|
||||
"description": "Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements.",
|
||||
"author": "Quratulain-bilal",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Quratulain-bilal/spec-kit-spectest/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Quratulain-bilal/spec-kit-spectest",
|
||||
"homepage": "https://github.com/Quratulain-bilal/spec-kit-spectest",
|
||||
"documentation": "https://github.com/Quratulain-bilal/spec-kit-spectest/blob/main/README.md",
|
||||
"changelog": "https://github.com/Quratulain-bilal/spec-kit-spectest/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.4.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 4,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"testing",
|
||||
"test-generation",
|
||||
"coverage",
|
||||
"quality",
|
||||
"automation",
|
||||
"traceability"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-10T16:00:00Z",
|
||||
"updated_at": "2026-04-10T16:00:00Z"
|
||||
},
|
||||
"staff-review": {
|
||||
"name": "Staff Review Extension",
|
||||
"id": "staff-review",
|
||||
@@ -1686,36 +1452,6 @@
|
||||
"created_at": "2026-03-16T00:00:00Z",
|
||||
"updated_at": "2026-03-16T00:00:00Z"
|
||||
},
|
||||
"status-report": {
|
||||
"name": "Status Report",
|
||||
"id": "status-report",
|
||||
"description": "Project status, feature progress, and next-action recommendations for spec-driven workflows.",
|
||||
"author": "Open-Agent-Tools",
|
||||
"version": "1.2.5",
|
||||
"download_url": "https://github.com/Open-Agent-Tools/spec-kit-status/archive/refs/tags/v1.2.5.zip",
|
||||
"repository": "https://github.com/Open-Agent-Tools/spec-kit-status",
|
||||
"homepage": "https://github.com/Open-Agent-Tools/spec-kit-status",
|
||||
"documentation": "https://github.com/Open-Agent-Tools/spec-kit-status/blob/main/README.md",
|
||||
"changelog": "https://github.com/Open-Agent-Tools/spec-kit-status/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.1.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"workflow",
|
||||
"project-management",
|
||||
"status"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-08T15:05:14Z",
|
||||
"updated_at": "2026-04-08T15:05:14Z"
|
||||
},
|
||||
"superb": {
|
||||
"name": "Superpowers Bridge",
|
||||
"id": "superb",
|
||||
@@ -1791,38 +1527,6 @@
|
||||
"created_at": "2026-03-02T00:00:00Z",
|
||||
"updated_at": "2026-03-02T00:00:00Z"
|
||||
},
|
||||
"tinyspec": {
|
||||
"name": "TinySpec",
|
||||
"id": "tinyspec",
|
||||
"description": "Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process.",
|
||||
"author": "Quratulain-bilal",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Quratulain-bilal/spec-kit-tinyspec",
|
||||
"homepage": "https://github.com/Quratulain-bilal/spec-kit-tinyspec",
|
||||
"documentation": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/blob/main/README.md",
|
||||
"changelog": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.4.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"lightweight",
|
||||
"small-tasks",
|
||||
"workflow",
|
||||
"productivity",
|
||||
"efficiency"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-10T00:00:00Z",
|
||||
"updated_at": "2026-04-10T00:00:00Z"
|
||||
},
|
||||
"v-model": {
|
||||
"name": "V-Model Extension Pack",
|
||||
"id": "v-model",
|
||||
@@ -1860,8 +1564,8 @@
|
||||
"id": "verify",
|
||||
"description": "Post-implementation quality gate that validates implemented code against specification artifacts.",
|
||||
"author": "ismaelJimenez",
|
||||
"version": "1.0.3",
|
||||
"download_url": "https://github.com/ismaelJimenez/spec-kit-verify/archive/refs/tags/v1.0.3.zip",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/ismaelJimenez/spec-kit-verify/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/ismaelJimenez/spec-kit-verify",
|
||||
"homepage": "https://github.com/ismaelJimenez/spec-kit-verify",
|
||||
"documentation": "https://github.com/ismaelJimenez/spec-kit-verify/blob/main/README.md",
|
||||
@@ -1885,7 +1589,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-03-03T00:00:00Z",
|
||||
"updated_at": "2026-04-09T00:00:00Z"
|
||||
"updated_at": "2026-03-03T00:00:00Z"
|
||||
},
|
||||
"verify-tasks": {
|
||||
"name": "Verify Tasks Extension",
|
||||
@@ -1917,66 +1621,6 @@
|
||||
"stars": 0,
|
||||
"created_at": "2026-03-16T00:00:00Z",
|
||||
"updated_at": "2026-03-16T00:00:00Z"
|
||||
},
|
||||
"whatif": {
|
||||
"name": "What-if Analysis",
|
||||
"id": "whatif",
|
||||
"description": "Preview the downstream impact (complexity, effort, tasks, risks) of requirement changes before committing to them.",
|
||||
"author": "DevAbdullah90",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/DevAbdullah90/spec-kit-whatif",
|
||||
"homepage": "https://github.com/DevAbdullah90/spec-kit-whatif",
|
||||
"documentation": "https://github.com/DevAbdullah90/spec-kit-whatif/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.6.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"analysis",
|
||||
"planning",
|
||||
"simulation"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-13T00:00:00Z",
|
||||
"updated_at": "2026-04-13T00:00:00Z"
|
||||
},
|
||||
"worktree": {
|
||||
"name": "Worktree Isolation",
|
||||
"id": "worktree",
|
||||
"description": "Spawn isolated git worktrees for parallel feature development without checkout switching.",
|
||||
"author": "Quratulain-bilal",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/Quratulain-bilal/spec-kit-worktree/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/Quratulain-bilal/spec-kit-worktree",
|
||||
"homepage": "https://github.com/Quratulain-bilal/spec-kit-worktree",
|
||||
"documentation": "https://github.com/Quratulain-bilal/spec-kit-worktree/blob/main/README.md",
|
||||
"changelog": "https://github.com/Quratulain-bilal/spec-kit-worktree/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.4.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"worktree",
|
||||
"git",
|
||||
"parallel",
|
||||
"isolation",
|
||||
"workflow"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-04-09T00:00:00Z",
|
||||
"updated_at": "2026-04-09T00:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-10T00:00:00Z",
|
||||
"updated_at": "2026-04-06T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
||||
"extensions": {
|
||||
"git": {
|
||||
@@ -10,13 +10,27 @@
|
||||
"description": "Feature branch creation, numbering (sequential/timestamp), validation, and Git remote detection",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"bundled": true,
|
||||
"download_url": "https://github.com/github/spec-kit/releases/download/ext-git-v1.0.0/git.zip",
|
||||
"tags": [
|
||||
"git",
|
||||
"branching",
|
||||
"workflow",
|
||||
"core"
|
||||
]
|
||||
},
|
||||
"selftest": {
|
||||
"name": "Spec Kit Self-Test Utility",
|
||||
"id": "selftest",
|
||||
"version": "1.0.0",
|
||||
"description": "Verifies catalog extensions by programmatically walking through the discovery, installation, and registration lifecycle.",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"download_url": "https://github.com/github/spec-kit/releases/download/selftest-v1.0.0/selftest.zip",
|
||||
"tags": [
|
||||
"testing",
|
||||
"core",
|
||||
"utility"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-09T08:00:00Z",
|
||||
"updated_at": "2026-04-06T06:30:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
|
||||
"presets": {
|
||||
"aide-in-place": {
|
||||
@@ -78,33 +78,6 @@
|
||||
"wave-dag"
|
||||
]
|
||||
},
|
||||
"multi-repo-branching": {
|
||||
"name": "Multi-Repo Branching",
|
||||
"id": "multi-repo-branching",
|
||||
"version": "1.0.0",
|
||||
"description": "Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases.",
|
||||
"author": "sakitA",
|
||||
"repository": "https://github.com/sakitA/spec-kit-preset-multi-repo-branching",
|
||||
"download_url": "https://github.com/sakitA/spec-kit-preset-multi-repo-branching/archive/refs/tags/v1.0.0.zip",
|
||||
"homepage": "https://github.com/sakitA/spec-kit-preset-multi-repo-branching",
|
||||
"documentation": "https://github.com/sakitA/spec-kit-preset-multi-repo-branching/blob/master/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.4.0"
|
||||
},
|
||||
"provides": {
|
||||
"templates": 0,
|
||||
"commands": 2
|
||||
},
|
||||
"tags": [
|
||||
"multi-repo-branching",
|
||||
"multi-module",
|
||||
"submodules",
|
||||
"monorepo"
|
||||
],
|
||||
"created_at": "2026-04-09T00:00:00Z",
|
||||
"updated_at": "2026-04-09T00:00:00Z"
|
||||
},
|
||||
"pirate": {
|
||||
"name": "Pirate Speak (Full)",
|
||||
"id": "pirate",
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-10T00:00:00Z",
|
||||
"updated_at": "2026-03-10T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.json",
|
||||
"presets": {
|
||||
"lean": {
|
||||
"name": "Lean Workflow",
|
||||
"id": "lean",
|
||||
"version": "1.0.0",
|
||||
"description": "Minimal core workflow commands - just the prompt, just the artifact",
|
||||
"author": "github",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"bundled": true,
|
||||
"tags": [
|
||||
"lean",
|
||||
"minimal",
|
||||
"workflow",
|
||||
"core"
|
||||
]
|
||||
}
|
||||
}
|
||||
"presets": {}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
description: Create or update the project constitution.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
## Outline
|
||||
|
||||
1. Create or update the project constitution and store it in `.specify/memory/constitution.md`.
|
||||
- Project name, guiding principles, non-negotiable rules
|
||||
- Derive from user input and existing repo context (README, docs)
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
description: Execute the implementation plan by processing all tasks in tasks.md.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
## Outline
|
||||
|
||||
1. Read `.specify/feature.json` to get the feature directory path.
|
||||
|
||||
2. **Load context**: `.specify/memory/constitution.md` and `<feature_directory>/spec.md` and `<feature_directory>/plan.md` and `<feature_directory>/tasks.md`.
|
||||
|
||||
3. **Execute tasks** in order:
|
||||
- Complete each task before moving to the next
|
||||
- Mark completed tasks by changing `- [ ]` to `- [x]` in `<feature_directory>/tasks.md`
|
||||
- Halt on failure and report the issue
|
||||
|
||||
4. **Validate**: Verify all tasks are completed and the implementation matches the spec.
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
description: Create a plan and store it in plan.md.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
## Outline
|
||||
|
||||
1. Read `.specify/feature.json` to get the feature directory path.
|
||||
|
||||
2. **Load context**: `.specify/memory/constitution.md` and `<feature_directory>/spec.md`.
|
||||
|
||||
3. Create an implementation plan and store it in `<feature_directory>/plan.md`.
|
||||
- Technical context: tech stack, dependencies, project structure
|
||||
- Design decisions, architecture, file structure
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Create a specification and store it in spec.md.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
## Outline
|
||||
|
||||
1. **Ask the user** for the feature directory path (e.g., `specs/my-feature`). Do not proceed until provided.
|
||||
|
||||
2. Create the directory and write `.specify/feature.json`:
|
||||
```json
|
||||
{ "feature_directory": "<feature_directory>" }
|
||||
```
|
||||
|
||||
3. Create a specification from the user input and store it in `<feature_directory>/spec.md`.
|
||||
- Overview, functional requirements, user scenarios, success criteria
|
||||
- Every requirement must be testable
|
||||
- Make informed defaults for unspecified details
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
description: Create the tasks needed for implementation and store them in tasks.md.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
## Outline
|
||||
|
||||
1. Read `.specify/feature.json` to get the feature directory path.
|
||||
|
||||
2. **Load context**: `.specify/memory/constitution.md` and `<feature_directory>/spec.md` and `<feature_directory>/plan.md`.
|
||||
|
||||
3. Create dependency-ordered implementation tasks and store them in `<feature_directory>/tasks.md`.
|
||||
- Every task uses checklist format: `- [ ] [TaskID] Description with file path`
|
||||
- Organized by phase: setup, foundational, user stories in priority order, polish
|
||||
@@ -1,50 +0,0 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
preset:
|
||||
id: "lean"
|
||||
name: "Lean Workflow"
|
||||
version: "1.0.0"
|
||||
description: "Minimal core workflow commands - just the prompt, just the artifact"
|
||||
author: "github"
|
||||
repository: "https://github.com/github/spec-kit"
|
||||
license: "MIT"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.6.0"
|
||||
|
||||
provides:
|
||||
templates:
|
||||
- type: "command"
|
||||
name: "speckit.specify"
|
||||
file: "commands/speckit.specify.md"
|
||||
description: "Lean specify - create spec.md from a feature description"
|
||||
replaces: "speckit.specify"
|
||||
|
||||
- type: "command"
|
||||
name: "speckit.plan"
|
||||
file: "commands/speckit.plan.md"
|
||||
description: "Lean plan - create plan.md from the spec"
|
||||
replaces: "speckit.plan"
|
||||
|
||||
- type: "command"
|
||||
name: "speckit.tasks"
|
||||
file: "commands/speckit.tasks.md"
|
||||
description: "Lean tasks - create tasks.md from plan and spec"
|
||||
replaces: "speckit.tasks"
|
||||
|
||||
- type: "command"
|
||||
name: "speckit.implement"
|
||||
file: "commands/speckit.implement.md"
|
||||
description: "Lean implement - execute tasks from tasks.md"
|
||||
replaces: "speckit.implement"
|
||||
|
||||
- type: "command"
|
||||
name: "speckit.constitution"
|
||||
file: "commands/speckit.constitution.md"
|
||||
description: "Lean constitution - create or update project constitution"
|
||||
replaces: "speckit.constitution"
|
||||
|
||||
tags:
|
||||
- "lean"
|
||||
- "minimal"
|
||||
- "workflow"
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "specify-cli"
|
||||
version = "0.6.2"
|
||||
version = "0.5.1"
|
||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
@@ -41,8 +41,6 @@ packages = ["src/specify_cli"]
|
||||
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
||||
# Bundled extensions (installable via `specify extension add <name>`)
|
||||
"extensions/git" = "specify_cli/core_pack/extensions/git"
|
||||
# Bundled presets (installable via `specify preset add <name>` or `specify init --preset <name>`)
|
||||
"presets/lean" = "specify_cli/core_pack/presets/lean"
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
|
||||
@@ -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, Forge, Goose, 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|forge|goose|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, Pi, Forge, and Goose 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"
|
||||
@@ -710,15 +710,12 @@ update_specific_agent() {
|
||||
forge)
|
||||
update_agent_file "$AGENTS_FILE" "Forge" || return 1
|
||||
;;
|
||||
goose)
|
||||
update_agent_file "$AGENTS_FILE" "Goose" || return 1
|
||||
;;
|
||||
generic)
|
||||
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown agent type '$agent_type'"
|
||||
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|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
|
||||
@@ -762,7 +759,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/Amp/Kiro/Bob/Pi/Forge/Goose" || _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
|
||||
@@ -803,7 +800,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|forge|goose|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]"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
|
||||
@@ -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, forge, goose, 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','vibe','qodercli','kimi','trae','pi','iflow','forge','goose','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
|
||||
)
|
||||
|
||||
@@ -68,7 +68,6 @@ $KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md'
|
||||
$TRAE_FILE = Join-Path $REPO_ROOT '.trae/rules/project_rules.md'
|
||||
$IFLOW_FILE = Join-Path $REPO_ROOT 'IFLOW.md'
|
||||
$FORGE_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||
$GOOSE_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||
|
||||
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
||||
|
||||
@@ -418,9 +417,8 @@ function Update-SpecificAgent {
|
||||
'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' }
|
||||
'goose' { Update-AgentFile -TargetFile $GOOSE_FILE -AgentName 'Goose' }
|
||||
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
|
||||
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +460,7 @@ function Update-AllExistingAgents {
|
||||
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/Goose')) { $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 }
|
||||
@@ -492,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|forge|goose|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 {
|
||||
|
||||
@@ -621,31 +621,6 @@ def _locate_bundled_extension(extension_id: str) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def _locate_bundled_preset(preset_id: str) -> Path | None:
|
||||
"""Return the path to a bundled preset, or None.
|
||||
|
||||
Checks the wheel's core_pack first, then falls back to the
|
||||
source-checkout ``presets/<id>/`` directory.
|
||||
"""
|
||||
import re as _re
|
||||
if not _re.match(r'^[a-z0-9-]+$', preset_id):
|
||||
return None
|
||||
|
||||
core = _locate_core_pack()
|
||||
if core is not None:
|
||||
candidate = core / "presets" / preset_id
|
||||
if (candidate / "preset.yml").is_file():
|
||||
return candidate
|
||||
|
||||
# Source-checkout / editable install: look relative to repo root
|
||||
repo_root = Path(__file__).parent.parent.parent
|
||||
candidate = repo_root / "presets" / preset_id
|
||||
if (candidate / "preset.yml").is_file():
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _install_shared_infra(
|
||||
project_path: Path,
|
||||
script_type: str,
|
||||
@@ -1291,44 +1266,27 @@ def init(
|
||||
preset_manager = PresetManager(project_path)
|
||||
speckit_ver = get_speckit_version()
|
||||
|
||||
# Try local directory first, then bundled, then catalog
|
||||
# Try local directory first, then catalog
|
||||
local_path = Path(preset).resolve()
|
||||
if local_path.is_dir() and (local_path / "preset.yml").exists():
|
||||
preset_manager.install_from_directory(local_path, speckit_ver)
|
||||
else:
|
||||
bundled_path = _locate_bundled_preset(preset)
|
||||
if bundled_path:
|
||||
preset_manager.install_from_directory(bundled_path, speckit_ver)
|
||||
preset_catalog = PresetCatalog(project_path)
|
||||
pack_info = preset_catalog.get_pack_info(preset)
|
||||
if not pack_info:
|
||||
console.print(f"[yellow]Warning:[/yellow] Preset '{preset}' not found in catalog. Skipping.")
|
||||
else:
|
||||
preset_catalog = PresetCatalog(project_path)
|
||||
pack_info = preset_catalog.get_pack_info(preset)
|
||||
if not pack_info:
|
||||
console.print(f"[yellow]Warning:[/yellow] Preset '{preset}' not found in catalog. Skipping.")
|
||||
elif pack_info.get("bundled") and not pack_info.get("download_url"):
|
||||
from .extensions import REINSTALL_COMMAND
|
||||
console.print(
|
||||
f"[yellow]Warning:[/yellow] Preset '{preset}' is bundled with spec-kit "
|
||||
f"but could not be found in the installed package."
|
||||
)
|
||||
console.print(
|
||||
"This usually means the spec-kit installation is incomplete or corrupted."
|
||||
)
|
||||
console.print(f"Try reinstalling: {REINSTALL_COMMAND}")
|
||||
else:
|
||||
zip_path = None
|
||||
try:
|
||||
zip_path = preset_catalog.download_pack(preset)
|
||||
preset_manager.install_from_zip(zip_path, speckit_ver)
|
||||
# Clean up downloaded ZIP to avoid cache accumulation
|
||||
try:
|
||||
zip_path = preset_catalog.download_pack(preset)
|
||||
preset_manager.install_from_zip(zip_path, speckit_ver)
|
||||
except PresetError as preset_err:
|
||||
console.print(f"[yellow]Warning:[/yellow] Failed to install preset '{preset}': {preset_err}")
|
||||
finally:
|
||||
if zip_path is not None:
|
||||
# Clean up downloaded ZIP to avoid cache accumulation
|
||||
try:
|
||||
zip_path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
# Best-effort cleanup; failure to delete is non-fatal
|
||||
pass
|
||||
zip_path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
# Best-effort cleanup; failure to delete is non-fatal
|
||||
pass
|
||||
except PresetError as preset_err:
|
||||
console.print(f"[yellow]Warning:[/yellow] Failed to install preset '{preset}': {preset_err}")
|
||||
except Exception as preset_err:
|
||||
console.print(f"[yellow]Warning:[/yellow] Failed to install preset: {preset_err}")
|
||||
|
||||
@@ -1380,7 +1338,7 @@ def init(
|
||||
step_num = 2
|
||||
|
||||
# Determine skill display mode for the next-steps panel.
|
||||
# Skills integrations (codex, kimi, agy, trae, cursor-agent) should show skill invocation syntax.
|
||||
# Skills integrations (codex, kimi, agy, trae) should show skill invocation syntax.
|
||||
from .integrations.base import SkillsIntegration as _SkillsInt
|
||||
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)
|
||||
|
||||
@@ -1389,8 +1347,7 @@ def init(
|
||||
kimi_skill_mode = selected_ai == "kimi"
|
||||
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
|
||||
trae_skill_mode = selected_ai == "trae"
|
||||
cursor_agent_skill_mode = selected_ai == "cursor-agent" and (ai_skills or _is_skills_integration)
|
||||
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode
|
||||
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode
|
||||
|
||||
if codex_skill_mode and not ai_skills:
|
||||
# Integration path installed skills; show the helpful notice
|
||||
@@ -1399,9 +1356,6 @@ def init(
|
||||
if claude_skill_mode and not ai_skills:
|
||||
steps_lines.append(f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]")
|
||||
step_num += 1
|
||||
if cursor_agent_skill_mode and not ai_skills:
|
||||
steps_lines.append(f"{step_num}. Start Cursor Agent in this project directory; spec-kit skills were installed to [cyan].cursor/skills[/cyan]")
|
||||
step_num += 1
|
||||
usage_label = "skills" if native_skill_mode else "slash commands"
|
||||
|
||||
def _display_cmd(name: str) -> str:
|
||||
@@ -1411,8 +1365,6 @@ def init(
|
||||
return f"/speckit-{name}"
|
||||
if kimi_skill_mode:
|
||||
return f"/skill:speckit-{name}"
|
||||
if cursor_agent_skill_mode:
|
||||
return f"/speckit-{name}"
|
||||
return f"/speckit.{name}"
|
||||
|
||||
steps_lines.append(f"{step_num}. Start using {usage_label} with your AI agent:")
|
||||
@@ -2182,50 +2134,28 @@ def preset_add(
|
||||
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
|
||||
|
||||
elif pack_id:
|
||||
# Try bundled preset first, then catalog
|
||||
bundled_path = _locate_bundled_preset(pack_id)
|
||||
if bundled_path:
|
||||
console.print(f"Installing bundled preset [cyan]{pack_id}[/cyan]...")
|
||||
manifest = manager.install_from_directory(bundled_path, speckit_version, priority)
|
||||
catalog = PresetCatalog(project_root)
|
||||
pack_info = catalog.get_pack_info(pack_id)
|
||||
|
||||
if not pack_info:
|
||||
console.print(f"[red]Error:[/red] Preset '{pack_id}' not found in catalog")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not pack_info.get("_install_allowed", True):
|
||||
catalog_name = pack_info.get("_catalog_name", "unknown")
|
||||
console.print(f"[red]Error:[/red] Preset '{pack_id}' is from the '{catalog_name}' catalog which is discovery-only (install not allowed).")
|
||||
console.print("Add the catalog with --install-allowed or install from the preset's repository directly with --from.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
console.print(f"Installing preset [cyan]{pack_info.get('name', pack_id)}[/cyan]...")
|
||||
|
||||
try:
|
||||
zip_path = catalog.download_pack(pack_id)
|
||||
manifest = manager.install_from_zip(zip_path, speckit_version, priority)
|
||||
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
|
||||
else:
|
||||
catalog = PresetCatalog(project_root)
|
||||
pack_info = catalog.get_pack_info(pack_id)
|
||||
|
||||
if not pack_info:
|
||||
console.print(f"[red]Error:[/red] Preset '{pack_id}' not found in catalog")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Bundled presets should have been caught above; if we reach
|
||||
# here the bundled files are missing from the installation.
|
||||
if pack_info.get("bundled") and not pack_info.get("download_url"):
|
||||
from .extensions import REINSTALL_COMMAND
|
||||
console.print(
|
||||
f"[red]Error:[/red] Preset '{pack_id}' is bundled with spec-kit "
|
||||
f"but could not be found in the installed package."
|
||||
)
|
||||
console.print(
|
||||
"\nThis usually means the spec-kit installation is incomplete or corrupted."
|
||||
)
|
||||
console.print("Try reinstalling spec-kit:")
|
||||
console.print(f" {REINSTALL_COMMAND}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not pack_info.get("_install_allowed", True):
|
||||
catalog_name = pack_info.get("_catalog_name", "unknown")
|
||||
console.print(f"[red]Error:[/red] Preset '{pack_id}' is from the '{catalog_name}' catalog which is discovery-only (install not allowed).")
|
||||
console.print("Add the catalog with --install-allowed or install from the preset's repository directly with --from.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
console.print(f"Installing preset [cyan]{pack_info.get('name', pack_id)}[/cyan]...")
|
||||
|
||||
try:
|
||||
zip_path = catalog.download_pack(pack_id)
|
||||
manifest = manager.install_from_zip(zip_path, speckit_version, priority)
|
||||
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
|
||||
finally:
|
||||
if 'zip_path' in locals() and zip_path.exists():
|
||||
zip_path.unlink(missing_ok=True)
|
||||
finally:
|
||||
if 'zip_path' in locals() and zip_path.exists():
|
||||
zip_path.unlink(missing_ok=True)
|
||||
else:
|
||||
console.print("[red]Error:[/red] Specify a preset ID, --from URL, or --dev path")
|
||||
raise typer.Exit(1)
|
||||
@@ -3071,7 +3001,7 @@ def extension_add(
|
||||
priority: int = typer.Option(10, "--priority", help="Resolution priority (lower = higher precedence, default 10)"),
|
||||
):
|
||||
"""Install an extension."""
|
||||
from .extensions import ExtensionManager, ExtensionCatalog, ExtensionError, ValidationError, CompatibilityError, REINSTALL_COMMAND
|
||||
from .extensions import ExtensionManager, ExtensionCatalog, ExtensionError, ValidationError, CompatibilityError
|
||||
|
||||
project_root = Path.cwd()
|
||||
|
||||
@@ -3173,19 +3103,6 @@ def extension_add(
|
||||
manifest = manager.install_from_directory(bundled_path, speckit_version, priority=priority)
|
||||
|
||||
if bundled_path is None:
|
||||
# Bundled extensions without a download URL must come from the local package
|
||||
if ext_info.get("bundled") and not ext_info.get("download_url"):
|
||||
console.print(
|
||||
f"[red]Error:[/red] Extension '{ext_info['id']}' is bundled with spec-kit "
|
||||
f"but could not be found in the installed package."
|
||||
)
|
||||
console.print(
|
||||
"\nThis usually means the spec-kit installation is incomplete or corrupted."
|
||||
)
|
||||
console.print("Try reinstalling spec-kit:")
|
||||
console.print(f" {REINSTALL_COMMAND}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Enforce install_allowed policy
|
||||
if not ext_info.get("_install_allowed", True):
|
||||
catalog_name = ext_info.get("_catalog_name", "community")
|
||||
|
||||
@@ -18,7 +18,6 @@ import yaml
|
||||
def _build_agent_configs() -> dict[str, Any]:
|
||||
"""Derive CommandRegistrar.AGENT_CONFIGS from INTEGRATION_REGISTRY."""
|
||||
from specify_cli.integrations import INTEGRATION_REGISTRY
|
||||
|
||||
configs: dict[str, dict[str, Any]] = {}
|
||||
for key, integration in INTEGRATION_REGISTRY.items():
|
||||
if key == "generic":
|
||||
@@ -76,7 +75,7 @@ class CommandRegistrar:
|
||||
return {}, content
|
||||
|
||||
frontmatter_str = content[3:end_marker].strip()
|
||||
body = content[end_marker + 3 :].strip()
|
||||
body = content[end_marker + 3:].strip()
|
||||
|
||||
try:
|
||||
frontmatter = yaml.safe_load(frontmatter_str) or {}
|
||||
@@ -101,9 +100,7 @@ class CommandRegistrar:
|
||||
if not fm:
|
||||
return ""
|
||||
|
||||
yaml_str = yaml.dump(
|
||||
fm, default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
)
|
||||
yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
return f"---\n{yaml_str}---\n"
|
||||
|
||||
def _adjust_script_paths(self, frontmatter: dict) -> dict:
|
||||
@@ -149,16 +146,16 @@ class CommandRegistrar:
|
||||
# ".specify/extensions/<ext>/scripts/..." remain intact.
|
||||
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?memory/', r"\1.specify/memory/", text)
|
||||
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?scripts/', r"\1.specify/scripts/", text)
|
||||
text = re.sub(
|
||||
r'(^|[\s`"\'(])(?:\.?/)?templates/', r"\1.specify/templates/", text
|
||||
)
|
||||
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?templates/', r"\1.specify/templates/", text)
|
||||
|
||||
return text.replace(".specify/.specify/", ".specify/").replace(
|
||||
".specify.specify/", ".specify/"
|
||||
)
|
||||
return text.replace(".specify/.specify/", ".specify/").replace(".specify.specify/", ".specify/")
|
||||
|
||||
def render_markdown_command(
|
||||
self, frontmatter: dict, body: str, source_id: str, context_note: str = None
|
||||
self,
|
||||
frontmatter: dict,
|
||||
body: str,
|
||||
source_id: str,
|
||||
context_note: str = None
|
||||
) -> str:
|
||||
"""Render command in Markdown format.
|
||||
|
||||
@@ -175,7 +172,12 @@ class CommandRegistrar:
|
||||
context_note = f"\n<!-- Source: {source_id} -->\n"
|
||||
return self.render_frontmatter(frontmatter) + "\n" + context_note + body
|
||||
|
||||
def render_toml_command(self, frontmatter: dict, body: str, source_id: str) -> str:
|
||||
def render_toml_command(
|
||||
self,
|
||||
frontmatter: dict,
|
||||
body: str,
|
||||
source_id: str
|
||||
) -> str:
|
||||
"""Render command in TOML format.
|
||||
|
||||
Args:
|
||||
@@ -190,7 +192,7 @@ class CommandRegistrar:
|
||||
|
||||
if "description" in frontmatter:
|
||||
toml_lines.append(
|
||||
f"description = {self._render_basic_toml_string(frontmatter['description'])}"
|
||||
f'description = {self._render_basic_toml_string(frontmatter["description"])}'
|
||||
)
|
||||
toml_lines.append("")
|
||||
|
||||
@@ -224,41 +226,6 @@ class CommandRegistrar:
|
||||
)
|
||||
return f'"{escaped}"'
|
||||
|
||||
def render_yaml_command(
|
||||
self,
|
||||
frontmatter: dict,
|
||||
body: str,
|
||||
source_id: str,
|
||||
cmd_name: str = "",
|
||||
) -> str:
|
||||
"""Render command in YAML recipe format for Goose.
|
||||
|
||||
Args:
|
||||
frontmatter: Command frontmatter
|
||||
body: Command body content
|
||||
source_id: Source identifier (extension or preset ID)
|
||||
cmd_name: Command name used as title fallback
|
||||
|
||||
Returns:
|
||||
Formatted YAML recipe file content
|
||||
"""
|
||||
from specify_cli.integrations.base import YamlIntegration
|
||||
|
||||
title = frontmatter.get("title", "") or frontmatter.get("name", "")
|
||||
if not isinstance(title, str):
|
||||
title = str(title) if title is not None else ""
|
||||
if not title and cmd_name:
|
||||
title = YamlIntegration._human_title(cmd_name)
|
||||
if not title and source_id:
|
||||
title = YamlIntegration._human_title(Path(str(source_id)).stem)
|
||||
if not title:
|
||||
title = "Command"
|
||||
|
||||
description = frontmatter.get("description", "")
|
||||
if not isinstance(description, str):
|
||||
description = str(description) if description is not None else ""
|
||||
return YamlIntegration._render_yaml(title, description, body, source_id)
|
||||
|
||||
def render_skill_command(
|
||||
self,
|
||||
agent_name: str,
|
||||
@@ -285,13 +252,9 @@ class CommandRegistrar:
|
||||
frontmatter = {}
|
||||
|
||||
if agent_name in {"codex", "kimi"}:
|
||||
body = self.resolve_skill_placeholders(
|
||||
agent_name, frontmatter, body, project_root
|
||||
)
|
||||
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
|
||||
|
||||
description = frontmatter.get(
|
||||
"description", f"Spec-kit workflow command: {skill_name}"
|
||||
)
|
||||
description = frontmatter.get("description", f"Spec-kit workflow command: {skill_name}")
|
||||
skill_frontmatter = self.build_skill_frontmatter(
|
||||
agent_name,
|
||||
skill_name,
|
||||
@@ -325,9 +288,7 @@ class CommandRegistrar:
|
||||
return skill_frontmatter
|
||||
|
||||
@staticmethod
|
||||
def resolve_skill_placeholders(
|
||||
agent_name: str, frontmatter: dict, body: str, project_root: Path
|
||||
) -> str:
|
||||
def resolve_skill_placeholders(agent_name: str, frontmatter: dict, body: str, project_root: Path) -> str:
|
||||
"""Resolve script placeholders for skills-backed agents."""
|
||||
try:
|
||||
from . import load_init_options
|
||||
@@ -351,9 +312,7 @@ class CommandRegistrar:
|
||||
script_variant = init_opts.get("script")
|
||||
if script_variant not in {"sh", "ps"}:
|
||||
fallback_order = []
|
||||
default_variant = (
|
||||
"ps" if platform.system().lower().startswith("win") else "sh"
|
||||
)
|
||||
default_variant = "ps" if platform.system().lower().startswith("win") else "sh"
|
||||
secondary_variant = "sh" if default_variant == "ps" else "ps"
|
||||
|
||||
if default_variant in scripts or default_variant in agent_scripts:
|
||||
@@ -375,9 +334,7 @@ class CommandRegistrar:
|
||||
script_command = script_command.replace("{ARGS}", "$ARGUMENTS")
|
||||
body = body.replace("{SCRIPT}", script_command)
|
||||
|
||||
agent_script_command = (
|
||||
agent_scripts.get(script_variant) if script_variant else None
|
||||
)
|
||||
agent_script_command = agent_scripts.get(script_variant) if script_variant else None
|
||||
if agent_script_command:
|
||||
agent_script_command = agent_script_command.replace("{ARGS}", "$ARGUMENTS")
|
||||
body = body.replace("{AGENT_SCRIPT}", agent_script_command)
|
||||
@@ -385,9 +342,7 @@ class CommandRegistrar:
|
||||
body = body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", agent_name)
|
||||
return CommandRegistrar.rewrite_project_relative_paths(body)
|
||||
|
||||
def _convert_argument_placeholder(
|
||||
self, content: str, from_placeholder: str, to_placeholder: str
|
||||
) -> str:
|
||||
def _convert_argument_placeholder(self, content: str, from_placeholder: str, to_placeholder: str) -> str:
|
||||
"""Convert argument placeholder format.
|
||||
|
||||
Args:
|
||||
@@ -401,16 +356,14 @@ class CommandRegistrar:
|
||||
return content.replace(from_placeholder, to_placeholder)
|
||||
|
||||
@staticmethod
|
||||
def _compute_output_name(
|
||||
agent_name: str, cmd_name: str, agent_config: Dict[str, Any]
|
||||
) -> str:
|
||||
def _compute_output_name(agent_name: str, cmd_name: str, agent_config: Dict[str, Any]) -> str:
|
||||
"""Compute the on-disk command or skill name for an agent."""
|
||||
if agent_config["extension"] != "/SKILL.md":
|
||||
return cmd_name
|
||||
|
||||
short_name = cmd_name
|
||||
if short_name.startswith("speckit."):
|
||||
short_name = short_name[len("speckit.") :]
|
||||
short_name = short_name[len("speckit."):]
|
||||
short_name = short_name.replace(".", "-")
|
||||
|
||||
return f"speckit-{short_name}"
|
||||
@@ -422,7 +375,7 @@ class CommandRegistrar:
|
||||
source_id: str,
|
||||
source_dir: Path,
|
||||
project_root: Path,
|
||||
context_note: str = None,
|
||||
context_note: str = None
|
||||
) -> List[str]:
|
||||
"""Register commands for a specific agent.
|
||||
|
||||
@@ -479,24 +432,12 @@ class CommandRegistrar:
|
||||
|
||||
if agent_config["extension"] == "/SKILL.md":
|
||||
output = self.render_skill_command(
|
||||
agent_name,
|
||||
output_name,
|
||||
frontmatter,
|
||||
body,
|
||||
source_id,
|
||||
cmd_file,
|
||||
project_root,
|
||||
agent_name, output_name, frontmatter, body, source_id, cmd_file, project_root
|
||||
)
|
||||
elif agent_config["format"] == "markdown":
|
||||
output = self.render_markdown_command(
|
||||
frontmatter, body, source_id, context_note
|
||||
)
|
||||
output = self.render_markdown_command(frontmatter, body, source_id, context_note)
|
||||
elif agent_config["format"] == "toml":
|
||||
output = self.render_toml_command(frontmatter, body, source_id)
|
||||
elif agent_config["format"] == "yaml":
|
||||
output = self.render_yaml_command(
|
||||
frontmatter, body, source_id, cmd_name
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported format: {agent_config['format']}")
|
||||
|
||||
@@ -510,68 +451,34 @@ class CommandRegistrar:
|
||||
registered.append(cmd_name)
|
||||
|
||||
for alias in cmd_info.get("aliases", []):
|
||||
alias_output_name = self._compute_output_name(
|
||||
agent_name, alias, agent_config
|
||||
)
|
||||
alias_output_name = self._compute_output_name(agent_name, alias, agent_config)
|
||||
|
||||
# For agents with inject_name, render with alias-specific frontmatter
|
||||
if agent_config.get("inject_name"):
|
||||
alias_frontmatter = deepcopy(frontmatter)
|
||||
# Use custom name formatter if provided (e.g., Forge's hyphenated format)
|
||||
format_name = agent_config.get("format_name")
|
||||
alias_frontmatter["name"] = (
|
||||
format_name(alias) if format_name else alias
|
||||
)
|
||||
alias_frontmatter["name"] = format_name(alias) if format_name else 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,
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
elif agent_config["format"] == "yaml":
|
||||
alias_output = self.render_yaml_command(
|
||||
alias_frontmatter, body, source_id, alias
|
||||
)
|
||||
alias_output = self.render_toml_command(alias_frontmatter, body, source_id)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported format: {agent_config['format']}"
|
||||
)
|
||||
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,
|
||||
agent_name, alias_output_name, frontmatter, body, source_id, cmd_file, project_root
|
||||
)
|
||||
|
||||
alias_file = (
|
||||
commands_dir / f"{alias_output_name}{agent_config['extension']}"
|
||||
)
|
||||
try:
|
||||
alias_file.resolve().relative_to(commands_dir.resolve())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Alias output path escapes commands directory: {alias_file!r}"
|
||||
)
|
||||
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")
|
||||
if agent_name == "copilot":
|
||||
@@ -599,7 +506,7 @@ class CommandRegistrar:
|
||||
source_id: str,
|
||||
source_dir: Path,
|
||||
project_root: Path,
|
||||
context_note: str = None,
|
||||
context_note: str = None
|
||||
) -> Dict[str, List[str]]:
|
||||
"""Register commands for all detected agents in the project.
|
||||
|
||||
@@ -622,12 +529,8 @@ class CommandRegistrar:
|
||||
if agent_dir.exists():
|
||||
try:
|
||||
registered = self.register_commands(
|
||||
agent_name,
|
||||
commands,
|
||||
source_id,
|
||||
source_dir,
|
||||
project_root,
|
||||
context_note=context_note,
|
||||
agent_name, commands, source_id, source_dir, project_root,
|
||||
context_note=context_note
|
||||
)
|
||||
if registered:
|
||||
results[agent_name] = registered
|
||||
@@ -637,7 +540,9 @@ class CommandRegistrar:
|
||||
return results
|
||||
|
||||
def unregister_commands(
|
||||
self, registered_commands: Dict[str, List[str]], project_root: Path
|
||||
self,
|
||||
registered_commands: Dict[str, List[str]],
|
||||
project_root: Path
|
||||
) -> None:
|
||||
"""Remove previously registered command files from agent directories.
|
||||
|
||||
@@ -654,17 +559,13 @@ class CommandRegistrar:
|
||||
commands_dir = project_root / agent_config["dir"]
|
||||
|
||||
for cmd_name in cmd_names:
|
||||
output_name = self._compute_output_name(
|
||||
agent_name, cmd_name, agent_config
|
||||
)
|
||||
output_name = self._compute_output_name(agent_name, cmd_name, agent_config)
|
||||
cmd_file = commands_dir / f"{output_name}{agent_config['extension']}"
|
||||
if cmd_file.exists():
|
||||
cmd_file.unlink()
|
||||
|
||||
if agent_name == "copilot":
|
||||
prompt_file = (
|
||||
project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
|
||||
)
|
||||
prompt_file = project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
|
||||
if prompt_file.exists():
|
||||
prompt_file.unlink()
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ _FALLBACK_CORE_COMMAND_NAMES = frozenset({
|
||||
})
|
||||
EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$")
|
||||
|
||||
REINSTALL_COMMAND = "uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git"
|
||||
|
||||
|
||||
def _load_core_command_names() -> frozenset[str]:
|
||||
"""Discover bundled core command names from the packaged templates.
|
||||
@@ -1872,14 +1870,6 @@ class ExtensionCatalog:
|
||||
if not ext_info:
|
||||
raise ExtensionError(f"Extension '{extension_id}' not found in catalog")
|
||||
|
||||
# Bundled extensions without a download URL must be installed locally
|
||||
if ext_info.get("bundled") and not ext_info.get("download_url"):
|
||||
raise ExtensionError(
|
||||
f"Extension '{extension_id}' is bundled with spec-kit and has no download URL. "
|
||||
f"It should be installed from the local package. "
|
||||
f"Try reinstalling: {REINSTALL_COMMAND}"
|
||||
)
|
||||
|
||||
download_url = ext_info.get("download_url")
|
||||
if not download_url:
|
||||
raise ExtensionError(f"Extension '{extension_id}' has no download URL")
|
||||
@@ -2180,7 +2170,6 @@ class HookExecutor:
|
||||
codex_skill_mode = selected_ai == "codex" and bool(init_options.get("ai_skills"))
|
||||
claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills"))
|
||||
kimi_skill_mode = selected_ai == "kimi"
|
||||
cursor_skill_mode = selected_ai == "cursor-agent" and bool(init_options.get("ai_skills"))
|
||||
|
||||
skill_name = self._skill_name_from_command(command_id)
|
||||
if codex_skill_mode and skill_name:
|
||||
@@ -2189,8 +2178,6 @@ class HookExecutor:
|
||||
return f"/{skill_name}"
|
||||
if kimi_skill_mode and skill_name:
|
||||
return f"/skill:{skill_name}"
|
||||
if cursor_skill_mode and skill_name:
|
||||
return f"/{skill_name}"
|
||||
|
||||
return f"/{command_id}"
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ def get_integration(key: str) -> IntegrationBase | None:
|
||||
|
||||
# -- Register built-in integrations --------------------------------------
|
||||
|
||||
|
||||
def _register_builtins() -> None:
|
||||
"""Register all built-in integrations.
|
||||
|
||||
@@ -52,14 +51,13 @@ def _register_builtins() -> None:
|
||||
from .auggie import AuggieIntegration
|
||||
from .bob import BobIntegration
|
||||
from .claude import ClaudeIntegration
|
||||
from .codebuddy import CodebuddyIntegration
|
||||
from .codex import CodexIntegration
|
||||
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 .goose import GooseIntegration
|
||||
from .iflow import IflowIntegration
|
||||
from .junie import JunieIntegration
|
||||
from .kilocode import KilocodeIntegration
|
||||
@@ -82,14 +80,13 @@ def _register_builtins() -> None:
|
||||
_register(AuggieIntegration())
|
||||
_register(BobIntegration())
|
||||
_register(ClaudeIntegration())
|
||||
_register(CodebuddyIntegration())
|
||||
_register(CodexIntegration())
|
||||
_register(CodebuddyIntegration())
|
||||
_register(CopilotIntegration())
|
||||
_register(CursorAgentIntegration())
|
||||
_register(ForgeIntegration())
|
||||
_register(GeminiIntegration())
|
||||
_register(GenericIntegration())
|
||||
_register(GooseIntegration())
|
||||
_register(IflowIntegration())
|
||||
_register(JunieIntegration())
|
||||
_register(KilocodeIntegration())
|
||||
|
||||
@@ -28,7 +28,6 @@ if TYPE_CHECKING:
|
||||
# IntegrationOption
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IntegrationOption:
|
||||
"""Declares an option that an integration accepts via ``--integration-options``.
|
||||
@@ -52,7 +51,6 @@ class IntegrationOption:
|
||||
# IntegrationBase — abstract base class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class IntegrationBase(ABC):
|
||||
"""Abstract base class every integration must implement.
|
||||
|
||||
@@ -277,7 +275,7 @@ class IntegrationBase(ABC):
|
||||
2. Replace ``{SCRIPT}`` with the extracted script command
|
||||
3. Extract ``agent_scripts.<script_type>`` and replace ``{AGENT_SCRIPT}``
|
||||
4. Strip ``scripts:`` and ``agent_scripts:`` sections from frontmatter
|
||||
5. Replace ``{ARGS}`` and ``$ARGUMENTS`` with *arg_placeholder*
|
||||
5. Replace ``{ARGS}`` with *arg_placeholder*
|
||||
6. Replace ``__AGENT__`` with *agent_name*
|
||||
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
|
||||
"""
|
||||
@@ -350,9 +348,8 @@ class IntegrationBase(ABC):
|
||||
output_lines.append(line)
|
||||
content = "".join(output_lines)
|
||||
|
||||
# 5. Replace {ARGS} and $ARGUMENTS
|
||||
# 5. Replace {ARGS}
|
||||
content = content.replace("{ARGS}", arg_placeholder)
|
||||
content = content.replace("$ARGUMENTS", arg_placeholder)
|
||||
|
||||
# 6. Replace __AGENT__
|
||||
content = content.replace("__AGENT__", agent_name)
|
||||
@@ -361,7 +358,6 @@ class IntegrationBase(ABC):
|
||||
# CommandRegistrar so extension-local paths are preserved and
|
||||
# boundary rules stay consistent across the codebase.
|
||||
from specify_cli.agents import CommandRegistrar
|
||||
|
||||
content = CommandRegistrar.rewrite_project_relative_paths(content)
|
||||
|
||||
return content
|
||||
@@ -437,7 +433,9 @@ class IntegrationBase(ABC):
|
||||
**opts: Any,
|
||||
) -> list[Path]:
|
||||
"""High-level install — calls ``setup()`` and returns created files."""
|
||||
return self.setup(project_root, manifest, parsed_options=parsed_options, **opts)
|
||||
return self.setup(
|
||||
project_root, manifest, parsed_options=parsed_options, **opts
|
||||
)
|
||||
|
||||
def uninstall(
|
||||
self,
|
||||
@@ -454,7 +452,6 @@ class IntegrationBase(ABC):
|
||||
# MarkdownIntegration — covers ~20 standard agents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MarkdownIntegration(IntegrationBase):
|
||||
"""Concrete base for integrations that use standard Markdown commands.
|
||||
|
||||
@@ -495,18 +492,12 @@ class MarkdownIntegration(IntegrationBase):
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
script_type = opts.get("script_type", "sh")
|
||||
arg_placeholder = (
|
||||
self.registrar_config.get("args", "$ARGUMENTS")
|
||||
if self.registrar_config
|
||||
else "$ARGUMENTS"
|
||||
)
|
||||
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS") if self.registrar_config else "$ARGUMENTS"
|
||||
created: list[Path] = []
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder
|
||||
)
|
||||
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
||||
dst_name = self.command_filename(src_file.stem)
|
||||
dst_file = self.write_file_and_record(
|
||||
processed, dest / dst_name, project_root, manifest
|
||||
@@ -521,7 +512,6 @@ class MarkdownIntegration(IntegrationBase):
|
||||
# TomlIntegration — TOML-format agents (Gemini, Tabnine)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TomlIntegration(IntegrationBase):
|
||||
"""Concrete base for integrations that use TOML command format.
|
||||
|
||||
@@ -613,17 +603,13 @@ class TomlIntegration(IntegrationBase):
|
||||
if "'''" not in value and not value.endswith("'"):
|
||||
return "'''\n" + value + "'''"
|
||||
|
||||
return (
|
||||
'"'
|
||||
+ (
|
||||
value.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
)
|
||||
+ '"'
|
||||
)
|
||||
return '"' + (
|
||||
value.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
) + '"'
|
||||
|
||||
@staticmethod
|
||||
def _render_toml(description: str, body: str) -> str:
|
||||
@@ -642,9 +628,7 @@ class TomlIntegration(IntegrationBase):
|
||||
toml_lines: list[str] = []
|
||||
|
||||
if description:
|
||||
toml_lines.append(
|
||||
f"description = {TomlIntegration._render_toml_string(description)}"
|
||||
)
|
||||
toml_lines.append(f"description = {TomlIntegration._render_toml_string(description)}")
|
||||
toml_lines.append("")
|
||||
|
||||
body = body.rstrip("\n")
|
||||
@@ -681,19 +665,13 @@ class TomlIntegration(IntegrationBase):
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
script_type = opts.get("script_type", "sh")
|
||||
arg_placeholder = (
|
||||
self.registrar_config.get("args", "{{args}}")
|
||||
if self.registrar_config
|
||||
else "{{args}}"
|
||||
)
|
||||
arg_placeholder = self.registrar_config.get("args", "{{args}}") if self.registrar_config else "{{args}}"
|
||||
created: list[Path] = []
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
description = self._extract_description(raw)
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder
|
||||
)
|
||||
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
||||
_, body = self._split_frontmatter(processed)
|
||||
toml_content = self._render_toml(description, body)
|
||||
dst_name = self.command_filename(src_file.stem)
|
||||
@@ -706,188 +684,6 @@ class TomlIntegration(IntegrationBase):
|
||||
return created
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# YamlIntegration — YAML-format agents (Goose)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class YamlIntegration(IntegrationBase):
|
||||
"""Concrete base for integrations that use YAML recipe format.
|
||||
|
||||
Mirrors ``TomlIntegration`` closely: subclasses only need to set
|
||||
``key``, ``config``, ``registrar_config`` (and optionally
|
||||
``context_file``). Everything else is inherited.
|
||||
|
||||
``setup()`` processes command templates through the same placeholder
|
||||
pipeline as ``MarkdownIntegration``, then converts the result to
|
||||
YAML recipe format (version, title, description, prompt block scalar).
|
||||
"""
|
||||
|
||||
def command_filename(self, template_name: str) -> str:
|
||||
"""YAML commands use ``.yaml`` extension."""
|
||||
return f"speckit.{template_name}.yaml"
|
||||
|
||||
@staticmethod
|
||||
def _extract_frontmatter(content: str) -> dict[str, Any]:
|
||||
"""Extract frontmatter as a dict from YAML frontmatter block."""
|
||||
import yaml
|
||||
|
||||
if not content.startswith("---"):
|
||||
return {}
|
||||
|
||||
lines = content.splitlines(keepends=True)
|
||||
if not lines or lines[0].rstrip("\r\n") != "---":
|
||||
return {}
|
||||
|
||||
frontmatter_end = -1
|
||||
for i, line in enumerate(lines[1:], start=1):
|
||||
if line.rstrip("\r\n") == "---":
|
||||
frontmatter_end = i
|
||||
break
|
||||
|
||||
if frontmatter_end == -1:
|
||||
return {}
|
||||
|
||||
frontmatter_text = "".join(lines[1:frontmatter_end])
|
||||
try:
|
||||
fm = yaml.safe_load(frontmatter_text) or {}
|
||||
except yaml.YAMLError:
|
||||
return {}
|
||||
|
||||
return fm if isinstance(fm, dict) else {}
|
||||
|
||||
@staticmethod
|
||||
def _split_frontmatter(content: str) -> tuple[str, str]:
|
||||
"""Split YAML frontmatter from the remaining body content."""
|
||||
if not content.startswith("---"):
|
||||
return "", content
|
||||
|
||||
lines = content.splitlines(keepends=True)
|
||||
if not lines or lines[0].rstrip("\r\n") != "---":
|
||||
return "", content
|
||||
|
||||
frontmatter_end = -1
|
||||
for i, line in enumerate(lines[1:], start=1):
|
||||
if line.rstrip("\r\n") == "---":
|
||||
frontmatter_end = i
|
||||
break
|
||||
|
||||
if frontmatter_end == -1:
|
||||
return "", content
|
||||
|
||||
frontmatter = "".join(lines[1:frontmatter_end])
|
||||
body = "".join(lines[frontmatter_end + 1 :])
|
||||
return frontmatter, body
|
||||
|
||||
@staticmethod
|
||||
def _human_title(identifier: str) -> str:
|
||||
"""Convert an identifier to a human-readable title.
|
||||
|
||||
Strips a leading ``speckit.`` prefix and replaces ``.``, ``-``,
|
||||
and ``_`` with spaces before title-casing.
|
||||
"""
|
||||
text = identifier
|
||||
if text.startswith("speckit."):
|
||||
text = text[len("speckit.") :]
|
||||
return text.replace(".", " ").replace("-", " ").replace("_", " ").title()
|
||||
|
||||
@staticmethod
|
||||
def _render_yaml(title: str, description: str, body: str, source_id: str) -> str:
|
||||
"""Render a YAML recipe file from title, description, and body.
|
||||
|
||||
Produces a Goose-compatible recipe with a literal block scalar
|
||||
for the prompt content. Uses ``yaml.safe_dump()`` for the
|
||||
header fields to ensure proper escaping.
|
||||
"""
|
||||
import yaml
|
||||
|
||||
header = {
|
||||
"version": "1.0.0",
|
||||
"title": title,
|
||||
"description": description,
|
||||
"author": {"contact": "spec-kit"},
|
||||
"extensions": [{"type": "builtin", "name": "developer"}],
|
||||
"activities": ["Spec-Driven Development"],
|
||||
}
|
||||
|
||||
header_yaml = yaml.safe_dump(
|
||||
header,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
default_flow_style=False,
|
||||
).strip()
|
||||
|
||||
# Indent each line for YAML block scalar
|
||||
indented = "\n".join(f" {line}" for line in body.split("\n"))
|
||||
|
||||
lines = [header_yaml, "prompt: |", indented, "", f"# Source: {source_id}"]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
def setup(
|
||||
self,
|
||||
project_root: Path,
|
||||
manifest: IntegrationManifest,
|
||||
parsed_options: dict[str, Any] | None = None,
|
||||
**opts: Any,
|
||||
) -> list[Path]:
|
||||
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", "{{args}}")
|
||||
if self.registrar_config
|
||||
else "{{args}}"
|
||||
)
|
||||
created: list[Path] = []
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
fm = self._extract_frontmatter(raw)
|
||||
description = fm.get("description", "")
|
||||
if not isinstance(description, str):
|
||||
description = str(description) if description is not None else ""
|
||||
title = fm.get("title", "") or fm.get("name", "")
|
||||
if not isinstance(title, str):
|
||||
title = str(title) if title is not None else ""
|
||||
if not title:
|
||||
title = self._human_title(src_file.stem)
|
||||
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder
|
||||
)
|
||||
_, body = self._split_frontmatter(processed)
|
||||
yaml_content = self._render_yaml(
|
||||
title, description, body, f"templates/commands/{src_file.name}"
|
||||
)
|
||||
dst_name = self.command_filename(src_file.stem)
|
||||
dst_file = self.write_file_and_record(
|
||||
yaml_content, dest / dst_name, project_root, manifest
|
||||
)
|
||||
created.append(dst_file)
|
||||
|
||||
created.extend(self.install_scripts(project_root, manifest))
|
||||
return created
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SkillsIntegration — skills-format agents (Codex, Kimi, Agy)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -917,7 +713,9 @@ class SkillsIntegration(IntegrationBase):
|
||||
Raises ``ValueError`` when ``config`` or ``folder`` is missing.
|
||||
"""
|
||||
if not self.config:
|
||||
raise ValueError(f"{type(self).__name__}.config is not set.")
|
||||
raise ValueError(
|
||||
f"{type(self).__name__}.config is not set."
|
||||
)
|
||||
folder = self.config.get("folder")
|
||||
if not folder:
|
||||
raise ValueError(
|
||||
|
||||
@@ -1,39 +1,21 @@
|
||||
"""Cursor IDE integration.
|
||||
"""Cursor IDE integration."""
|
||||
|
||||
Cursor Agent uses the ``.cursor/skills/speckit-<name>/SKILL.md`` layout.
|
||||
Commands are deprecated; ``--skills`` defaults to ``True``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..base import IntegrationOption, SkillsIntegration
|
||||
from ..base import MarkdownIntegration
|
||||
|
||||
|
||||
class CursorAgentIntegration(SkillsIntegration):
|
||||
class CursorAgentIntegration(MarkdownIntegration):
|
||||
key = "cursor-agent"
|
||||
config = {
|
||||
"name": "Cursor",
|
||||
"folder": ".cursor/",
|
||||
"commands_subdir": "skills",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": None,
|
||||
"requires_cli": False,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".cursor/skills",
|
||||
"dir": ".cursor/commands",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
"extension": ".md",
|
||||
}
|
||||
|
||||
context_file = ".cursor/rules/specify-rules.mdc"
|
||||
|
||||
@classmethod
|
||||
def options(cls) -> list[IntegrationOption]:
|
||||
return [
|
||||
IntegrationOption(
|
||||
"--skills",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
help="Install as agent skills (recommended for Cursor)",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Goose integration — Block's open source AI agent."""
|
||||
|
||||
from ..base import YamlIntegration
|
||||
|
||||
|
||||
class GooseIntegration(YamlIntegration):
|
||||
key = "goose"
|
||||
config = {
|
||||
"name": "Goose",
|
||||
"folder": ".goose/",
|
||||
"commands_subdir": "recipes",
|
||||
"install_url": "https://block.github.io/goose/docs/getting-started/installation",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".goose/recipes",
|
||||
"format": "yaml",
|
||||
"args": "{{args}}",
|
||||
"extension": ".yaml",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
@@ -1,33 +0,0 @@
|
||||
# update-context.ps1 — Goose 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 "Goose integration requires support in scripts/powershell/update-agent-context.ps1."
|
||||
exit 1
|
||||
}
|
||||
|
||||
& $sharedScript -AgentType goose
|
||||
exit $LASTEXITCODE
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# update-context.sh — Goose 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 "Goose integration requires support in scripts/bash/update-agent-context.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$shared_script" goose
|
||||
@@ -1587,16 +1587,6 @@ class PresetCatalog:
|
||||
f"Preset '{pack_id}' not found in catalog"
|
||||
)
|
||||
|
||||
# Bundled presets without a download URL must be installed locally
|
||||
if pack_info.get("bundled") and not pack_info.get("download_url"):
|
||||
from .extensions import REINSTALL_COMMAND
|
||||
raise PresetError(
|
||||
f"Preset '{pack_id}' is bundled with spec-kit and has no download URL. "
|
||||
f"It should be installed from the local package. "
|
||||
f"Use 'specify preset add {pack_id}' to install from the bundled package, "
|
||||
f"or reinstall spec-kit if the bundled files are missing: {REINSTALL_COMMAND}"
|
||||
)
|
||||
|
||||
if not pack_info.get("_install_allowed", True):
|
||||
catalog_name = pack_info.get("_catalog_name", "unknown")
|
||||
raise PresetError(
|
||||
|
||||
@@ -84,9 +84,7 @@ class TomlIntegrationTests:
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
expected_dir = i.commands_dest(tmp_path)
|
||||
assert expected_dir.exists(), (
|
||||
f"Expected directory {expected_dir} was not created"
|
||||
)
|
||||
assert expected_dir.exists(), f"Expected directory {expected_dir} was not created"
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
assert len(cmd_files) > 0, "No command files were created"
|
||||
for f in cmd_files:
|
||||
@@ -136,12 +134,6 @@ class TomlIntegrationTests:
|
||||
# At least one file should contain {{args}} from the {ARGS} placeholder
|
||||
has_args = any("{{args}}" in f.read_text(encoding="utf-8") for f in cmd_files)
|
||||
assert has_args, "No TOML command file contains {{args}} placeholder"
|
||||
has_dollar_args = any(
|
||||
"$ARGUMENTS" in f.read_text(encoding="utf-8") for f in cmd_files
|
||||
)
|
||||
assert not has_dollar_args, (
|
||||
"TOML command still contains $ARGUMENTS instead of {{args}}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("frontmatter", "expected"),
|
||||
@@ -164,13 +156,19 @@ class TomlIntegrationTests:
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_toml_extract_description_supports_block_scalars(
|
||||
self, frontmatter, expected
|
||||
):
|
||||
def test_toml_extract_description_supports_block_scalars(self, frontmatter, expected):
|
||||
assert TomlIntegration._extract_description(frontmatter) == expected
|
||||
|
||||
def test_split_frontmatter_ignores_indented_delimiters(self):
|
||||
content = "---\ndescription: |\n line one\n ---\n line two\n---\nBody\n"
|
||||
content = (
|
||||
"---\n"
|
||||
"description: |\n"
|
||||
" line one\n"
|
||||
" ---\n"
|
||||
" line two\n"
|
||||
"---\n"
|
||||
"Body\n"
|
||||
)
|
||||
|
||||
frontmatter, body = TomlIntegration._split_frontmatter(content)
|
||||
|
||||
@@ -207,7 +205,7 @@ class TomlIntegrationTests:
|
||||
assert "---" not in parsed["prompt"]
|
||||
|
||||
def test_toml_no_ambiguous_closing_quotes(self, tmp_path, monkeypatch):
|
||||
"""Multiline body ending with a double quote must not produce an ambiguous TOML multiline-string closing delimiter (#2113)."""
|
||||
"""Multiline body ending with `"` must not produce `""""` (#2113)."""
|
||||
i = get_integration(self.KEY)
|
||||
template = tmp_path / "sample.md"
|
||||
template.write_text(
|
||||
@@ -232,9 +230,7 @@ class TomlIntegrationTests:
|
||||
assert '"""\n' in raw, "body must use multiline basic string"
|
||||
parsed = tomllib.loads(raw)
|
||||
assert parsed["prompt"].endswith('specified?"')
|
||||
assert not parsed["prompt"].endswith("\n"), (
|
||||
"parsed value must not gain a trailing newline"
|
||||
)
|
||||
assert not parsed["prompt"].endswith("\n"), "parsed value must not gain a trailing newline"
|
||||
|
||||
def test_toml_triple_double_and_single_quote_ending(self, tmp_path, monkeypatch):
|
||||
"""Body containing `\"\"\"` and ending with `'` falls back to escaped basic string."""
|
||||
@@ -258,15 +254,11 @@ class TomlIntegrationTests:
|
||||
assert len(cmd_files) == 1
|
||||
|
||||
raw = cmd_files[0].read_text(encoding="utf-8")
|
||||
assert "''''" not in raw, (
|
||||
"literal string must not produce ambiguous closing quotes"
|
||||
)
|
||||
assert "''''" not in raw, "literal string must not produce ambiguous closing quotes"
|
||||
parsed = tomllib.loads(raw)
|
||||
assert parsed["prompt"].endswith("'single'")
|
||||
assert '"""triple"""' in parsed["prompt"]
|
||||
assert not parsed["prompt"].endswith("\n"), (
|
||||
"parsed value must not gain a trailing newline"
|
||||
)
|
||||
assert not parsed["prompt"].endswith("\n"), "parsed value must not gain a trailing newline"
|
||||
|
||||
def test_toml_closing_delimiter_inline_when_safe(self, tmp_path, monkeypatch):
|
||||
"""Body NOT ending with `"` keeps closing `\"\"\"` inline (no extra newline)."""
|
||||
@@ -292,9 +284,8 @@ class TomlIntegrationTests:
|
||||
raw = cmd_files[0].read_text(encoding="utf-8")
|
||||
parsed = tomllib.loads(raw)
|
||||
assert parsed["prompt"] == "Line one\nPlain body content"
|
||||
assert raw.rstrip().endswith('content"""'), (
|
||||
assert raw.rstrip().endswith('content"""'), \
|
||||
"closing delimiter should be inline when body does not end with a quote"
|
||||
)
|
||||
|
||||
def test_toml_is_valid(self, tmp_path):
|
||||
"""Every generated TOML file must parse without errors."""
|
||||
@@ -363,14 +354,7 @@ class TomlIntegrationTests:
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
i.setup(tmp_path, m)
|
||||
sh = (
|
||||
tmp_path
|
||||
/ ".specify"
|
||||
/ "integrations"
|
||||
/ self.KEY
|
||||
/ "scripts"
|
||||
/ "update-context.sh"
|
||||
)
|
||||
sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh"
|
||||
assert os.access(sh, os.X_OK)
|
||||
|
||||
# -- CLI auto-promote -------------------------------------------------
|
||||
@@ -385,20 +369,10 @@ class TomlIntegrationTests:
|
||||
try:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--ai",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"sh",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", self.KEY, "--script", "sh", "--no-git",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}"
|
||||
@@ -416,25 +390,13 @@ class TomlIntegrationTests:
|
||||
try:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--integration",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"sh",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--integration", self.KEY, "--script", "sh", "--no-git",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, (
|
||||
f"init --integration {self.KEY} failed: {result.output}"
|
||||
)
|
||||
assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}"
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.commands_dest(project)
|
||||
assert cmd_dir.is_dir(), f"Commands directory {cmd_dir} not created"
|
||||
@@ -444,15 +406,8 @@ class TomlIntegrationTests:
|
||||
# -- Complete file inventory ------------------------------------------
|
||||
|
||||
COMMAND_STEMS = [
|
||||
"analyze",
|
||||
"checklist",
|
||||
"clarify",
|
||||
"constitution",
|
||||
"implement",
|
||||
"plan",
|
||||
"specify",
|
||||
"tasks",
|
||||
"taskstoissues",
|
||||
"analyze", "checklist", "clarify", "constitution",
|
||||
"implement", "plan", "specify", "tasks", "taskstoissues",
|
||||
]
|
||||
|
||||
def _expected_files(self, script_variant: str) -> list[str]:
|
||||
@@ -470,38 +425,23 @@ class TomlIntegrationTests:
|
||||
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh")
|
||||
|
||||
# Framework files
|
||||
files.append(".specify/integration.json")
|
||||
files.append(".specify/init-options.json")
|
||||
files.append(f".specify/integration.json")
|
||||
files.append(f".specify/init-options.json")
|
||||
files.append(f".specify/integrations/{self.KEY}.manifest.json")
|
||||
files.append(".specify/integrations/speckit.manifest.json")
|
||||
files.append(f".specify/integrations/speckit.manifest.json")
|
||||
|
||||
if script_variant == "sh":
|
||||
for name in [
|
||||
"check-prerequisites.sh",
|
||||
"common.sh",
|
||||
"create-new-feature.sh",
|
||||
"setup-plan.sh",
|
||||
"update-agent-context.sh",
|
||||
]:
|
||||
for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.sh",
|
||||
"setup-plan.sh", "update-agent-context.sh"]:
|
||||
files.append(f".specify/scripts/bash/{name}")
|
||||
else:
|
||||
for name in [
|
||||
"check-prerequisites.ps1",
|
||||
"common.ps1",
|
||||
"create-new-feature.ps1",
|
||||
"setup-plan.ps1",
|
||||
"update-agent-context.ps1",
|
||||
]:
|
||||
for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1",
|
||||
"setup-plan.ps1", "update-agent-context.ps1"]:
|
||||
files.append(f".specify/scripts/powershell/{name}")
|
||||
|
||||
for name in [
|
||||
"agent-file-template.md",
|
||||
"checklist-template.md",
|
||||
"constitution-template.md",
|
||||
"plan-template.md",
|
||||
"spec-template.md",
|
||||
"tasks-template.md",
|
||||
]:
|
||||
for name in ["agent-file-template.md", "checklist-template.md",
|
||||
"constitution-template.md", "plan-template.md",
|
||||
"spec-template.md", "tasks-template.md"]:
|
||||
files.append(f".specify/templates/{name}")
|
||||
|
||||
files.append(".specify/memory/constitution.md")
|
||||
@@ -517,26 +457,15 @@ class TomlIntegrationTests:
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--integration",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"sh",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", self.KEY, "--script", "sh",
|
||||
"--no-git", "--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
actual = sorted(
|
||||
p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()
|
||||
)
|
||||
actual = sorted(p.relative_to(project).as_posix()
|
||||
for p in project.rglob("*") if p.is_file())
|
||||
expected = self._expected_files("sh")
|
||||
assert actual == expected, (
|
||||
f"Missing: {sorted(set(expected) - set(actual))}\n"
|
||||
@@ -553,26 +482,15 @@ class TomlIntegrationTests:
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--integration",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"ps",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", self.KEY, "--script", "ps",
|
||||
"--no-git", "--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
actual = sorted(
|
||||
p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()
|
||||
)
|
||||
actual = sorted(p.relative_to(project).as_posix()
|
||||
for p in project.rglob("*") if p.is_file())
|
||||
expected = self._expected_files("ps")
|
||||
assert actual == expected, (
|
||||
f"Missing: {sorted(set(expected) - set(actual))}\n"
|
||||
|
||||
@@ -1,459 +0,0 @@
|
||||
"""Reusable test mixin for standard YamlIntegration subclasses.
|
||||
|
||||
Each per-agent test file sets ``KEY``, ``FOLDER``, ``COMMANDS_SUBDIR``,
|
||||
``REGISTRAR_DIR``, and ``CONTEXT_FILE``, then inherits all verification
|
||||
logic from ``YamlIntegrationTests``.
|
||||
|
||||
Mirrors ``TomlIntegrationTests`` closely — same test structure,
|
||||
adapted for YAML recipe output format.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import yaml
|
||||
|
||||
from specify_cli.integrations import INTEGRATION_REGISTRY, get_integration
|
||||
from specify_cli.integrations.base import YamlIntegration
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
|
||||
class YamlIntegrationTests:
|
||||
"""Mixin — set class-level constants and inherit these tests.
|
||||
|
||||
Required class attrs on subclass::
|
||||
|
||||
KEY: str — integration registry key
|
||||
FOLDER: str — e.g. ".goose/"
|
||||
COMMANDS_SUBDIR: str — e.g. "recipes"
|
||||
REGISTRAR_DIR: str — e.g. ".goose/recipes"
|
||||
CONTEXT_FILE: str — e.g. "AGENTS.md"
|
||||
"""
|
||||
|
||||
KEY: str
|
||||
FOLDER: str
|
||||
COMMANDS_SUBDIR: str
|
||||
REGISTRAR_DIR: str
|
||||
CONTEXT_FILE: str
|
||||
|
||||
# -- Registration -----------------------------------------------------
|
||||
|
||||
def test_registered(self):
|
||||
assert self.KEY in INTEGRATION_REGISTRY
|
||||
assert get_integration(self.KEY) is not None
|
||||
|
||||
def test_is_yaml_integration(self):
|
||||
assert isinstance(get_integration(self.KEY), YamlIntegration)
|
||||
|
||||
# -- Config -----------------------------------------------------------
|
||||
|
||||
def test_config_folder(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.config["folder"] == self.FOLDER
|
||||
|
||||
def test_config_commands_subdir(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.config["commands_subdir"] == self.COMMANDS_SUBDIR
|
||||
|
||||
def test_registrar_config(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.registrar_config["dir"] == self.REGISTRAR_DIR
|
||||
assert i.registrar_config["format"] == "yaml"
|
||||
assert i.registrar_config["args"] == "{{args}}"
|
||||
assert i.registrar_config["extension"] == ".yaml"
|
||||
|
||||
def test_context_file(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.context_file == self.CONTEXT_FILE
|
||||
|
||||
# -- Setup / teardown -------------------------------------------------
|
||||
|
||||
def test_setup_creates_files(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
assert len(created) > 0
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
for f in cmd_files:
|
||||
assert f.exists()
|
||||
assert f.name.startswith("speckit.")
|
||||
assert f.name.endswith(".yaml")
|
||||
|
||||
def test_setup_writes_to_correct_directory(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
expected_dir = i.commands_dest(tmp_path)
|
||||
assert expected_dir.exists(), (
|
||||
f"Expected directory {expected_dir} was not created"
|
||||
)
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
assert len(cmd_files) > 0, "No command files were created"
|
||||
for f in cmd_files:
|
||||
assert f.resolve().parent == expected_dir.resolve(), (
|
||||
f"{f} is not under {expected_dir}"
|
||||
)
|
||||
|
||||
def test_templates_are_processed(self, tmp_path):
|
||||
"""Command files must have placeholders replaced."""
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
assert len(cmd_files) > 0
|
||||
for f in cmd_files:
|
||||
content = f.read_text(encoding="utf-8")
|
||||
assert "{SCRIPT}" not in content, f"{f.name} has unprocessed {{SCRIPT}}"
|
||||
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
|
||||
assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}"
|
||||
|
||||
def test_yaml_has_title(self, tmp_path):
|
||||
"""Every YAML recipe should have a title field."""
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
for f in cmd_files:
|
||||
content = f.read_text(encoding="utf-8")
|
||||
assert "title:" in content, f"{f.name} missing title field"
|
||||
|
||||
def test_yaml_has_prompt(self, tmp_path):
|
||||
"""Every YAML recipe should have a prompt block scalar."""
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
for f in cmd_files:
|
||||
content = f.read_text(encoding="utf-8")
|
||||
assert "prompt: |" in content, f"{f.name} missing prompt block scalar"
|
||||
|
||||
def test_yaml_uses_correct_arg_placeholder(self, tmp_path):
|
||||
"""YAML recipes must use {{args}} placeholder."""
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
has_args = any("{{args}}" in f.read_text(encoding="utf-8") for f in cmd_files)
|
||||
assert has_args, "No YAML recipe contains {{args}} placeholder"
|
||||
has_dollar_args = any(
|
||||
"$ARGUMENTS" in f.read_text(encoding="utf-8") for f in cmd_files
|
||||
)
|
||||
assert not has_dollar_args, (
|
||||
"YAML recipe still contains $ARGUMENTS instead of {{args}}"
|
||||
)
|
||||
|
||||
def test_yaml_is_valid(self, tmp_path):
|
||||
"""Every generated YAML file must parse without errors."""
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
for f in cmd_files:
|
||||
content = f.read_text(encoding="utf-8")
|
||||
# Strip trailing source comment before parsing
|
||||
lines = content.split("\n")
|
||||
yaml_lines = [l for l in lines if not l.startswith("# Source:")]
|
||||
try:
|
||||
parsed = yaml.safe_load("\n".join(yaml_lines))
|
||||
except Exception as exc:
|
||||
raise AssertionError(f"{f.name} is not valid YAML: {exc}") from exc
|
||||
assert "prompt" in parsed, f"{f.name} parsed YAML has no 'prompt' key"
|
||||
assert "title" in parsed, f"{f.name} parsed YAML has no 'title' key"
|
||||
|
||||
def test_yaml_prompt_excludes_frontmatter(self, tmp_path, monkeypatch):
|
||||
i = get_integration(self.KEY)
|
||||
template = tmp_path / "sample.md"
|
||||
template.write_text(
|
||||
"---\n"
|
||||
"description: Summary line one\n"
|
||||
"scripts:\n"
|
||||
" sh: scripts/bash/example.sh\n"
|
||||
"---\n"
|
||||
"Body line one\n"
|
||||
"Body line two\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(i, "list_command_templates", lambda: [template])
|
||||
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
assert len(cmd_files) == 1
|
||||
|
||||
content = cmd_files[0].read_text(encoding="utf-8")
|
||||
# Strip source comment for parsing
|
||||
lines = content.split("\n")
|
||||
yaml_lines = [l for l in lines if not l.startswith("# Source:")]
|
||||
parsed = yaml.safe_load("\n".join(yaml_lines))
|
||||
|
||||
assert "description:" not in parsed["prompt"]
|
||||
assert "scripts:" not in parsed["prompt"]
|
||||
assert "---" not in parsed["prompt"]
|
||||
|
||||
def test_all_files_tracked_in_manifest(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
for f in created:
|
||||
rel = f.resolve().relative_to(tmp_path.resolve()).as_posix()
|
||||
assert rel in m.files, f"{rel} not tracked in manifest"
|
||||
|
||||
def test_install_uninstall_roundtrip(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.install(tmp_path, m)
|
||||
assert len(created) > 0
|
||||
m.save()
|
||||
for f in created:
|
||||
assert f.exists()
|
||||
removed, skipped = i.uninstall(tmp_path, m)
|
||||
assert len(removed) == len(created)
|
||||
assert skipped == []
|
||||
|
||||
def test_modified_file_survives_uninstall(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.install(tmp_path, m)
|
||||
m.save()
|
||||
modified_file = created[0]
|
||||
modified_file.write_text("user modified this", encoding="utf-8")
|
||||
removed, skipped = i.uninstall(tmp_path, m)
|
||||
assert modified_file.exists()
|
||||
assert modified_file in skipped
|
||||
|
||||
# -- Scripts ----------------------------------------------------------
|
||||
|
||||
def test_setup_installs_update_context_scripts(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts"
|
||||
assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}"
|
||||
assert (scripts_dir / "update-context.sh").exists()
|
||||
assert (scripts_dir / "update-context.ps1").exists()
|
||||
|
||||
def test_scripts_tracked_in_manifest(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
i.setup(tmp_path, m)
|
||||
script_rels = [k for k in m.files if "update-context" in k]
|
||||
assert len(script_rels) >= 2
|
||||
|
||||
def test_sh_script_is_executable(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
i.setup(tmp_path, m)
|
||||
sh = (
|
||||
tmp_path
|
||||
/ ".specify"
|
||||
/ "integrations"
|
||||
/ self.KEY
|
||||
/ "scripts"
|
||||
/ "update-context.sh"
|
||||
)
|
||||
assert os.access(sh, os.X_OK)
|
||||
|
||||
# -- CLI auto-promote -------------------------------------------------
|
||||
|
||||
def test_ai_flag_auto_promotes(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / f"promote-{self.KEY}"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--ai",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"sh",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}"
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.commands_dest(project)
|
||||
assert cmd_dir.is_dir(), f"--ai {self.KEY} did not create commands directory"
|
||||
|
||||
def test_integration_flag_creates_files(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / f"int-{self.KEY}"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--integration",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"sh",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, (
|
||||
f"init --integration {self.KEY} failed: {result.output}"
|
||||
)
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.commands_dest(project)
|
||||
assert cmd_dir.is_dir(), f"Commands directory {cmd_dir} not created"
|
||||
commands = sorted(cmd_dir.glob("speckit.*.yaml"))
|
||||
assert len(commands) > 0, f"No command files in {cmd_dir}"
|
||||
|
||||
# -- Complete file inventory ------------------------------------------
|
||||
|
||||
COMMAND_STEMS = [
|
||||
"analyze",
|
||||
"checklist",
|
||||
"clarify",
|
||||
"constitution",
|
||||
"implement",
|
||||
"plan",
|
||||
"specify",
|
||||
"tasks",
|
||||
"taskstoissues",
|
||||
]
|
||||
|
||||
def _expected_files(self, script_variant: str) -> list[str]:
|
||||
"""Build the expected file list for this integration + script variant."""
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.registrar_config["dir"]
|
||||
files = []
|
||||
|
||||
# Command files (.yaml)
|
||||
for stem in self.COMMAND_STEMS:
|
||||
files.append(f"{cmd_dir}/speckit.{stem}.yaml")
|
||||
|
||||
# Integration scripts
|
||||
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1")
|
||||
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh")
|
||||
|
||||
# Framework files
|
||||
files.append(".specify/integration.json")
|
||||
files.append(".specify/init-options.json")
|
||||
files.append(f".specify/integrations/{self.KEY}.manifest.json")
|
||||
files.append(".specify/integrations/speckit.manifest.json")
|
||||
|
||||
if script_variant == "sh":
|
||||
for name in [
|
||||
"check-prerequisites.sh",
|
||||
"common.sh",
|
||||
"create-new-feature.sh",
|
||||
"setup-plan.sh",
|
||||
"update-agent-context.sh",
|
||||
]:
|
||||
files.append(f".specify/scripts/bash/{name}")
|
||||
else:
|
||||
for name in [
|
||||
"check-prerequisites.ps1",
|
||||
"common.ps1",
|
||||
"create-new-feature.ps1",
|
||||
"setup-plan.ps1",
|
||||
"update-agent-context.ps1",
|
||||
]:
|
||||
files.append(f".specify/scripts/powershell/{name}")
|
||||
|
||||
for name in [
|
||||
"agent-file-template.md",
|
||||
"checklist-template.md",
|
||||
"constitution-template.md",
|
||||
"plan-template.md",
|
||||
"spec-template.md",
|
||||
"tasks-template.md",
|
||||
]:
|
||||
files.append(f".specify/templates/{name}")
|
||||
|
||||
files.append(".specify/memory/constitution.md")
|
||||
return sorted(files)
|
||||
|
||||
def test_complete_file_inventory_sh(self, tmp_path):
|
||||
"""Every file produced by specify init --integration <key> --script sh."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / f"inventory-sh-{self.KEY}"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--integration",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"sh",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
actual = sorted(
|
||||
p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()
|
||||
)
|
||||
expected = self._expected_files("sh")
|
||||
assert actual == expected, (
|
||||
f"Missing: {sorted(set(expected) - set(actual))}\n"
|
||||
f"Extra: {sorted(set(actual) - set(expected))}"
|
||||
)
|
||||
|
||||
def test_complete_file_inventory_ps(self, tmp_path):
|
||||
"""Every file produced by specify init --integration <key> --script ps."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / f"inventory-ps-{self.KEY}"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--integration",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"ps",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
actual = sorted(
|
||||
p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()
|
||||
)
|
||||
expected = self._expected_files("ps")
|
||||
assert actual == expected, (
|
||||
f"Missing: {sorted(set(expected) - set(actual))}\n"
|
||||
f"Extra: {sorted(set(actual) - set(expected))}"
|
||||
)
|
||||
@@ -1,28 +1,11 @@
|
||||
"""Tests for CursorAgentIntegration."""
|
||||
|
||||
from .test_integration_base_skills import SkillsIntegrationTests
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestCursorAgentIntegration(SkillsIntegrationTests):
|
||||
class TestCursorAgentIntegration(MarkdownIntegrationTests):
|
||||
KEY = "cursor-agent"
|
||||
FOLDER = ".cursor/"
|
||||
COMMANDS_SUBDIR = "skills"
|
||||
REGISTRAR_DIR = ".cursor/skills"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".cursor/commands"
|
||||
CONTEXT_FILE = ".cursor/rules/specify-rules.mdc"
|
||||
|
||||
|
||||
class TestCursorAgentAutoPromote:
|
||||
"""--ai cursor-agent auto-promotes to integration path."""
|
||||
|
||||
def test_ai_cursor_agent_without_ai_skills_auto_promotes(self, tmp_path):
|
||||
"""--ai cursor-agent should work the same as --integration cursor-agent."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "test-proj"
|
||||
result = runner.invoke(app, ["init", str(target), "--ai", "cursor-agent", "--no-git", "--ignore-agent-tools", "--script", "sh"])
|
||||
|
||||
assert result.exit_code == 0, f"init --ai cursor-agent failed: {result.output}"
|
||||
assert (target / ".cursor" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"""Tests for GooseIntegration."""
|
||||
|
||||
from .test_integration_base_yaml import YamlIntegrationTests
|
||||
|
||||
|
||||
class TestGooseIntegration(YamlIntegrationTests):
|
||||
KEY = "goose"
|
||||
FOLDER = ".goose/"
|
||||
COMMANDS_SUBDIR = "recipes"
|
||||
REGISTRAR_DIR = ".goose/recipes"
|
||||
CONTEXT_FILE = "AGENTS.md"
|
||||
@@ -50,25 +50,16 @@ class TestAgentConfigConsistency:
|
||||
|
||||
def test_devcontainer_kiro_installer_uses_pinned_checksum(self):
|
||||
"""Devcontainer installer should always verify Kiro installer via pinned SHA256."""
|
||||
post_create_text = (REPO_ROOT / ".devcontainer" / "post-create.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
post_create_text = (REPO_ROOT / ".devcontainer" / "post-create.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert (
|
||||
'KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"'
|
||||
in post_create_text
|
||||
)
|
||||
assert 'KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"' in post_create_text
|
||||
assert "sha256sum -c -" in post_create_text
|
||||
assert "KIRO_SKIP_KIRO_INSTALLER_VERIFY" not in post_create_text
|
||||
|
||||
def test_agent_context_scripts_use_kiro_cli(self):
|
||||
"""Agent context scripts should advertise kiro-cli and not legacy q agent key."""
|
||||
bash_text = (
|
||||
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
pwsh_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
|
||||
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||
|
||||
assert "kiro-cli" in bash_text
|
||||
assert "kiro-cli" in pwsh_text
|
||||
@@ -98,12 +89,8 @@ class TestAgentConfigConsistency:
|
||||
|
||||
def test_agent_context_scripts_include_tabnine(self):
|
||||
"""Agent context scripts should support tabnine agent type."""
|
||||
bash_text = (
|
||||
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
pwsh_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
|
||||
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||
|
||||
assert "tabnine" in bash_text
|
||||
assert "TABNINE_FILE" in bash_text
|
||||
@@ -134,9 +121,7 @@ class TestAgentConfigConsistency:
|
||||
|
||||
def test_kimi_in_powershell_validate_set(self):
|
||||
"""PowerShell update-agent-context script should include 'kimi' in ValidateSet."""
|
||||
ps_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
ps_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||
|
||||
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
|
||||
assert validate_set_match is not None
|
||||
@@ -170,12 +155,8 @@ class TestAgentConfigConsistency:
|
||||
|
||||
def test_trae_in_agent_context_scripts(self):
|
||||
"""Agent context scripts should support trae agent type."""
|
||||
bash_text = (
|
||||
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
pwsh_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
|
||||
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||
|
||||
assert "trae" in bash_text
|
||||
assert "TRAE_FILE" in bash_text
|
||||
@@ -184,9 +165,7 @@ class TestAgentConfigConsistency:
|
||||
|
||||
def test_trae_in_powershell_validate_set(self):
|
||||
"""PowerShell update-agent-context script should include 'trae' in ValidateSet."""
|
||||
ps_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
ps_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||
|
||||
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
|
||||
assert validate_set_match is not None
|
||||
@@ -221,9 +200,7 @@ class TestAgentConfigConsistency:
|
||||
|
||||
def test_pi_in_powershell_validate_set(self):
|
||||
"""PowerShell update-agent-context script should include 'pi' in ValidateSet."""
|
||||
ps_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
ps_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||
|
||||
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
|
||||
assert validate_set_match is not None
|
||||
@@ -233,12 +210,8 @@ class TestAgentConfigConsistency:
|
||||
|
||||
def test_agent_context_scripts_include_pi(self):
|
||||
"""Agent context scripts should support pi agent type."""
|
||||
bash_text = (
|
||||
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
pwsh_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
|
||||
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||
|
||||
assert "pi" in bash_text
|
||||
assert "Pi Coding Agent" in bash_text
|
||||
@@ -269,12 +242,8 @@ class TestAgentConfigConsistency:
|
||||
|
||||
def test_iflow_in_agent_context_scripts(self):
|
||||
"""Agent context scripts should support iflow agent type."""
|
||||
bash_text = (
|
||||
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
pwsh_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
|
||||
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
|
||||
|
||||
assert "iflow" in bash_text
|
||||
assert "IFLOW_FILE" in bash_text
|
||||
@@ -284,37 +253,3 @@ class TestAgentConfigConsistency:
|
||||
def test_ai_help_includes_iflow(self):
|
||||
"""CLI help text for --ai should include iflow."""
|
||||
assert "iflow" in AI_ASSISTANT_HELP
|
||||
|
||||
# --- Goose consistency checks ---
|
||||
|
||||
def test_goose_in_agent_config(self):
|
||||
"""AGENT_CONFIG should include goose with correct folder and commands_subdir."""
|
||||
assert "goose" in AGENT_CONFIG
|
||||
assert AGENT_CONFIG["goose"]["folder"] == ".goose/"
|
||||
assert AGENT_CONFIG["goose"]["commands_subdir"] == "recipes"
|
||||
assert AGENT_CONFIG["goose"]["requires_cli"] is True
|
||||
|
||||
def test_goose_in_extension_registrar(self):
|
||||
"""Extension command registrar should include goose targeting .goose/recipes."""
|
||||
cfg = CommandRegistrar.AGENT_CONFIGS
|
||||
|
||||
assert "goose" in cfg
|
||||
assert cfg["goose"]["dir"] == ".goose/recipes"
|
||||
assert cfg["goose"]["format"] == "yaml"
|
||||
assert cfg["goose"]["args"] == "{{args}}"
|
||||
|
||||
def test_goose_in_agent_context_scripts(self):
|
||||
"""Agent context scripts should support goose agent type."""
|
||||
bash_text = (
|
||||
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
pwsh_text = (
|
||||
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert "goose" in bash_text
|
||||
assert "goose" in pwsh_text
|
||||
|
||||
def test_ai_help_includes_goose(self):
|
||||
"""CLI help text for --ai should include goose."""
|
||||
assert "goose" in AI_ASSISTANT_HELP
|
||||
|
||||
@@ -2995,122 +2995,6 @@ class TestExtensionAddCLI:
|
||||
f"but was called with '{download_called_with[0]}'"
|
||||
)
|
||||
|
||||
def test_add_bundled_extension_not_found_gives_clear_error(self, tmp_path):
|
||||
"""extension add should give a clear error when a bundled extension is not found locally."""
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import patch, MagicMock
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
# Create project structure
|
||||
project_dir = tmp_path / "test-project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / ".specify").mkdir()
|
||||
(project_dir / ".specify" / "extensions").mkdir(parents=True)
|
||||
|
||||
# Mock catalog that returns a bundled extension without download_url
|
||||
mock_catalog = MagicMock()
|
||||
mock_catalog.get_extension_info.return_value = {
|
||||
"id": "git",
|
||||
"name": "Git Branching Workflow",
|
||||
"version": "1.0.0",
|
||||
"description": "Git branching extension",
|
||||
"bundled": True,
|
||||
"_install_allowed": True,
|
||||
}
|
||||
mock_catalog.search.return_value = []
|
||||
|
||||
with patch("specify_cli.extensions.ExtensionCatalog", return_value=mock_catalog), \
|
||||
patch("specify_cli._locate_bundled_extension", return_value=None), \
|
||||
patch.object(Path, "cwd", return_value=project_dir):
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["extension", "add", "git"],
|
||||
catch_exceptions=True,
|
||||
)
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "bundled with spec-kit" in result.output
|
||||
assert "reinstall" in result.output.lower()
|
||||
|
||||
|
||||
class TestDownloadExtensionBundled:
|
||||
"""Tests for download_extension handling of bundled extensions."""
|
||||
|
||||
def test_download_extension_raises_for_bundled(self, temp_dir):
|
||||
"""download_extension should raise a clear error for bundled extensions without a URL."""
|
||||
from unittest.mock import patch
|
||||
|
||||
project_dir = temp_dir / "project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / ".specify").mkdir()
|
||||
|
||||
catalog = ExtensionCatalog(project_dir)
|
||||
|
||||
bundled_ext_info = {
|
||||
"name": "Git Branching Workflow",
|
||||
"id": "git",
|
||||
"version": "1.0.0",
|
||||
"description": "Git workflow",
|
||||
"bundled": True,
|
||||
}
|
||||
|
||||
with patch.object(catalog, "get_extension_info", return_value=bundled_ext_info):
|
||||
with pytest.raises(ExtensionError, match="bundled with spec-kit"):
|
||||
catalog.download_extension("git")
|
||||
|
||||
def test_download_extension_allows_bundled_with_url(self, temp_dir):
|
||||
"""download_extension should allow bundled extensions that have a download_url (newer version)."""
|
||||
from unittest.mock import patch, MagicMock
|
||||
import urllib.request
|
||||
|
||||
project_dir = temp_dir / "project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / ".specify").mkdir()
|
||||
|
||||
catalog = ExtensionCatalog(project_dir)
|
||||
|
||||
bundled_with_url = {
|
||||
"name": "Git Branching Workflow",
|
||||
"id": "git",
|
||||
"version": "2.0.0",
|
||||
"description": "Git workflow",
|
||||
"bundled": True,
|
||||
"download_url": "https://example.com/git-2.0.0.zip",
|
||||
}
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.read.return_value = b"fake zip data"
|
||||
mock_response.__enter__ = lambda s: s
|
||||
mock_response.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
with patch.object(catalog, "get_extension_info", return_value=bundled_with_url), \
|
||||
patch.object(urllib.request, "urlopen", return_value=mock_response):
|
||||
result = catalog.download_extension("git")
|
||||
assert result.name == "git-2.0.0.zip"
|
||||
|
||||
def test_download_extension_raises_no_url_for_non_bundled(self, temp_dir):
|
||||
"""download_extension should raise 'no download URL' for non-bundled extensions without URL."""
|
||||
from unittest.mock import patch
|
||||
|
||||
project_dir = temp_dir / "project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / ".specify").mkdir()
|
||||
|
||||
catalog = ExtensionCatalog(project_dir)
|
||||
|
||||
non_bundled_ext_info = {
|
||||
"name": "Some Extension",
|
||||
"id": "some-ext",
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
}
|
||||
|
||||
with patch.object(catalog, "get_extension_info", return_value=non_bundled_ext_info):
|
||||
with pytest.raises(ExtensionError, match="has no download URL"):
|
||||
catalog.download_extension("some-ext")
|
||||
|
||||
|
||||
class TestExtensionUpdateCLI:
|
||||
"""CLI integration tests for extension update command."""
|
||||
|
||||
@@ -2865,182 +2865,3 @@ class TestPresetEnableDisable:
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "corrupted state" in result.output.lower()
|
||||
|
||||
|
||||
# ===== Lean Preset Tests =====
|
||||
|
||||
|
||||
LEAN_PRESET_DIR = Path(__file__).parent.parent / "presets" / "lean"
|
||||
|
||||
LEAN_COMMAND_NAMES = [
|
||||
"speckit.specify",
|
||||
"speckit.plan",
|
||||
"speckit.tasks",
|
||||
"speckit.implement",
|
||||
"speckit.constitution",
|
||||
]
|
||||
|
||||
|
||||
class TestLeanPreset:
|
||||
"""Tests for the lean preset that ships with the repo."""
|
||||
|
||||
def test_lean_preset_exists(self):
|
||||
"""Verify the lean preset directory and manifest exist."""
|
||||
assert LEAN_PRESET_DIR.exists()
|
||||
assert (LEAN_PRESET_DIR / "preset.yml").exists()
|
||||
|
||||
def test_lean_manifest_valid(self):
|
||||
"""Verify the lean preset manifest is valid."""
|
||||
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
|
||||
assert manifest.id == "lean"
|
||||
assert manifest.name == "Lean Workflow"
|
||||
assert manifest.version == "1.0.0"
|
||||
assert len(manifest.templates) == 5 # 5 commands
|
||||
|
||||
def test_lean_provides_core_workflow_commands(self):
|
||||
"""Verify the lean preset provides overrides for core workflow commands."""
|
||||
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
|
||||
provided_names = {t["name"] for t in manifest.templates}
|
||||
for name in LEAN_COMMAND_NAMES:
|
||||
assert name in provided_names, f"Lean preset missing command: {name}"
|
||||
|
||||
def test_lean_command_files_exist(self):
|
||||
"""Verify that all declared command files actually exist on disk."""
|
||||
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
|
||||
for tmpl in manifest.templates:
|
||||
tmpl_path = LEAN_PRESET_DIR / tmpl["file"]
|
||||
assert tmpl_path.exists(), f"Missing command file: {tmpl['file']}"
|
||||
|
||||
def test_lean_commands_have_no_scripts(self):
|
||||
"""Verify lean commands have no scripts or agent_scripts in frontmatter."""
|
||||
from specify_cli.agents import CommandRegistrar
|
||||
|
||||
for name in LEAN_COMMAND_NAMES:
|
||||
cmd_path = LEAN_PRESET_DIR / "commands" / f"speckit.{name.split('.')[-1]}.md"
|
||||
content = cmd_path.read_text()
|
||||
frontmatter, _ = CommandRegistrar.parse_frontmatter(content)
|
||||
assert "scripts" not in frontmatter, f"{name} should not have scripts in frontmatter"
|
||||
assert "agent_scripts" not in frontmatter, f"{name} should not have agent_scripts in frontmatter"
|
||||
|
||||
def test_lean_commands_have_no_hooks(self):
|
||||
"""Verify lean commands do not contain extension hook boilerplate."""
|
||||
for name in LEAN_COMMAND_NAMES:
|
||||
cmd_path = LEAN_PRESET_DIR / "commands" / f"speckit.{name.split('.')[-1]}.md"
|
||||
content = cmd_path.read_text()
|
||||
assert "hooks." not in content, f"{name} should not reference extension hooks"
|
||||
assert "extensions.yml" not in content, f"{name} should not reference extensions.yml"
|
||||
|
||||
def test_install_lean_preset(self, project_dir):
|
||||
"""Test installing the lean preset from its directory."""
|
||||
manager = PresetManager(project_dir)
|
||||
manifest = manager.install_from_directory(LEAN_PRESET_DIR, "0.6.0")
|
||||
assert manifest.id == "lean"
|
||||
assert manager.registry.is_installed("lean")
|
||||
|
||||
def test_lean_overrides_commands(self, project_dir):
|
||||
"""Test that lean preset overrides are resolved correctly."""
|
||||
manager = PresetManager(project_dir)
|
||||
manager.install_from_directory(LEAN_PRESET_DIR, "0.6.0")
|
||||
|
||||
resolver = PresetResolver(project_dir)
|
||||
for name in LEAN_COMMAND_NAMES:
|
||||
result = resolver.resolve(name, template_type="command")
|
||||
assert result is not None, f"Lean override for {name} not resolved"
|
||||
|
||||
|
||||
# ===== Bundled Preset Locator Tests =====
|
||||
|
||||
|
||||
class TestBundledPresetLocator:
|
||||
"""Tests for _locate_bundled_preset discovery function."""
|
||||
|
||||
def test_locate_bundled_lean_preset(self):
|
||||
"""_locate_bundled_preset finds the lean preset."""
|
||||
from specify_cli import _locate_bundled_preset
|
||||
|
||||
path = _locate_bundled_preset("lean")
|
||||
assert path is not None
|
||||
assert (path / "preset.yml").is_file()
|
||||
|
||||
def test_locate_bundled_preset_not_found(self):
|
||||
"""_locate_bundled_preset returns None for nonexistent preset."""
|
||||
from specify_cli import _locate_bundled_preset
|
||||
|
||||
path = _locate_bundled_preset("nonexistent-preset")
|
||||
assert path is None
|
||||
|
||||
def test_locate_bundled_preset_rejects_invalid_id(self):
|
||||
"""_locate_bundled_preset rejects IDs with invalid characters."""
|
||||
from specify_cli import _locate_bundled_preset
|
||||
|
||||
assert _locate_bundled_preset("../escape") is None
|
||||
assert _locate_bundled_preset("UPPERCASE") is None
|
||||
assert _locate_bundled_preset("has spaces") is None
|
||||
|
||||
def test_bundled_preset_add_via_cli(self, project_dir):
|
||||
"""Test that 'specify preset add lean' installs the bundled preset."""
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import patch
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
with patch.object(Path, "cwd", return_value=project_dir), \
|
||||
patch("specify_cli.get_speckit_version", return_value="0.6.0"):
|
||||
result = runner.invoke(app, ["preset", "add", "lean"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "Lean Workflow" in result.output
|
||||
assert "installed" in result.output.lower()
|
||||
|
||||
def test_bundled_preset_in_catalog(self):
|
||||
"""Verify the lean preset is listed in catalog.json with bundled marker."""
|
||||
catalog_path = Path(__file__).parent.parent / "presets" / "catalog.json"
|
||||
catalog = json.loads(catalog_path.read_text())
|
||||
assert "lean" in catalog["presets"]
|
||||
assert catalog["presets"]["lean"]["bundled"] is True
|
||||
assert "download_url" not in catalog["presets"]["lean"]
|
||||
|
||||
def test_bundled_preset_download_raises_error(self, project_dir):
|
||||
"""download_pack raises PresetError for bundled presets without download_url."""
|
||||
catalog = PresetCatalog(project_dir)
|
||||
|
||||
catalog_data = {
|
||||
"test-bundled": {
|
||||
"name": "Test Bundled",
|
||||
"version": "1.0.0",
|
||||
"bundled": True,
|
||||
}
|
||||
}
|
||||
from unittest.mock import patch
|
||||
with patch.object(catalog, "_get_merged_packs", return_value=catalog_data):
|
||||
with pytest.raises(PresetError, match="bundled with spec-kit"):
|
||||
catalog.download_pack("test-bundled")
|
||||
|
||||
def test_bundled_preset_missing_locally_cli_error(self, project_dir):
|
||||
"""CLI shows clear error when bundled preset cannot be found locally."""
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import patch
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
# Patch _locate_bundled_preset to return None (simulating missing files)
|
||||
# and mock the catalog to return a bundled entry for "lean"
|
||||
fake_pack_info = {
|
||||
"id": "lean",
|
||||
"name": "Lean Workflow",
|
||||
"version": "1.0.0",
|
||||
"bundled": True,
|
||||
"_install_allowed": True,
|
||||
}
|
||||
with patch.object(Path, "cwd", return_value=project_dir), \
|
||||
patch("specify_cli._locate_bundled_preset", return_value=None), \
|
||||
patch("specify_cli.presets.PresetCatalog") as MockCatalog:
|
||||
MockCatalog.return_value.get_pack_info.return_value = fake_pack_info
|
||||
result = runner.invoke(app, ["preset", "add", "lean"])
|
||||
|
||||
# Should fail with a helpful error explaining this is a bundled preset
|
||||
# and suggesting how to recover.
|
||||
assert result.exit_code == 1
|
||||
output = strip_ansi(result.output).lower()
|
||||
assert "bundled" in output, result.output
|
||||
assert "reinstall" in output, result.output
|
||||
|
||||
Reference in New Issue
Block a user