diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index caf184c1c6..88acdcd5fd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -50,8 +50,6 @@ "kilocode.Kilo-Code", // Roo Code "RooVeterinaryInc.roo-cline", - // Amazon Developer Q - "AmazonWebServices.amazon-q-vscode", // Claude Code "anthropic.claude-code" ], diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 9608a28a3d..024ff0db48 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -51,32 +51,33 @@ echo -e "\nšŸ¤– Installing OpenCode CLI..." run_command "npm install -g opencode-ai@latest" echo "āœ… Done" -echo -e "\nšŸ¤– Installing Amazon Q CLI..." -# šŸ‘‰šŸ¾ https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-verify-download.html - -run_command "curl --proto '=https' --tlsv1.2 -sSf 'https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux.zip' -o 'q.zip'" -run_command "curl --proto '=https' --tlsv1.2 -sSf 'https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux.zip.sig' -o 'q.zip.sig'" -cat > amazonq-public-key.asc << 'EOF' ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEZig60RYJKwYBBAHaRw8BAQdAy/+G05U5/EOA72WlcD4WkYn5SInri8pc4Z6D -BKNNGOm0JEFtYXpvbiBRIENMSSBUZWFtIDxxLWNsaUBhbWF6b24uY29tPoiZBBMW -CgBBFiEEmvYEF+gnQskUPgPsUNx6jcJMVmcFAmYoOtECGwMFCQPCZwAFCwkIBwIC -IgIGFQoJCAsCBBYCAwECHgcCF4AACgkQUNx6jcJMVmef5QD/QWWEGG/cOnbDnp68 -SJXuFkwiNwlH2rPw9ZRIQMnfAS0A/0V6ZsGB4kOylBfc7CNfzRFGtovdBBgHqA6P -zQ/PNscGuDgEZig60RIKKwYBBAGXVQEFAQEHQC4qleONMBCq3+wJwbZSr0vbuRba -D1xr4wUPn4Avn4AnAwEIB4h+BBgWCgAmFiEEmvYEF+gnQskUPgPsUNx6jcJMVmcF -AmYoOtECGwwFCQPCZwAACgkQUNx6jcJMVmchMgEA6l3RveCM0YHAGQaSFMkguoAo -vK6FgOkDawgP0NPIP2oA/jIAO4gsAntuQgMOsPunEdDeji2t+AhV02+DQIsXZpoB -=f8yY ------END PGP PUBLIC KEY BLOCK----- -EOF -run_command "gpg --batch --import amazonq-public-key.asc" -run_command "gpg --verify q.zip.sig q.zip" -run_command "unzip -q q.zip" -run_command "chmod +x ./q/install.sh" -run_command "./q/install.sh --no-confirm" -run_command "rm -rf ./q q.zip q.zip.sig amazonq-public-key.asc" +echo -e "\nšŸ¤– Installing Kiro CLI..." +# https://kiro.dev/docs/cli/ +KIRO_INSTALLER_URL="https://cli.kiro.dev/install" +KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb" +KIRO_INSTALLER_PATH="$(mktemp)" + +cleanup_kiro_installer() { + rm -f "$KIRO_INSTALLER_PATH" +} +trap cleanup_kiro_installer EXIT + +run_command "curl -fsSL \"$KIRO_INSTALLER_URL\" -o \"$KIRO_INSTALLER_PATH\"" +run_command "echo \"$KIRO_INSTALLER_SHA256 $KIRO_INSTALLER_PATH\" | sha256sum -c -" + +run_command "bash \"$KIRO_INSTALLER_PATH\"" + +kiro_binary="" +if command -v kiro-cli >/dev/null 2>&1; then + kiro_binary="kiro-cli" +elif command -v kiro >/dev/null 2>&1; then + kiro_binary="kiro" +else + echo -e "\033[0;31m[ERROR] Kiro CLI installation did not create 'kiro-cli' or 'kiro' in PATH.\033[0m" >&2 + exit 1 +fi + +run_command "$kiro_binary --help > /dev/null" echo "āœ… Done" echo -e "\nšŸ¤– Installing CodeBuddy CLI..." diff --git a/.github/ISSUE_TEMPLATE/agent_request.yml b/.github/ISSUE_TEMPLATE/agent_request.yml index a72dacda52..a6ac6c4bff 100644 --- a/.github/ISSUE_TEMPLATE/agent_request.yml +++ b/.github/ISSUE_TEMPLATE/agent_request.yml @@ -8,7 +8,7 @@ body: value: | Thanks for requesting a new agent! Before submitting, please check if the agent is already supported. - **Currently supported agents**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Amazon Q Developer CLI, Amp, SHAI, IBM Bob, Antigravity + **Currently supported agents**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Kiro CLI, Amp, SHAI, IBM Bob, Antigravity - type: input id: agent-name diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cbe1955ad4..dd09f8e02a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -75,7 +75,7 @@ body: - Roo Code - CodeBuddy - Qoder CLI - - Amazon Q Developer CLI + - Kiro CLI - Amp - SHAI - IBM Bob diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 71786ddcef..3b5889288b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -69,7 +69,7 @@ body: - Roo Code - CodeBuddy - Qoder CLI - - Amazon Q Developer CLI + - Kiro CLI - Amp - SHAI - IBM Bob diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b2811b43bb..3fe3894076 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # Fetch all history for git info diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f6fbd24738..3b3bcf8d3c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run markdownlint-cli2 uses: DavidAnson/markdownlint-cli2-action@v19 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30f28f3210..39e0d8531a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scripts/create-github-release.sh b/.github/workflows/scripts/create-github-release.sh index 29f5024f8c..0418ce2b08 100644 --- a/.github/workflows/scripts/create-github-release.sh +++ b/.github/workflows/scripts/create-github-release.sh @@ -46,8 +46,8 @@ gh release create "$VERSION" \ .genreleases/spec-kit-template-amp-ps-"$VERSION".zip \ .genreleases/spec-kit-template-shai-sh-"$VERSION".zip \ .genreleases/spec-kit-template-shai-ps-"$VERSION".zip \ - .genreleases/spec-kit-template-q-sh-"$VERSION".zip \ - .genreleases/spec-kit-template-q-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-kiro-cli-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-kiro-cli-ps-"$VERSION".zip \ .genreleases/spec-kit-template-agy-sh-"$VERSION".zip \ .genreleases/spec-kit-template-agy-ps-"$VERSION".zip \ .genreleases/spec-kit-template-bob-sh-"$VERSION".zip \ diff --git a/.github/workflows/scripts/create-release-packages.ps1 b/.github/workflows/scripts/create-release-packages.ps1 index ed04d9cd3f..0b33b0d19d 100644 --- a/.github/workflows/scripts/create-release-packages.ps1 +++ b/.github/workflows/scripts/create-release-packages.ps1 @@ -14,7 +14,7 @@ .PARAMETER Agents Comma or space separated subset of agents to build (default: all) - Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, q, bob, qodercli, shai, agy, generic + Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, agy, generic .PARAMETER Scripts Comma or space separated subset of script types to build (default: both) @@ -335,9 +335,9 @@ function Build-Variant { $cmdDir = Join-Path $baseDir ".agents/commands" Generate-Commands -Agent 'amp' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script } - 'q' { - $cmdDir = Join-Path $baseDir ".amazonq/prompts" - Generate-Commands -Agent 'q' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script + 'kiro-cli' { + $cmdDir = Join-Path $baseDir ".kiro/prompts" + Generate-Commands -Agent 'kiro-cli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script } 'bob' { $cmdDir = Join-Path $baseDir ".bob/commands" @@ -347,10 +347,21 @@ function Build-Variant { $cmdDir = Join-Path $baseDir ".qoder/commands" Generate-Commands -Agent 'qodercli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script } + 'shai' { + $cmdDir = Join-Path $baseDir ".shai/commands" + Generate-Commands -Agent 'shai' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script + } + 'agy' { + $cmdDir = Join-Path $baseDir ".agent/workflows" + Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script + } 'generic' { $cmdDir = Join-Path $baseDir ".speckit/commands" Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script } + default { + throw "Unsupported agent '$Agent'." + } } # Create zip archive @@ -360,7 +371,7 @@ function Build-Variant { } # Define all agents and scripts -$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'q', 'bob', 'qodercli', 'shai', 'agy', 'generic') +$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'agy', 'generic') $AllScripts = @('sh', 'ps') function Normalize-List { @@ -425,4 +436,4 @@ foreach ($agent in $AgentList) { Write-Host "`nArchives in ${GenReleasesDir}:" Get-ChildItem -Path $GenReleasesDir -Filter "spec-kit-template-*-${Version}.zip" | ForEach-Object { Write-Host " $($_.Name)" -} \ No newline at end of file +} diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 1b2ced3ea3..08ff1de212 100755 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -6,7 +6,7 @@ set -euo pipefail # Usage: .github/workflows/scripts/create-release-packages.sh # Version argument should include leading 'v'. # Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built. -# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex amp shai bob generic (default: all) +# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai kiro-cli agy bob qodercli generic (default: all) # SCRIPTS : space or comma separated subset of: sh ps (default: both) # Examples: # AGENTS=claude SCRIPTS=sh $0 v0.2.0 @@ -212,9 +212,9 @@ build_variant() { shai) mkdir -p "$base_dir/.shai/commands" generate_commands shai md "\$ARGUMENTS" "$base_dir/.shai/commands" "$script" ;; - q) - mkdir -p "$base_dir/.amazonq/prompts" - generate_commands q md "\$ARGUMENTS" "$base_dir/.amazonq/prompts" "$script" ;; + kiro-cli) + mkdir -p "$base_dir/.kiro/prompts" + generate_commands kiro-cli md "\$ARGUMENTS" "$base_dir/.kiro/prompts" "$script" ;; agy) mkdir -p "$base_dir/.agent/workflows" generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/workflows" "$script" ;; @@ -230,7 +230,7 @@ build_variant() { } # Determine agent list -ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai q agy bob qodercli generic) +ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai kiro-cli agy bob qodercli generic) ALL_SCRIPTS=(sh ps) norm_list() { @@ -277,4 +277,3 @@ done echo "Archives in $GENRELEASES_DIR:" ls -1 "$GENRELEASES_DIR"/spec-kit-template-*-"${NEW_VERSION}".zip - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60e3114c85..9c62304388 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,10 +16,10 @@ jobs: uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.13" @@ -36,10 +36,10 @@ jobs: uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/AGENTS.md b/AGENTS.md index d8dc0f08f7..4cafa7defb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ Specify supports multiple AI agents by generating agent-specific command files a | **Roo Code** | `.roo/rules/` | Markdown | N/A (IDE-based) | Roo Code IDE | | **CodeBuddy CLI** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy CLI | | **Qoder CLI** | `.qoder/commands/` | Markdown | `qodercli` | Qoder CLI | -| **Amazon Q Developer CLI** | `.amazonq/prompts/` | Markdown | `q` | Amazon Q Developer CLI | +| **Kiro CLI** | `.kiro/prompts/` | Markdown | `kiro-cli` | Kiro CLI | | **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI | | **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI | | **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE | @@ -86,7 +86,7 @@ This eliminates the need for special-case mappings throughout the codebase. - `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, agy), `"prompts"` (codex, q), `"command"` (opencode - singular) + - Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode, agy), `"prompts"` (codex, kiro-cli), `"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 @@ -96,7 +96,7 @@ This eliminates the need for special-case mappings throughout the codebase. Update the `--ai` parameter help text in the `init()` command to include the new agent: ```python -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 q"), +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"), ``` Also update any function docstrings, examples, and error messages that list available agents. @@ -117,7 +117,7 @@ Modify `.github/workflows/scripts/create-release-packages.sh`: ##### Add to ALL_AGENTS array ```bash -ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf q) +ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf kiro-cli) ``` ##### Add case statement for directory structure @@ -317,7 +317,7 @@ Require a command-line tool to be installed: - **Cursor**: `cursor-agent` CLI - **Qwen Code**: `qwen` CLI - **opencode**: `opencode` CLI -- **Amazon Q Developer CLI**: `q` CLI +- **Kiro CLI**: `kiro-cli` CLI - **CodeBuddy CLI**: `codebuddy` CLI - **Qoder CLI**: `qodercli` CLI - **Amp**: `amp` CLI @@ -335,7 +335,7 @@ Work within integrated development environments: ### Markdown Format -Used by: Claude, Cursor, opencode, Windsurf, Amazon Q Developer, Amp, SHAI, IBM Bob +Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob **Standard format:** diff --git a/CHANGELOG.md b/CHANGELOG.md index 741ce5d0cb..fd30657161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ Recent changes to the Specify CLI and templates are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.8] - 2026-02-28 + +### Fixed + +- **`init --ai-skills` cleanup for non-standard command paths**: Updated post-skills cleanup to remove the configured `commands_subdir` from `AGENT_CONFIG` rather than assuming `commands/`, so agents like `kiro-cli` now correctly remove extracted `.kiro/prompts` after successful skills installation on new projects. +- **Kiro devcontainer installer integrity**: Enforced always-on installer verification using a repo-pinned `KIRO_INSTALLER_SHA256` checksum before executing the downloaded script. + +## [0.1.7] - 2026-02-25 + +### Changed + +- **Kiro CLI migration**: Replaced Amazon Q (`q`) integration with Kiro CLI (`kiro-cli`) across runtime config, release packaging, extension command registration, and documentation. + - Added canonical `kiro-cli` agent support with prompt directory `.kiro/prompts` + - Added `kiro` alias support for `specify init --ai` + - Updated context update scripts and release packaging scripts to emit Kiro artifacts + - Updated devcontainer setup to install Kiro CLI using the official installer command +- **Amazon Q retirement**: Removed Amazon Q (`q`) support from active agent configuration and release artifacts. + ## [0.1.6] - 2026-02-23 ### Fixed diff --git a/README.md b/README.md index 85a9a8f1e9..5316c3a2a4 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c | Agent | Support | Notes | | ------------------------------------------------------------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | [Qoder CLI](https://qoder.com/cli) | āœ… | | -| [Amazon Q Developer CLI](https://aws.amazon.com/developer/learning/q-developer-cli/) | āš ļø | Amazon Q Developer CLI [does not support](https://github.com/aws/amazon-q-developer-cli/issues/3064) custom arguments for slash commands. | +| [Kiro CLI](https://kiro.dev/docs/cli/) | āœ… | Use `--ai kiro-cli` (alias: `--ai kiro`) | | [Amp](https://ampcode.com/) | āœ… | | | [Auggie CLI](https://docs.augmentcode.com/cli/overview) | āœ… | | | [Claude Code](https://www.anthropic.com/claude-code) | āœ… | | @@ -173,14 +173,14 @@ The `specify` command supports the following options: | Command | Description | | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `init` | Initialize a new Specify project from the latest template | -| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `shai`, `qodercli`) | +| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`) | ### `specify init` Arguments & Options | Argument/Option | Type | Description | | ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) | -| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `q`, `agy`, `bob`, `qodercli`, or `generic` (requires `--ai-commands-dir`) | +| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, or `generic` (requires `--ai-commands-dir`) | | `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) | | `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) | | `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code | @@ -210,6 +210,9 @@ specify init my-project --ai qodercli # Initialize with Windsurf support specify init my-project --ai windsurf +# Initialize with Kiro CLI support +specify init my-project --ai kiro-cli + # Initialize with Amp support specify init my-project --ai amp @@ -393,7 +396,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, or Amazon Q Developer CLI 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, or Kiro CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command: ```bash specify init --ai claude --ignore-agent-tools diff --git a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md index ff7a3aabe5..f86beb62bb 100644 --- a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md +++ b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md @@ -456,18 +456,20 @@ Users install with: specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip ``` -### Option 3: Extension Catalog (Future) +### Option 3: Community Reference Catalog -Submit to official catalog: +Submit to the community catalog for public discovery: 1. **Fork** spec-kit repository -2. **Add entry** to `extensions/catalog.json` -3. **Create PR** -4. **After merge**, users can install with: - - ```bash - specify extension add my-ext # No URL needed! - ``` +2. **Add entry** to `extensions/catalog.community.json` +3. **Update** `extensions/README.md` with your extension +4. **Create PR** following the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) +5. **After merge**, your extension becomes available: + - Users can browse `catalog.community.json` to discover your extension + - Users copy the entry to their own `catalog.json` + - Users install with: `specify extension add my-ext` (from their catalog) + +See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed submission instructions. --- diff --git a/extensions/EXTENSION-PUBLISHING-GUIDE.md b/extensions/EXTENSION-PUBLISHING-GUIDE.md index 10eacbf909..39b744b2e1 100644 --- a/extensions/EXTENSION-PUBLISHING-GUIDE.md +++ b/extensions/EXTENSION-PUBLISHING-GUIDE.md @@ -129,26 +129,32 @@ specify extension add --from https://github.com/your-org/spec-kit-your-extension ## Submit to Catalog +### Understanding the Catalogs + +Spec Kit uses a dual-catalog system. For details about how catalogs work, see the main [Extensions README](README.md#extension-catalogs). + +**For extension publishing**: All community extensions should be added to `catalog.community.json`. Users browse this catalog and copy extensions they trust into their own `catalog.json`. + ### 1. Fork the spec-kit Repository ```bash # Fork on GitHub -# https://github.com/statsperform/spec-kit/fork +# https://github.com/github/spec-kit/fork # Clone your fork git clone https://github.com/YOUR-USERNAME/spec-kit.git cd spec-kit ``` -### 2. Add Extension to Catalog +### 2. Add Extension to Community Catalog -Edit `extensions/catalog.json` and add your extension: +Edit `extensions/catalog.community.json` and add your extension: ```json { "schema_version": "1.0", "updated_at": "2026-01-28T15:54:00Z", - "catalog_url": "https://raw.githubusercontent.com/statsperform/spec-kit/main/extensions/catalog.json", + "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json", "extensions": { "your-extension": { "name": "Your Extension Name", @@ -198,15 +204,25 @@ Edit `extensions/catalog.json` and add your extension: - Use current timestamp for `created_at` and `updated_at` - Update the top-level `updated_at` to current time -### 3. Submit Pull Request +### 3. Update Extensions README + +Add your extension to the Available Extensions table in `extensions/README.md`: + +```markdown +| Your Extension Name | Brief description of what it does | [repo-name](https://github.com/your-org/spec-kit-your-extension) | +``` + +Insert your extension in alphabetical order in the table. + +### 4. Submit Pull Request ```bash # Create a branch git checkout -b add-your-extension # Commit your changes -git add extensions/catalog.json -git commit -m "Add your-extension to catalog +git add extensions/catalog.community.json extensions/README.md +git commit -m "Add your-extension to community catalog - Extension ID: your-extension - Version: 1.0.0 @@ -218,7 +234,7 @@ git commit -m "Add your-extension to catalog git push origin add-your-extension # Create Pull Request on GitHub -# https://github.com/statsperform/spec-kit/compare +# https://github.com/github/spec-kit/compare ``` **Pull Request Template**: @@ -243,6 +259,8 @@ Brief description of what your extension does. - [x] Extension tested on real project - [x] All commands working - [x] No security vulnerabilities +- [x] Added to extensions/catalog.community.json +- [x] Added to extensions/README.md Available Extensions table ### Testing Tested on: diff --git a/extensions/EXTENSION-USER-GUIDE.md b/extensions/EXTENSION-USER-GUIDE.md index 802026231f..f5b5befaf5 100644 --- a/extensions/EXTENSION-USER-GUIDE.md +++ b/extensions/EXTENSION-USER-GUIDE.md @@ -76,13 +76,15 @@ vim .specify/extensions/jira/jira-config.yml ## Finding Extensions +**Note**: By default, `specify extension search` uses your organization's catalog (`catalog.json`). If the catalog is empty, you won't see any results. See [Extension Catalogs](#extension-catalogs) to learn how to populate your catalog from the community reference catalog. + ### Browse All Extensions ```bash specify extension search ``` -Shows all available extensions in the catalog. +Shows all extensions in your organization's catalog. ### Search by Keyword @@ -415,11 +417,15 @@ export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json" --- +## Extension Catalogs + +For information about how Spec Kit's dual-catalog system works (`catalog.json` vs `catalog.community.json`), see the main [Extensions README](README.md#extension-catalogs). + ## Organization Catalog Customization -### Why the Default Catalog is Empty +### Why Customize Your Catalog -The default spec-kit catalog ships empty by design. This allows organizations to: +Organizations customize their `catalog.json` to: - **Control available extensions** - Curate which extensions your team can install - **Host private extensions** - Internal tools that shouldn't be public diff --git a/extensions/README.md b/extensions/README.md index 0be853a40b..574144a4d1 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -1,8 +1,74 @@ -# Spec Kit Community Extensions +# Spec Kit Extensions -Community-contributed extensions for [Spec Kit](https://github.com/github/spec-kit). +Extension system for [Spec Kit](https://github.com/github/spec-kit) - add new functionality without bloating the core framework. -## Available Extensions +## Extension Catalogs + +Spec Kit provides two catalog files with different purposes: + +### Your Catalog (`catalog.json`) + +- **Purpose**: Default upstream catalog of extensions used by the Spec Kit CLI +- **Default State**: Empty by design in the upstream project - you or your organization populate a fork/copy with extensions you trust +- **Location (upstream)**: `extensions/catalog.json` in the GitHub-hosted spec-kit repo +- **CLI Default**: The `specify extension` commands use the upstream catalog URL by default, unless overridden +- **Org Catalog**: Point `SPECKIT_CATALOG_URL` at your organization's fork or hosted catalog JSON to use it instead of the upstream default +- **Customization**: Copy entries from the community catalog into your org catalog, or add your own extensions directly + +**Example override:** +```bash +# Override the default upstream catalog with your organization's catalog +export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json" +specify extension search # Now uses your organization's catalog instead of the upstream default +``` + +### Community Reference Catalog (`catalog.community.json`) + +- **Purpose**: Browse available community-contributed extensions +- **Status**: Active - contains extensions submitted by the community +- **Location**: `extensions/catalog.community.json` +- **Usage**: Reference catalog for discovering available extensions +- **Submission**: Open to community contributions via Pull Request + +**How It Works:** + +## Making Extensions Available + +You control which extensions your team can discover and install: + +### Option 1: Curated Catalog (Recommended for Organizations) + +Populate your `catalog.json` with approved extensions: + +1. **Discover** extensions from various sources: + - Browse `catalog.community.json` for community extensions + - Find private/internal extensions in your organization's repos + - Discover extensions from trusted third parties +2. **Review** extensions and choose which ones you want to make available +3. **Add** those extension entries to your own `catalog.json` +4. **Team members** can now discover and install them: + - `specify extension search` shows your curated catalog + - `specify extension add ` installs from your catalog + +**Benefits**: Full control over available extensions, team consistency, organizational approval workflow + +**Example**: Copy an entry from `catalog.community.json` to your `catalog.json`, then your team can discover and install it by name. + +### Option 2: Direct URLs (For Ad-hoc Use) + +Skip catalog curation - team members install directly using URLs: + +```bash +specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip +``` + +**Benefits**: Quick for one-off testing or private extensions + +**Tradeoff**: Extensions installed this way won't appear in `specify extension search` for other team members unless you also add them to your `catalog.json`. + +## Available Community Extensions + +The following community-contributed extensions are available in [`catalog.community.json`](catalog.community.json): | Extension | Purpose | URL | |-----------|---------|-----| @@ -11,4 +77,43 @@ Community-contributed extensions for [Spec Kit](https://github.com/github/spec-k ## Adding Your Extension -See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for instructions on how to submit your extension to the community catalog. +### Submission Process + +To add your extension to the community catalog: + +1. **Prepare your extension** following the [Extension Development Guide](EXTENSION-DEVELOPMENT-GUIDE.md) +2. **Create a GitHub release** for your extension +3. **Submit a Pull Request** that: + - Adds your extension to `extensions/catalog.community.json` + - Updates this README with your extension in the Available Extensions table +4. **Wait for review** - maintainers will review and merge if criteria are met + +See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed step-by-step instructions. + +### Submission Checklist + +Before submitting, ensure: + +- āœ… Valid `extension.yml` manifest +- āœ… Complete README with installation and usage instructions +- āœ… LICENSE file included +- āœ… GitHub release created with semantic version (e.g., v1.0.0) +- āœ… Extension tested on a real project +- āœ… All commands working as documented + +## Installing Extensions +Once extensions are available (either in your catalog or via direct URL), install them: + +```bash +# From your curated catalog (by name) +specify extension search # See what's in your catalog +specify extension add # Install by name + +# Direct from URL (bypasses catalog) +specify extension add --from https://github.com///archive/refs/tags/.zip + +# List installed extensions +specify extension list +``` + +For more information, see the [Extension User Guide](EXTENSION-USER-GUIDE.md). diff --git a/extensions/RFC-EXTENSION-SYSTEM.md b/extensions/RFC-EXTENSION-SYSTEM.md index 3bfa0ea060..248e6275aa 100644 --- a/extensions/RFC-EXTENSION-SYSTEM.md +++ b/extensions/RFC-EXTENSION-SYSTEM.md @@ -858,11 +858,41 @@ def should_execute_hook(hook: dict, config: dict) -> bool: ## Extension Discovery & Catalog -### Central Catalog +### Dual Catalog System + +Spec Kit uses two catalog files with different purposes: + +#### User Catalog (`catalog.json`) **URL**: `https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json` -**Format**: +- **Purpose**: Organization's curated catalog of approved extensions +- **Default State**: Empty by design - users populate with extensions they trust +- **Usage**: Default catalog used by `specify extension` CLI commands +- **Control**: Organizations maintain their own fork/version for their teams + +#### Community Reference Catalog (`catalog.community.json`) + +**URL**: `https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json` + +- **Purpose**: Reference catalog of available community-contributed extensions +- **Verification**: Community extensions may have `verified: false` initially +- **Status**: Active - open for community contributions +- **Submission**: Via Pull Request following the Extension Publishing Guide +- **Usage**: Browse to discover extensions, then copy to your `catalog.json` + +**How It Works:** + +1. **Discover**: Browse `catalog.community.json` to find available extensions +2. **Review**: Evaluate extensions for security, quality, and organizational fit +3. **Curate**: Copy approved extension entries from community catalog to your `catalog.json` +4. **Install**: Use `specify extension add ` (pulls from your curated catalog) + +This approach gives organizations full control over which extensions are available to their teams while maintaining a shared community resource for discovery. + +### Catalog Format + +**Format** (same for both catalogs): ```json { @@ -931,25 +961,52 @@ specify extension info jira ### Custom Catalogs -Organizations can host private catalogs: +**āš ļø FUTURE FEATURE - NOT YET IMPLEMENTED** + +The following catalog management commands are proposed design concepts but are not yet available in the current implementation: ```bash -# Add custom catalog +# Add custom catalog (FUTURE - NOT AVAILABLE) specify extension add-catalog https://internal.company.com/spec-kit/catalog.json -# Set as default +# Set as default (FUTURE - NOT AVAILABLE) specify extension set-catalog --default https://internal.company.com/spec-kit/catalog.json -# List catalogs +# List catalogs (FUTURE - NOT AVAILABLE) specify extension catalogs ``` -**Catalog priority**: +**Proposed catalog priority** (future design): -1. Project-specific catalog (`.specify/extension-catalogs.yml`) -2. User-level catalog (`~/.specify/extension-catalogs.yml`) +1. Project-specific catalog (`.specify/extension-catalogs.yml`) - *not implemented* +2. User-level catalog (`~/.specify/extension-catalogs.yml`) - *not implemented* 3. Default GitHub catalog +#### Current Implementation: SPECKIT_CATALOG_URL + +**The currently available method** for using custom catalogs is the `SPECKIT_CATALOG_URL` environment variable: + +```bash +# Point to your organization's catalog +export SPECKIT_CATALOG_URL="https://internal.company.com/spec-kit/catalog.json" + +# All extension commands now use your custom catalog +specify extension search # Uses custom catalog +specify extension add jira # Installs from custom catalog +``` + +**Requirements:** +- URL must use HTTPS (HTTP only allowed for localhost testing) +- Catalog must follow the standard catalog.json schema +- Must be publicly accessible or accessible within your network + +**Example for testing:** +```bash +# Test with localhost during development +export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json" +specify extension search +``` + --- ## CLI Commands diff --git a/pyproject.toml b/pyproject.toml index 5f6a2eb7ab..75c0c8ced1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "specify-cli" -version = "0.1.6" +version = "0.1.8" description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)." requires-python = ">=3.11" dependencies = [ @@ -50,5 +50,3 @@ omit = ["*/tests/*", "*/__pycache__/*"] precision = 2 show_missing = true skip_covered = false - - diff --git a/scripts/bash/update-agent-context.sh b/scripts/bash/update-agent-context.sh index a33ea5cdee..fdebac65f7 100644 --- a/scripts/bash/update-agent-context.sh +++ b/scripts/bash/update-agent-context.sh @@ -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, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Amazon Q Developer CLI, or Antigravity +# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Kiro CLI, or Antigravity # - 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|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli +# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli # Leave empty to update all existing agent files set -e @@ -73,7 +73,7 @@ CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" QODER_FILE="$REPO_ROOT/QODER.md" AMP_FILE="$REPO_ROOT/AGENTS.md" SHAI_FILE="$REPO_ROOT/SHAI.md" -Q_FILE="$REPO_ROOT/AGENTS.md" +KIRO_FILE="$REPO_ROOT/AGENTS.md" AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md" BOB_FILE="$REPO_ROOT/AGENTS.md" @@ -351,10 +351,19 @@ create_new_agent_file() { # Convert \n sequences to actual newlines newline=$(printf '\n') sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" - + # Clean up backup files rm -f "$temp_file.bak" "$temp_file.bak2" - + + # Prepend Cursor frontmatter for .mdc files so rules are auto-included + if [[ "$target_file" == *.mdc ]]; then + local frontmatter_file + frontmatter_file=$(mktemp) || return 1 + printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" + cat "$temp_file" >> "$frontmatter_file" + mv "$frontmatter_file" "$temp_file" + fi + return 0 } @@ -492,13 +501,24 @@ update_existing_agent_file() { changes_entries_added=true fi + # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion + if [[ "$target_file" == *.mdc ]]; then + if ! head -1 "$temp_file" | grep -q '^---'; then + local frontmatter_file + frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; } + printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" + cat "$temp_file" >> "$frontmatter_file" + mv "$frontmatter_file" "$temp_file" + fi + fi + # Move temp file to target atomically if ! mv "$temp_file" "$target_file"; then log_error "Failed to update target file" rm -f "$temp_file" return 1 fi - + return 0 } #============================================================================== @@ -628,8 +648,8 @@ update_specific_agent() { shai) update_agent_file "$SHAI_FILE" "SHAI" ;; - q) - update_agent_file "$Q_FILE" "Amazon Q Developer CLI" + kiro-cli) + update_agent_file "$KIRO_FILE" "Kiro CLI" ;; agy) update_agent_file "$AGY_FILE" "Antigravity" @@ -642,7 +662,7 @@ update_specific_agent() { ;; *) log_error "Unknown agent type '$agent_type'" - log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic" + log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic" exit 1 ;; esac @@ -717,8 +737,8 @@ update_all_existing_agents() { found_agent=true fi - if [[ -f "$Q_FILE" ]]; then - update_agent_file "$Q_FILE" "Amazon Q Developer CLI" + if [[ -f "$KIRO_FILE" ]]; then + update_agent_file "$KIRO_FILE" "Kiro CLI" found_agent=true fi @@ -755,7 +775,7 @@ print_summary() { echo - log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli]" + log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli]" } #============================================================================== @@ -807,4 +827,3 @@ main() { if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi - diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 index 61718e96ce..02ce102057 100644 --- a/scripts/powershell/update-agent-context.ps1 +++ b/scripts/powershell/update-agent-context.ps1 @@ -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, kilocode, auggie, roo, codebuddy, amp, shai, q, agy, bob, qodercli) + 5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, kiro-cli, agy, bob, qodercli) .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','kilocode','auggie','roo','codebuddy','amp','shai','q','agy','bob','qodercli','generic')] + [ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','kiro-cli','agy','bob','qodercli','generic')] [string]$AgentType ) @@ -58,7 +58,7 @@ $CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md' $QODER_FILE = Join-Path $REPO_ROOT 'QODER.md' $AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md' -$Q_FILE = Join-Path $REPO_ROOT 'AGENTS.md' +$KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md' $BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md' @@ -258,6 +258,12 @@ function New-AgentFile { # Convert literal \n sequences introduced by Escape to real newlines $content = $content -replace '\\n',[Environment]::NewLine + # Prepend Cursor frontmatter for .mdc files so rules are auto-included + if ($TargetFile -match '\.mdc$') { + $frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') -join [Environment]::NewLine + $content = $frontmatter + $content + } + $parent = Split-Path -Parent $TargetFile if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null } Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8 @@ -334,6 +340,12 @@ function Update-ExistingAgentFile { $newTechEntries | ForEach-Object { $output.Add($_) } } + # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion + if ($TargetFile -match '\.mdc$' -and $output.Count -gt 0 -and $output[0] -ne '---') { + $frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') + $output.InsertRange(0, $frontmatter) + } + Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8 return $true } @@ -387,11 +399,11 @@ function Update-SpecificAgent { 'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' } 'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' } 'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' } - 'q' { Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI' } + 'kiro-cli' { Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI' } 'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' } 'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' } '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|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic'; return $false } + default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic'; return $false } } } @@ -411,7 +423,7 @@ function Update-AllExistingAgents { if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true } if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true } if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true } - if (Test-Path $Q_FILE) { if (-not (Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI')) { $ok = $false }; $found = $true } + if (Test-Path $KIRO_FILE) { if (-not (Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI')) { $ok = $false }; $found = $true } if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true } if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true } if (-not $found) { @@ -428,7 +440,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|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic]' + Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic]' } function Main { @@ -449,4 +461,3 @@ function Main { } Main - diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 5651ac7226..17273ea8c7 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -216,11 +216,11 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) "install_url": None, # IDE-based "requires_cli": False, }, - "q": { - "name": "Amazon Q Developer CLI", - "folder": ".amazonq/", + "kiro-cli": { + "name": "Kiro CLI", + "folder": ".kiro/", "commands_subdir": "prompts", # Special: uses prompts/ not commands/ - "install_url": "https://aws.amazon.com/developer/learning/q-developer-cli/", + "install_url": "https://kiro.dev/docs/cli/", "requires_cli": True, }, "amp": { @@ -260,6 +260,23 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) }, } +AI_ASSISTANT_ALIASES = { + "kiro": "kiro-cli", +} + +def _build_ai_assistant_help() -> str: + """Build the --ai help text from AGENT_CONFIG so it stays in sync with runtime config.""" + + non_generic_agents = [agent for agent in AGENT_CONFIG if agent != "generic"] + return ( + f"AI assistant to use: {', '.join(non_generic_agents)}, " + "or generic (requires --ai-commands-dir). " + "Use 'kiro' as an alias for 'kiro-cli'." + ) + + +AI_ASSISTANT_HELP = _build_ai_assistant_help() + SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude" @@ -534,7 +551,12 @@ def check_tool(tool: str, tracker: StepTracker = None) -> bool: tracker.complete(tool, "available") return True - found = shutil.which(tool) is not None + if tool == "kiro-cli": + # Kiro currently supports both executable names. Prefer kiro-cli and + # accept kiro as a compatibility fallback. + found = shutil.which("kiro-cli") is not None or shutil.which("kiro") is not None + else: + found = shutil.which(tool) is not None if tracker: if found: @@ -1214,7 +1236,7 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker @app.command() def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"), - ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, q, agy, bob, qodercli, or generic (requires --ai-commands-dir)"), + ai_assistant: str = typer.Option(None, "--ai", help=AI_ASSISTANT_HELP), ai_commands_dir: str = typer.Option(None, "--ai-commands-dir", help="Directory for agent command files (required with --ai generic, e.g. .myagent/commands/)"), script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"), ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"), @@ -1270,6 +1292,9 @@ def init( console.print("[yellow]Example:[/yellow] specify init --ai generic --ai-commands-dir .myagent/commands/") raise typer.Exit(1) + if ai_assistant: + ai_assistant = AI_ASSISTANT_ALIASES.get(ai_assistant, ai_assistant) + if project_name == ".": here = True project_name = None # Clear project_name to use existing validation logic @@ -1464,8 +1489,9 @@ def init( if skills_ok and not here: agent_cfg = AGENT_CONFIG.get(selected_ai, {}) agent_folder = agent_cfg.get("folder", "") + commands_subdir = agent_cfg.get("commands_subdir", "commands") if agent_folder: - cmds_dir = project_path / agent_folder.rstrip("/") / "commands" + cmds_dir = project_path / agent_folder.rstrip("/") / commands_subdir if cmds_dir.exists(): try: shutil.rmtree(cmds_dir) @@ -2350,4 +2376,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index b8881e7c89..5a90b4d58d 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -653,8 +653,8 @@ class CommandRegistrar: "args": "$ARGUMENTS", "extension": ".md" }, - "q": { - "dir": ".amazonq/prompts", + "kiro-cli": { + "dir": ".kiro/prompts", "format": "markdown", "args": "$ARGUMENTS", "extension": ".md" @@ -1782,4 +1782,3 @@ def disable_hooks(self, extension_id: str): self.save_project_config(config) - diff --git a/tests/test_agent_config_consistency.py b/tests/test_agent_config_consistency.py new file mode 100644 index 0000000000..5296a7cb4f --- /dev/null +++ b/tests/test_agent_config_consistency.py @@ -0,0 +1,97 @@ +"""Consistency checks for agent configuration across runtime and packaging scripts.""" + +import re +from pathlib import Path + +from specify_cli import AGENT_CONFIG, AI_ASSISTANT_HELP +from specify_cli.extensions import CommandRegistrar + + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +class TestAgentConfigConsistency: + """Ensure kiro-cli migration stays synchronized across key surfaces.""" + + def test_runtime_config_uses_kiro_cli_and_removes_q(self): + """AGENT_CONFIG should include kiro-cli and exclude legacy q.""" + assert "kiro-cli" in AGENT_CONFIG + assert AGENT_CONFIG["kiro-cli"]["folder"] == ".kiro/" + assert AGENT_CONFIG["kiro-cli"]["commands_subdir"] == "prompts" + assert "q" not in AGENT_CONFIG + + def test_extension_registrar_uses_kiro_cli_and_removes_q(self): + """Extension command registrar should target .kiro/prompts.""" + cfg = CommandRegistrar.AGENT_CONFIGS + + assert "kiro-cli" in cfg + assert cfg["kiro-cli"]["dir"] == ".kiro/prompts" + assert "q" not in cfg + + def test_release_agent_lists_include_kiro_cli_and_exclude_q(self): + """Bash and PowerShell release scripts should agree on agent key set for Kiro.""" + sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8") + ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8") + + sh_match = re.search(r"ALL_AGENTS=\(([^)]*)\)", sh_text) + assert sh_match is not None + sh_agents = sh_match.group(1).split() + + ps_match = re.search(r"\$AllAgents = @\(([^)]*)\)", ps_text) + assert ps_match is not None + ps_agents = re.findall(r"'([^']+)'", ps_match.group(1)) + + assert "kiro-cli" in sh_agents + assert "kiro-cli" in ps_agents + assert "shai" in sh_agents + assert "shai" in ps_agents + assert "agy" in sh_agents + assert "agy" in ps_agents + assert "q" not in sh_agents + assert "q" not in ps_agents + + def test_release_ps_switch_has_shai_and_agy_generation(self): + """PowerShell release builder must generate files for shai and agy agents.""" + ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8") + + assert re.search(r"'shai'\s*\{.*?\.shai/commands", ps_text, re.S) is not None + assert re.search(r"'agy'\s*\{.*?\.agent/workflows", ps_text, re.S) is not None + + def test_init_ai_help_includes_roo_and_kiro_alias(self): + """CLI help text for --ai should stay in sync with agent config and alias guidance.""" + assert "roo" in AI_ASSISTANT_HELP + assert "Use 'kiro' as an alias for 'kiro-cli'." in AI_ASSISTANT_HELP + + 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") + + 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_release_output_targets_kiro_prompt_dir(self): + """Packaging and release scripts should no longer emit amazonq artifacts.""" + sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8") + ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8") + gh_release_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-github-release.sh").read_text(encoding="utf-8") + + assert ".kiro/prompts" in sh_text + assert ".kiro/prompts" in ps_text + assert ".amazonq/prompts" not in sh_text + assert ".amazonq/prompts" not in ps_text + + assert "spec-kit-template-kiro-cli-sh-" in gh_release_text + assert "spec-kit-template-kiro-cli-ps-" in gh_release_text + assert "spec-kit-template-q-sh-" not in gh_release_text + assert "spec-kit-template-q-ps-" not in gh_release_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") + + assert "kiro-cli" in bash_text + assert "kiro-cli" in pwsh_text + assert "Amazon Q Developer CLI" not in bash_text + assert "Amazon Q Developer CLI" not in pwsh_text diff --git a/tests/test_ai_skills.py b/tests/test_ai_skills.py index 3eec4a419c..59f13bd5ee 100644 --- a/tests/test_ai_skills.py +++ b/tests/test_ai_skills.py @@ -162,6 +162,11 @@ def test_cursor_agent_skills_dir(self, project_dir): result = _get_skills_dir(project_dir, "cursor-agent") assert result == project_dir / ".cursor" / "skills" + def test_kiro_cli_skills_dir(self, project_dir): + """Kiro CLI should use .kiro/skills/.""" + result = _get_skills_dir(project_dir, "kiro-cli") + assert result == project_dir / ".kiro" / "skills" + def test_unknown_agent_uses_default(self, project_dir): """Unknown agents should fall back to DEFAULT_SKILLS_DIR.""" result = _get_skills_dir(project_dir, "nonexistent-agent") @@ -460,8 +465,9 @@ def _fake_extract(self, agent, project_path, **_kwargs): """Simulate template extraction: create agent commands dir.""" agent_cfg = AGENT_CONFIG.get(agent, {}) agent_folder = agent_cfg.get("folder", "") + commands_subdir = agent_cfg.get("commands_subdir", "commands") if agent_folder: - cmds_dir = project_path / agent_folder.rstrip("/") / "commands" + cmds_dir = project_path / agent_folder.rstrip("/") / commands_subdir cmds_dir.mkdir(parents=True, exist_ok=True) (cmds_dir / "speckit.specify.md").write_text("# spec") @@ -483,6 +489,7 @@ def fake_download(project_path, *args, **kwargs): patch("specify_cli.shutil.which", return_value="/usr/bin/git"): result = runner.invoke(app, ["init", str(target), "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"]) + assert result.exit_code == 0 # Skills should have been called mock_skills.assert_called_once() @@ -490,6 +497,30 @@ def fake_download(project_path, *args, **kwargs): cmds_dir = target / ".claude" / "commands" assert not cmds_dir.exists() + def test_new_project_nonstandard_commands_subdir_removed_after_skills_succeed(self, tmp_path): + """For non-standard agents, configured commands_subdir should be removed on success.""" + from typer.testing import CliRunner + + runner = CliRunner() + target = tmp_path / "new-kiro-proj" + + def fake_download(project_path, *args, **kwargs): + self._fake_extract("kiro-cli", project_path) + + with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \ + patch("specify_cli.ensure_executable_scripts"), \ + patch("specify_cli.ensure_constitution_from_template"), \ + patch("specify_cli.install_ai_skills", return_value=True) as mock_skills, \ + patch("specify_cli.is_git_repo", return_value=False), \ + patch("specify_cli.shutil.which", return_value="/usr/bin/git"): + result = runner.invoke(app, ["init", str(target), "--ai", "kiro-cli", "--ai-skills", "--script", "sh", "--no-git"]) + + assert result.exit_code == 0 + mock_skills.assert_called_once() + + prompts_dir = target / ".kiro" / "prompts" + assert not prompts_dir.exists() + def test_commands_preserved_when_skills_fail(self, tmp_path): """If skills fail, commands should NOT be removed (safety net).""" from typer.testing import CliRunner @@ -508,6 +539,7 @@ def fake_download(project_path, *args, **kwargs): patch("specify_cli.shutil.which", return_value="/usr/bin/git"): result = runner.invoke(app, ["init", str(target), "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"]) + assert result.exit_code == 0 # Commands should still exist since skills failed cmds_dir = target / ".claude" / "commands" assert cmds_dir.exists() @@ -538,8 +570,9 @@ def fake_download(project_path, *args, **kwargs): patch("specify_cli.install_ai_skills", return_value=True), \ patch("specify_cli.is_git_repo", return_value=True), \ patch("specify_cli.shutil.which", return_value="/usr/bin/git"): - result = runner.invoke(app, ["init", "--here", "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"]) + result = runner.invoke(app, ["init", "--here", "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"], input="y\n") + assert result.exit_code == 0 # Commands must remain for --here assert cmds_dir.exists() assert (cmds_dir / "speckit.specify.md").exists() @@ -631,6 +664,42 @@ def test_ai_skills_flag_appears_in_help(self): assert "--ai-skills" in plain assert "agent skills" in plain.lower() + def test_kiro_alias_normalized_to_kiro_cli(self, tmp_path): + """--ai kiro should normalize to canonical kiro-cli agent key.""" + from typer.testing import CliRunner + + runner = CliRunner() + target = tmp_path / "kiro-alias-proj" + + with patch("specify_cli.download_and_extract_template") as mock_download, \ + patch("specify_cli.ensure_executable_scripts"), \ + patch("specify_cli.ensure_constitution_from_template"), \ + patch("specify_cli.is_git_repo", return_value=False), \ + patch("specify_cli.shutil.which", return_value="/usr/bin/git"): + result = runner.invoke( + app, + [ + "init", + str(target), + "--ai", + "kiro", + "--ignore-agent-tools", + "--script", + "sh", + "--no-git", + ], + ) + + assert result.exit_code == 0 + assert mock_download.called + # download_and_extract_template(project_path, ai_assistant, script_type, ...) + assert mock_download.call_args.args[1] == "kiro-cli" + + def test_q_removed_from_agent_config(self): + """Amazon Q legacy key should not remain in AGENT_CONFIG.""" + assert "q" not in AGENT_CONFIG + assert "kiro-cli" in AGENT_CONFIG + class TestParameterOrderingIssue: """Test fix for GitHub issue #1641: parameter ordering issues.""" diff --git a/tests/test_cursor_frontmatter.py b/tests/test_cursor_frontmatter.py new file mode 100644 index 0000000000..d9d0e34237 --- /dev/null +++ b/tests/test_cursor_frontmatter.py @@ -0,0 +1,263 @@ +""" +Tests for Cursor .mdc frontmatter generation (issue #669). + +Verifies that update-agent-context.sh properly prepends YAML frontmatter +to .mdc files so that Cursor IDE auto-includes the rules. +""" + +import os +import shutil +import subprocess +import textwrap + +import pytest + +SCRIPT_PATH = os.path.join( + os.path.dirname(__file__), + os.pardir, + "scripts", + "bash", + "update-agent-context.sh", +) + +EXPECTED_FRONTMATTER_LINES = [ + "---", + "description: Project Development Guidelines", + 'globs: ["**/*"]', + "alwaysApply: true", + "---", +] + +requires_git = pytest.mark.skipif( + shutil.which("git") is None, + reason="git is not installed", +) + + +class TestScriptFrontmatterPattern: + """Static analysis — no git required.""" + + def test_create_new_has_mdc_frontmatter_logic(self): + """create_new_agent_file() must contain .mdc frontmatter logic.""" + with open(SCRIPT_PATH, encoding="utf-8") as f: + content = f.read() + assert 'if [[ "$target_file" == *.mdc ]]' in content + assert "alwaysApply: true" in content + + def test_update_existing_has_mdc_frontmatter_logic(self): + """update_existing_agent_file() must also handle .mdc frontmatter.""" + with open(SCRIPT_PATH, encoding="utf-8") as f: + content = f.read() + # There should be two occurrences of the .mdc check — one per function + occurrences = content.count('if [[ "$target_file" == *.mdc ]]') + assert occurrences >= 2, ( + f"Expected at least 2 .mdc frontmatter checks, found {occurrences}" + ) + + def test_powershell_script_has_mdc_frontmatter_logic(self): + """PowerShell script must also handle .mdc frontmatter.""" + ps_path = os.path.join( + os.path.dirname(__file__), + os.pardir, + "scripts", + "powershell", + "update-agent-context.ps1", + ) + with open(ps_path, encoding="utf-8") as f: + content = f.read() + assert "alwaysApply: true" in content + occurrences = content.count(r"\.mdc$") + assert occurrences >= 2, ( + f"Expected at least 2 .mdc frontmatter checks in PS script, found {occurrences}" + ) + + +@requires_git +class TestCursorFrontmatterIntegration: + """Integration tests using a real git repo.""" + + @pytest.fixture + def git_repo(self, tmp_path): + """Create a minimal git repo with the spec-kit structure.""" + repo = tmp_path / "repo" + repo.mkdir() + + # Init git repo + subprocess.run( + ["git", "init"], cwd=str(repo), capture_output=True, check=True + ) + subprocess.run( + ["git", "config", "user.email", "test@test.com"], + cwd=str(repo), + capture_output=True, + check=True, + ) + subprocess.run( + ["git", "config", "user.name", "Test"], + cwd=str(repo), + capture_output=True, + check=True, + ) + + # Create .specify dir with config + specify_dir = repo / ".specify" + specify_dir.mkdir() + (specify_dir / "config.yaml").write_text( + textwrap.dedent("""\ + project_type: webapp + language: python + framework: fastapi + database: N/A + """) + ) + + # Create template + templates_dir = specify_dir / "templates" + templates_dir.mkdir() + (templates_dir / "agent-file-template.md").write_text( + "# [PROJECT NAME] Development Guidelines\n\n" + "Auto-generated from all feature plans. Last updated: [DATE]\n\n" + "## Active Technologies\n\n" + "[EXTRACTED FROM ALL PLAN.MD FILES]\n\n" + "## Project Structure\n\n" + "[ACTUAL STRUCTURE FROM PLANS]\n\n" + "## Development Commands\n\n" + "[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]\n\n" + "## Coding Conventions\n\n" + "[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]\n\n" + "## Recent Changes\n\n" + "[LAST 3 FEATURES AND WHAT THEY ADDED]\n" + ) + + # Create initial commit + subprocess.run( + ["git", "add", "-A"], cwd=str(repo), capture_output=True, check=True + ) + subprocess.run( + ["git", "commit", "-m", "init"], + cwd=str(repo), + capture_output=True, + check=True, + ) + + # Create a feature branch so CURRENT_BRANCH detection works + subprocess.run( + ["git", "checkout", "-b", "001-test-feature"], + cwd=str(repo), + capture_output=True, + check=True, + ) + + # Create a spec so the script detects the feature + spec_dir = repo / "specs" / "001-test-feature" + spec_dir.mkdir(parents=True) + (spec_dir / "plan.md").write_text( + "# Test Feature Plan\n\n" + "## Technology Stack\n\n" + "- Language: Python\n" + "- Framework: FastAPI\n" + ) + + return repo + + def _run_update(self, repo, agent_type="cursor-agent"): + """Run update-agent-context.sh for a specific agent type.""" + script = os.path.abspath(SCRIPT_PATH) + result = subprocess.run( + ["bash", script, agent_type], + cwd=str(repo), + capture_output=True, + text=True, + timeout=30, + ) + return result + + def test_new_mdc_file_has_frontmatter(self, git_repo): + """Creating a new .mdc file must include YAML frontmatter.""" + result = self._run_update(git_repo) + assert result.returncode == 0, f"Script failed: {result.stderr}" + + mdc_file = git_repo / ".cursor" / "rules" / "specify-rules.mdc" + assert mdc_file.exists(), "Cursor .mdc file was not created" + + content = mdc_file.read_text() + lines = content.splitlines() + + # First line must be the opening --- + assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}" + + # Check all frontmatter lines are present + for expected in EXPECTED_FRONTMATTER_LINES: + assert expected in content, f"Missing frontmatter line: {expected}" + + # Content after frontmatter should be the template content + assert "Development Guidelines" in content + + def test_existing_mdc_without_frontmatter_gets_it_added(self, git_repo): + """Updating an existing .mdc file that lacks frontmatter must add it.""" + # First, create the file WITHOUT frontmatter (simulating pre-fix state) + cursor_dir = git_repo / ".cursor" / "rules" + cursor_dir.mkdir(parents=True, exist_ok=True) + mdc_file = cursor_dir / "specify-rules.mdc" + mdc_file.write_text( + "# repo Development Guidelines\n\n" + "Auto-generated from all feature plans. Last updated: 2025-01-01\n\n" + "## Active Technologies\n\n" + "- Python + FastAPI (main)\n\n" + "## Recent Changes\n\n" + "- main: Added Python + FastAPI\n" + ) + + result = self._run_update(git_repo) + assert result.returncode == 0, f"Script failed: {result.stderr}" + + content = mdc_file.read_text() + lines = content.splitlines() + + assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}" + for expected in EXPECTED_FRONTMATTER_LINES: + assert expected in content, f"Missing frontmatter line: {expected}" + + def test_existing_mdc_with_frontmatter_not_duplicated(self, git_repo): + """Updating an .mdc file that already has frontmatter must not duplicate it.""" + cursor_dir = git_repo / ".cursor" / "rules" + cursor_dir.mkdir(parents=True, exist_ok=True) + mdc_file = cursor_dir / "specify-rules.mdc" + + frontmatter = ( + "---\n" + "description: Project Development Guidelines\n" + 'globs: ["**/*"]\n' + "alwaysApply: true\n" + "---\n\n" + ) + body = ( + "# repo Development Guidelines\n\n" + "Auto-generated from all feature plans. Last updated: 2025-01-01\n\n" + "## Active Technologies\n\n" + "- Python + FastAPI (main)\n\n" + "## Recent Changes\n\n" + "- main: Added Python + FastAPI\n" + ) + mdc_file.write_text(frontmatter + body) + + result = self._run_update(git_repo) + assert result.returncode == 0, f"Script failed: {result.stderr}" + + content = mdc_file.read_text() + # Count occurrences of the frontmatter delimiter + assert content.count("alwaysApply: true") == 1, ( + "Frontmatter was duplicated" + ) + + def test_non_mdc_file_has_no_frontmatter(self, git_repo): + """Non-.mdc agent files (e.g., Claude) must NOT get frontmatter.""" + result = self._run_update(git_repo, agent_type="claude") + assert result.returncode == 0, f"Script failed: {result.stderr}" + + claude_file = git_repo / ".claude" / "CLAUDE.md" + if claude_file.exists(): + content = claude_file.read_text() + assert not content.startswith("---"), ( + "Non-mdc file should not have frontmatter" + ) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index a2c4121ed4..e6849729e3 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -399,6 +399,12 @@ def test_config_backup_on_remove(self, extension_dir, project_dir): class TestCommandRegistrar: """Test CommandRegistrar command registration.""" + def test_kiro_cli_agent_config_present(self): + """Kiro CLI should be mapped to .kiro/prompts and legacy q removed.""" + assert "kiro-cli" in CommandRegistrar.AGENT_CONFIGS + assert CommandRegistrar.AGENT_CONFIGS["kiro-cli"]["dir"] == ".kiro/prompts" + assert "q" not in CommandRegistrar.AGENT_CONFIGS + def test_parse_frontmatter_valid(self): """Test parsing valid YAML frontmatter.""" content = """---