Compare commits

..

12 Commits

Author SHA1 Message Date
Roland Huß
f8da535d71 feat(scripts): add --allow-existing-branch flag to create-new-feature (#1999)
* feat(scripts): add --allow-existing-branch flag to create-new-feature

Add an --allow-existing-branch / -AllowExistingBranch flag to both
bash and PowerShell create-new-feature scripts. When the target branch
already exists, the script switches to it instead of failing. The spec
directory and template are still created if missing, but existing
spec.md files are not overwritten (prevents data loss on re-runs).

The flag is opt-in, so existing behavior is completely unchanged
without it. This enables worktree-based workflows and CI/CD pipelines
that create branches externally before running speckit.specify.

Relates to #1931. Also addresses #1680, #841, #1921.

Assisted-By: 🤖 Claude Code

* fix: address PR review feedback for allow-existing-branch

- Make checkout failure fatal instead of suppressing with || true (bash)
- Check $LASTEXITCODE after git checkout in PowerShell
- Use Test-Path -PathType Leaf for spec file existence check (PS)
- Add PowerShell static assertion test for -AllowExistingBranch flag

Assisted-By: 🤖 Claude Code
2026-03-27 14:04:14 -05:00
Alexander Rampp
edaa5a7ff1 fix(scripts): add correct path for copilot-instructions.md (#1997) 2026-03-27 11:43:57 -05:00
PChemGuy
5be705e414 Update README.md (#1995)
Thank you!
2026-03-27 11:14:11 -05:00
Andrii Furmanets
796b4f47c4 fix: prevent extension command shadowing (#1994)
* fix: prevent extension command shadowing

* Validate extension command namespaces

* Reuse extension command name pattern
2026-03-27 10:55:26 -05:00
Kash
6b1f45c50c Fix Claude Code CLI detection for npm-local installs (#1978)
* Fix Claude Code CLI detection for npm-local installs

`specify check` reports "Claude Code CLI (not found)" for users who
installed Claude Code via npm-local (the default installer path, common
with nvm). The binary lives at ~/.claude/local/node_modules/.bin/claude
which was not checked. Add CLAUDE_NPM_LOCAL_PATH as a second well-known
location alongside the existing migrate-installer path.

Fixes https://github.com/github/spec-kit/issues/550

* Address Copilot review feedback

- Remove unused pytest import from test_check_tool.py
- Use tmp_path instead of hardcoded /nonexistent/claude for hermetic tests
- Simplify redundant exists() + is_file() to just is_file()

AI-assisted: Changes applied with Claude Code.

* Update tests/test_check_tool.py

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

* Update tests/test_check_tool.py

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-27 10:50:43 -05:00
AK
8778c26dcf fix(scripts): honor PowerShell agent and script filters (#1969)
Rename the Normalize-List parameter in create-release-packages.ps1 to avoid conflicting with PowerShell's automatic $input variable. This fixes Windows offline scaffolding when -Agents and -Scripts are passed.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 10:48:11 -05:00
Daniel Badde
41d1f4b0ac feat: add MAQA extension suite (7 extensions) to community catalog (#1981)
* feat: add MAQA extension suite to community catalog and README

Adds 7 extensions forming the MAQA (Multi-Agent & Quality Assurance)
suite to catalog.community.json in correct alphabetical order (after
'learn', before 'onboard') and to the README community extensions table:

- maqa           — coordinator/feature/QA workflow, board auto-detection
- maqa-azure-devops — Azure DevOps Boards integration
- maqa-ci           — CI/CD gate (GitHub Actions/CircleCI/GitLab/Bitbucket)
- maqa-github-projects — GitHub Projects v2 integration
- maqa-jira         — Jira integration
- maqa-linear       — Linear integration
- maqa-trello       — Trello integration

All entries placed alphabetically. maqa v0.1.3 bumped to reflect
multi-board auto-detection added in this release.

* fix: set catalog updated_at to match latest entry timestamp

Top-level updated_at was 00:00:00Z while plan-review-gate entries
had 08:22:30Z, making metadata inconsistent for freshness consumers.
Updated to 2026-03-27T08:22:30Z (>= all entry timestamps).
2026-03-27 10:45:19 -05:00
Rafael Sales
9c2481fd67 feat: add spec-kit-onboard extension to community catalog (#1991)
Adds the onboard extension (v2.1.0) — contextual onboarding and
progressive growth for developers new to spec-kit projects.

- 7 commands: start, explain, trail, quiz, badge, mentor, team
- 3 hooks: after-implement, before-implement, after-explain
- Repository: https://github.com/dmux/spec-kit-onboard
2026-03-27 09:58:43 -05:00
Ed Harrod
8520241dfe Add plan-review-gate to community catalog (#1993)
- Extension ID: plan-review-gate
- Version: 1.0.0
- Author: luno
- Catalog entries sorted alphabetically by ID
- README table row inserted alphabetically by name

Co-authored-by: Ed Harrod <your-real-email@luno.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 08:23:01 -05:00
dependabot[bot]
362868a342 chore(deps): bump actions/deploy-pages from 4 to 5 (#1990)
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:03:34 -05:00
dependabot[bot]
d7206126e0 chore(deps): bump DavidAnson/markdownlint-cli2-action from 19 to 23 (#1989)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 19 to 23.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v19...v23)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: '23'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:02:40 -05:00
Manfred Riem
b22f381c0d chore: bump version to 0.4.3 (#1986)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-26 16:04:09 -05:00
19 changed files with 1081 additions and 161 deletions

View File

@@ -64,5 +64,5 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v4 uses: actions/deploy-pages@v5

View File

@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Run markdownlint-cli2 - name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@v19 uses: DavidAnson/markdownlint-cli2-action@v23
with: with:
globs: | globs: |
'**/*.md' '**/*.md'

View File

@@ -497,13 +497,13 @@ $AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode'
$AllScripts = @('sh', 'ps') $AllScripts = @('sh', 'ps')
function Normalize-List { function Normalize-List {
param([string]$Input) param([string]$Value)
if ([string]::IsNullOrEmpty($Input)) { if ([string]::IsNullOrEmpty($Value)) {
return @() return @()
} }
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique $items = $Value -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
return $items return $items
} }
@@ -526,7 +526,7 @@ function Validate-Subset {
# Determine agent list # Determine agent list
if (-not [string]::IsNullOrEmpty($Agents)) { if (-not [string]::IsNullOrEmpty($Agents)) {
$AgentList = Normalize-List -Input $Agents $AgentList = Normalize-List -Value $Agents
if (-not (Validate-Subset -Type 'agent' -Allowed $AllAgents -Items $AgentList)) { if (-not (Validate-Subset -Type 'agent' -Allowed $AllAgents -Items $AgentList)) {
exit 1 exit 1
} }
@@ -536,7 +536,7 @@ if (-not [string]::IsNullOrEmpty($Agents)) {
# Determine script list # Determine script list
if (-not [string]::IsNullOrEmpty($Scripts)) { if (-not [string]::IsNullOrEmpty($Scripts)) {
$ScriptList = Normalize-List -Input $Scripts $ScriptList = Normalize-List -Value $Scripts
if (-not (Validate-Subset -Type 'script' -Allowed $AllScripts -Items $ScriptList)) { if (-not (Validate-Subset -Type 'script' -Allowed $AllScripts -Items $ScriptList)) {
exit 1 exit 1
} }

View File

@@ -162,9 +162,18 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
The following community-contributed extensions are available in [`catalog.community.json`](extensions/catalog.community.json): The following community-contributed extensions are available in [`catalog.community.json`](extensions/catalog.community.json):
**Categories:** `docs` — reads, validates, or generates spec artifacts · `code` — reviews, validates, or modifies source code · `process` — orchestrates workflow across phases · `integration` — syncs with external platforms · `visibility` — reports on project health or progress **Categories:**
**Effect:** `Read-only` — produces reports without modifying files · `Read+Write` — modifies files, creates artifacts, or updates specs - `docs` — reads, validates, or generates spec artifacts
- `code` — reviews, validates, or modifies source code
- `process` — orchestrates workflow across phases
- `integration` — syncs with external platforms
- `visibility` — reports on project health or progress
**Effect:**
- `Read-only` — produces reports without modifying files
- `Read+Write` — modifies files, creates artifacts, or updates specs
| Extension | Purpose | Category | Effect | URL | | Extension | Purpose | Category | Effect | URL |
|-----------|---------|----------|--------|-----| |-----------|---------|----------|--------|-----|
@@ -181,6 +190,15 @@ The following community-contributed extensions are available in [`catalog.commun
| 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) | | 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) | | 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) | | 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) |
| MAQA — Multi-Agent & Quality Assurance | Coordinator → feature → QA agent workflow with parallel worktree-based implementation. Language-agnostic. Auto-detects installed board plugins. Optional CI gate. | `process` | Read+Write | [spec-kit-maqa-ext](https://github.com/GenieRobot/spec-kit-maqa-ext) |
| MAQA Azure DevOps Integration | Azure DevOps Boards integration for MAQA — syncs User Stories and Task children as features progress | `integration` | Read+Write | [spec-kit-maqa-azure-devops](https://github.com/GenieRobot/spec-kit-maqa-azure-devops) |
| MAQA CI/CD Gate | Auto-detects GitHub Actions, CircleCI, GitLab CI, and Bitbucket Pipelines. Blocks QA handoff until pipeline is green. | `process` | Read+Write | [spec-kit-maqa-ci](https://github.com/GenieRobot/spec-kit-maqa-ci) |
| MAQA GitHub Projects Integration | GitHub Projects v2 integration for MAQA — syncs draft issues and Status columns as features progress | `integration` | Read+Write | [spec-kit-maqa-github-projects](https://github.com/GenieRobot/spec-kit-maqa-github-projects) |
| 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) |
| 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) |
| 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) |
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) | | Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
| 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) | | 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) |
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) | | Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |

View File

@@ -44,7 +44,7 @@ provides:
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$ - name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
file: string # Required, relative path to command file file: string # Required, relative path to command file
description: string # Required description: string # Required
aliases: [string] # Optional, array of alternate names aliases: [string] # Optional, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands
config: # Optional, array of config files config: # Optional, array of config files
- name: string # Config file name - name: string # Config file name

View File

@@ -41,7 +41,7 @@ provides:
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd} - name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
file: "commands/hello.md" file: "commands/hello.md"
description: "Say hello" description: "Say hello"
aliases: ["speckit.hello"] # Optional aliases aliases: ["speckit.my-ext.hi"] # Optional aliases, same pattern
config: # Optional: Config files config: # Optional: Config files
- name: "my-ext-config.yml" - name: "my-ext-config.yml"
@@ -186,7 +186,7 @@ What the extension provides.
- `name`: Command name (must match `speckit.{ext-id}.{command}`) - `name`: Command name (must match `speckit.{ext-id}.{command}`)
- `file`: Path to command file (relative to extension root) - `file`: Path to command file (relative to extension root)
- `description`: Command description (optional) - `description`: Command description (optional)
- `aliases`: Alternative command names (optional, array) - `aliases`: Alternative command names (optional, array; each must match `speckit.{ext-id}.{command}`)
### Optional Fields ### Optional Fields

View File

@@ -214,8 +214,8 @@ Extensions add commands that appear in your AI agent (Claude Code):
# In Claude Code # In Claude Code
> /speckit.jira.specstoissues > /speckit.jira.specstoissues
# Or use short alias (if provided) # Or use a namespaced alias (if provided)
> /speckit.specstoissues > /speckit.jira.sync
``` ```
### Extension Configuration ### Extension Configuration

View File

@@ -223,7 +223,7 @@ provides:
- name: "speckit.jira.specstoissues" - name: "speckit.jira.specstoissues"
file: "commands/specstoissues.md" file: "commands/specstoissues.md"
description: "Create Jira hierarchy from spec and tasks" description: "Create Jira hierarchy from spec and tasks"
aliases: ["speckit.specstoissues"] # Alternate names aliases: ["speckit.jira.sync"] # Alternate names
- name: "speckit.jira.discover-fields" - name: "speckit.jira.discover-fields"
file: "commands/discover-fields.md" file: "commands/discover-fields.md"
@@ -1517,7 +1517,7 @@ specify extension add github-projects
/speckit.github.taskstoissues /speckit.github.taskstoissues
``` ```
**Compatibility shim** (if needed): **Migration alias** (if needed):
```yaml ```yaml
# extension.yml # extension.yml
@@ -1525,10 +1525,10 @@ provides:
commands: commands:
- name: "speckit.github.taskstoissues" - name: "speckit.github.taskstoissues"
file: "commands/taskstoissues.md" file: "commands/taskstoissues.md"
aliases: ["speckit.taskstoissues"] # Backward compatibility aliases: ["speckit.github.sync-taskstoissues"] # Alternate namespaced entry point
``` ```
AI agent registers both names, so old scripts work. AI agents register both names, so callers can migrate to the alternate alias without relying on deprecated global shortcuts like `/speckit.taskstoissues`.
--- ---

View File

@@ -1,12 +1,12 @@
{ {
"schema_version": "1.0", "schema_version": "1.0",
"updated_at": "2026-03-19T12:08:20Z", "updated_at": "2026-03-27T08:22:30Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": { "extensions": {
"aide": { "aide": {
"name": "AI-Driven Engineering (AIDE)", "name": "AI-Driven Engineering (AIDE)",
"id": "aide", "id": "aide",
"description": "A structured 7-step workflow for building new projects from scratch with AI assistants from vision through implementation.", "description": "A structured 7-step workflow for building new projects from scratch with AI assistants \u2014 from vision through implementation.",
"author": "mnriem", "author": "mnriem",
"version": "1.0.0", "version": "1.0.0",
"download_url": "https://github.com/mnriem/spec-kit-extensions/releases/download/aide-v1.0.0/aide.zip", "download_url": "https://github.com/mnriem/spec-kit-extensions/releases/download/aide-v1.0.0/aide.zip",
@@ -170,7 +170,7 @@
"cognitive-squad": { "cognitive-squad": {
"name": "Cognitive Squad", "name": "Cognitive Squad",
"id": "cognitive-squad", "id": "cognitive-squad",
"description": "Multi-agent cognitive system with Triadic Model: understanding, internalization, application with quality gates, backpropagation verification, and self-healing", "description": "Multi-agent cognitive system with Triadic Model: understanding, internalization, application \u2014 with quality gates, backpropagation verification, and self-healing",
"author": "Testimonial", "author": "Testimonial",
"version": "0.1.0", "version": "0.1.0",
"download_url": "https://github.com/Testimonial/cognitive-squad/archive/refs/tags/v0.1.0.zip", "download_url": "https://github.com/Testimonial/cognitive-squad/archive/refs/tags/v0.1.0.zip",
@@ -379,7 +379,7 @@
"iterate": { "iterate": {
"name": "Iterate", "name": "Iterate",
"id": "iterate", "id": "iterate",
"description": "Iterate on spec documents with a two-phase define-and-apply workflow refine specs mid-implementation and go straight back to building", "description": "Iterate on spec documents with a two-phase define-and-apply workflow \u2014 refine specs mid-implementation and go straight back to building",
"author": "Vianca Martinez", "author": "Vianca Martinez",
"version": "2.0.0", "version": "2.0.0",
"download_url": "https://github.com/imviancagrace/spec-kit-iterate/archive/refs/tags/v2.0.0.zip", "download_url": "https://github.com/imviancagrace/spec-kit-iterate/archive/refs/tags/v2.0.0.zip",
@@ -437,6 +437,327 @@
"created_at": "2026-03-05T00:00:00Z", "created_at": "2026-03-05T00:00:00Z",
"updated_at": "2026-03-05T00:00:00Z" "updated_at": "2026-03-05T00:00:00Z"
}, },
"learn": {
"name": "Learning Extension",
"id": "learn",
"description": "Generate educational guides from implementations and enhance clarifications with mentoring context.",
"author": "Vianca Martinez",
"version": "1.0.0",
"download_url": "https://github.com/imviancagrace/spec-kit-learn/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/imviancagrace/spec-kit-learn",
"homepage": "https://github.com/imviancagrace/spec-kit-learn",
"documentation": "https://github.com/imviancagrace/spec-kit-learn/blob/main/README.md",
"changelog": "https://github.com/imviancagrace/spec-kit-learn/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2,
"hooks": 1
},
"tags": [
"learning",
"education",
"mentoring",
"knowledge-transfer"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-17T00:00:00Z",
"updated_at": "2026-03-17T00:00:00Z"
},
"maqa": {
"name": "MAQA \u2014 Multi-Agent & Quality Assurance",
"id": "maqa",
"description": "Coordinator \u2192 feature \u2192 QA agent workflow with parallel worktree-based implementation. Language-agnostic. Auto-detects installed board plugins (Trello, Linear, GitHub Projects, Jira, Azure DevOps). Optional CI gate.",
"author": "GenieRobot",
"version": "0.1.3",
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-ext/releases/download/maqa-v0.1.3/maqa.zip",
"repository": "https://github.com/GenieRobot/spec-kit-maqa-ext",
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-ext",
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-ext/blob/main/README.md",
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-ext/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.3.0"
},
"provides": {
"commands": 4,
"hooks": 1
},
"tags": [
"multi-agent",
"orchestration",
"quality-assurance",
"workflow",
"parallel",
"tdd"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-26T00:00:00Z",
"updated_at": "2026-03-27T00:00:00Z"
},
"maqa-azure-devops": {
"name": "MAQA Azure DevOps Integration",
"id": "maqa-azure-devops",
"description": "Azure DevOps Boards integration for the MAQA extension. Populates work items from specs, moves User Stories across columns as features progress, real-time Task child ticking.",
"author": "GenieRobot",
"version": "0.1.0",
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops/releases/download/maqa-azure-devops-v0.1.0/maqa-azure-devops.zip",
"repository": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops",
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops",
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops/blob/main/README.md",
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.3.0"
},
"provides": {
"commands": 2,
"hooks": 0
},
"tags": [
"azure-devops",
"project-management",
"multi-agent",
"maqa",
"kanban"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-27T00:00:00Z",
"updated_at": "2026-03-27T00:00:00Z"
},
"maqa-ci": {
"name": "MAQA CI/CD Gate",
"id": "maqa-ci",
"description": "CI/CD pipeline gate for the MAQA extension. Auto-detects GitHub Actions, CircleCI, GitLab CI, and Bitbucket Pipelines. Blocks QA handoff until pipeline is green.",
"author": "GenieRobot",
"version": "0.1.0",
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-ci/releases/download/maqa-ci-v0.1.0/maqa-ci.zip",
"repository": "https://github.com/GenieRobot/spec-kit-maqa-ci",
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-ci",
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-ci/blob/main/README.md",
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-ci/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.3.0"
},
"provides": {
"commands": 2,
"hooks": 0
},
"tags": [
"ci-cd",
"github-actions",
"circleci",
"gitlab-ci",
"quality-gate",
"maqa"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-27T00:00:00Z",
"updated_at": "2026-03-27T00:00:00Z"
},
"maqa-github-projects": {
"name": "MAQA GitHub Projects Integration",
"id": "maqa-github-projects",
"description": "GitHub Projects v2 integration for the MAQA extension. Populates draft issues from specs, moves items across Status columns as features progress, real-time task list ticking.",
"author": "GenieRobot",
"version": "0.1.0",
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-github-projects/releases/download/maqa-github-projects-v0.1.0/maqa-github-projects.zip",
"repository": "https://github.com/GenieRobot/spec-kit-maqa-github-projects",
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-github-projects",
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-github-projects/blob/main/README.md",
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-github-projects/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.3.0"
},
"provides": {
"commands": 2,
"hooks": 0
},
"tags": [
"github-projects",
"project-management",
"multi-agent",
"maqa",
"kanban"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-27T00:00:00Z",
"updated_at": "2026-03-27T00:00:00Z"
},
"maqa-jira": {
"name": "MAQA Jira Integration",
"id": "maqa-jira",
"description": "Jira integration for the MAQA extension. Populates Stories from specs, moves issues across board columns as features progress, real-time Subtask ticking.",
"author": "GenieRobot",
"version": "0.1.0",
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-jira/releases/download/maqa-jira-v0.1.0/maqa-jira.zip",
"repository": "https://github.com/GenieRobot/spec-kit-maqa-jira",
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-jira",
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-jira/blob/main/README.md",
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-jira/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.3.0"
},
"provides": {
"commands": 2,
"hooks": 0
},
"tags": [
"jira",
"project-management",
"multi-agent",
"maqa",
"kanban"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-27T00:00:00Z",
"updated_at": "2026-03-27T00:00:00Z"
},
"maqa-linear": {
"name": "MAQA Linear Integration",
"id": "maqa-linear",
"description": "Linear integration for the MAQA extension. Populates issues from specs, moves items across workflow states as features progress, real-time sub-issue ticking.",
"author": "GenieRobot",
"version": "0.1.0",
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-linear/releases/download/maqa-linear-v0.1.0/maqa-linear.zip",
"repository": "https://github.com/GenieRobot/spec-kit-maqa-linear",
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-linear",
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-linear/blob/main/README.md",
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-linear/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.3.0"
},
"provides": {
"commands": 2,
"hooks": 0
},
"tags": [
"linear",
"project-management",
"multi-agent",
"maqa",
"kanban"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-27T00:00:00Z",
"updated_at": "2026-03-27T00:00:00Z"
},
"maqa-trello": {
"name": "MAQA Trello Integration",
"id": "maqa-trello",
"description": "Trello board integration for the MAQA extension. Populates board from specs, moves cards between lists as features progress, real-time checklist ticking.",
"author": "GenieRobot",
"version": "0.1.1",
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-trello/releases/download/maqa-trello-v0.1.1/maqa-trello.zip",
"repository": "https://github.com/GenieRobot/spec-kit-maqa-trello",
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-trello",
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-trello/blob/main/README.md",
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-trello/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.3.0"
},
"provides": {
"commands": 2,
"hooks": 0
},
"tags": [
"trello",
"project-management",
"multi-agent",
"maqa",
"kanban"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-26T00:00:00Z",
"updated_at": "2026-03-26T00:00:00Z"
},
"onboard": {
"name": "Onboard",
"id": "onboard",
"description": "Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step.",
"author": "Rafael Sales",
"version": "2.1.0",
"download_url": "https://github.com/dmux/spec-kit-onboard/archive/refs/tags/v2.1.0.zip",
"repository": "https://github.com/dmux/spec-kit-onboard",
"homepage": "https://github.com/dmux/spec-kit-onboard",
"documentation": "https://github.com/dmux/spec-kit-onboard/blob/main/README.md",
"changelog": "https://github.com/dmux/spec-kit-onboard/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 7,
"hooks": 3
},
"tags": [
"onboarding",
"learning",
"mentoring",
"developer-experience",
"gamification",
"knowledge-transfer"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-26T00:00:00Z",
"updated_at": "2026-03-26T00:00:00Z"
},
"plan-review-gate": {
"name": "Plan Review Gate",
"id": "plan-review-gate",
"description": "Require spec.md and plan.md to be merged via MR/PR before allowing task generation",
"author": "luno",
"version": "1.0.0",
"download_url": "https://github.com/luno/spec-kit-plan-review-gate/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/luno/spec-kit-plan-review-gate",
"homepage": "https://github.com/luno/spec-kit-plan-review-gate",
"documentation": "https://github.com/luno/spec-kit-plan-review-gate/blob/main/README.md",
"changelog": "https://github.com/luno/spec-kit-plan-review-gate/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"review",
"quality",
"workflow",
"gate"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-27T08:22:30Z",
"updated_at": "2026-03-27T08:22:30Z"
},
"presetify": { "presetify": {
"name": "Presetify", "name": "Presetify",
"id": "presetify", "id": "presetify",
@@ -638,6 +959,38 @@
"created_at": "2026-03-18T00:00:00Z", "created_at": "2026-03-18T00:00:00Z",
"updated_at": "2026-03-18T00:00:00Z" "updated_at": "2026-03-18T00:00:00Z"
}, },
"status": {
"name": "Project Status",
"id": "status",
"description": "Show current SDD workflow progress \u2014 active feature, artifact status, task completion, workflow phase, and extensions summary.",
"author": "KhawarHabibKhan",
"version": "1.0.0",
"download_url": "https://github.com/KhawarHabibKhan/spec-kit-status/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/KhawarHabibKhan/spec-kit-status",
"homepage": "https://github.com/KhawarHabibKhan/spec-kit-status",
"documentation": "https://github.com/KhawarHabibKhan/spec-kit-status/blob/main/README.md",
"changelog": "https://github.com/KhawarHabibKhan/spec-kit-status/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 0
},
"tags": [
"status",
"workflow",
"progress",
"feature-tracking",
"task-progress"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-16T00:00:00Z",
"updated_at": "2026-03-16T00:00:00Z"
},
"sync": { "sync": {
"name": "Spec Sync", "name": "Spec Sync",
"id": "sync", "id": "sync",
@@ -711,38 +1064,6 @@
"created_at": "2026-03-07T00:00:00Z", "created_at": "2026-03-07T00:00:00Z",
"updated_at": "2026-03-07T00:00:00Z" "updated_at": "2026-03-07T00:00:00Z"
}, },
"status": {
"name": "Project Status",
"id": "status",
"description": "Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary.",
"author": "KhawarHabibKhan",
"version": "1.0.0",
"download_url": "https://github.com/KhawarHabibKhan/spec-kit-status/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/KhawarHabibKhan/spec-kit-status",
"homepage": "https://github.com/KhawarHabibKhan/spec-kit-status",
"documentation": "https://github.com/KhawarHabibKhan/spec-kit-status/blob/main/README.md",
"changelog": "https://github.com/KhawarHabibKhan/spec-kit-status/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 0
},
"tags": [
"status",
"workflow",
"progress",
"feature-tracking",
"task-progress"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-16T00:00:00Z",
"updated_at": "2026-03-16T00:00:00Z"
},
"v-model": { "v-model": {
"name": "V-Model Extension Pack", "name": "V-Model Extension Pack",
"id": "v-model", "id": "v-model",
@@ -775,37 +1096,6 @@
"created_at": "2026-02-20T00:00:00Z", "created_at": "2026-02-20T00:00:00Z",
"updated_at": "2026-02-22T00:00:00Z" "updated_at": "2026-02-22T00:00:00Z"
}, },
"learn": {
"name": "Learning Extension",
"id": "learn",
"description": "Generate educational guides from implementations and enhance clarifications with mentoring context.",
"author": "Vianca Martinez",
"version": "1.0.0",
"download_url": "https://github.com/imviancagrace/spec-kit-learn/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/imviancagrace/spec-kit-learn",
"homepage": "https://github.com/imviancagrace/spec-kit-learn",
"documentation": "https://github.com/imviancagrace/spec-kit-learn/blob/main/README.md",
"changelog": "https://github.com/imviancagrace/spec-kit-learn/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2,
"hooks": 1
},
"tags": [
"learning",
"education",
"mentoring",
"knowledge-transfer"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-17T00:00:00Z",
"updated_at": "2026-03-17T00:00:00Z"
},
"verify": { "verify": {
"name": "Verify Extension", "name": "Verify Extension",
"id": "verify", "id": "verify",

View File

@@ -47,8 +47,8 @@ provides:
- name: "speckit.my-extension.example" - name: "speckit.my-extension.example"
file: "commands/example.md" file: "commands/example.md"
description: "Example command that demonstrates functionality" description: "Example command that demonstrates functionality"
# Optional: Add aliases for shorter command names # Optional: Add aliases in the same namespaced format
aliases: ["speckit.example"] aliases: ["speckit.my-extension.example-short"]
# ADD MORE COMMANDS: Copy this block for each command # ADD MORE COMMANDS: Copy this block for each command
# - name: "speckit.my-extension.another-command" # - name: "speckit.my-extension.another-command"

View File

@@ -3,6 +3,7 @@
set -e set -e
JSON_MODE=false JSON_MODE=false
ALLOW_EXISTING=false
SHORT_NAME="" SHORT_NAME=""
BRANCH_NUMBER="" BRANCH_NUMBER=""
USE_TIMESTAMP=false USE_TIMESTAMP=false
@@ -14,6 +15,9 @@ while [ $i -le $# ]; do
--json) --json)
JSON_MODE=true JSON_MODE=true
;; ;;
--allow-existing-branch)
ALLOW_EXISTING=true
;;
--short-name) --short-name)
if [ $((i + 1)) -gt $# ]; then if [ $((i + 1)) -gt $# ]; then
echo 'Error: --short-name requires a value' >&2 echo 'Error: --short-name requires a value' >&2
@@ -45,10 +49,11 @@ while [ $i -le $# ]; do
USE_TIMESTAMP=true USE_TIMESTAMP=true
;; ;;
--help|-h) --help|-h)
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" echo "Usage: $0 [--json] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
echo "" echo ""
echo "Options:" echo "Options:"
echo " --json Output in JSON format" echo " --json Output in JSON format"
echo " --allow-existing-branch Switch to branch if it already exists instead of failing"
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch" echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
echo " --number N Specify branch number manually (overrides auto-detection)" echo " --number N Specify branch number manually (overrides auto-detection)"
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering" echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
@@ -69,7 +74,7 @@ done
FEATURE_DESCRIPTION="${ARGS[*]}" FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2 echo "Usage: $0 [--json] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
exit 1 exit 1
fi fi
@@ -287,12 +292,19 @@ if [ "$HAS_GIT" = true ]; then
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
# Check if branch already exists # Check if branch already exists
if git branch --list "$BRANCH_NAME" | grep -q .; then if git branch --list "$BRANCH_NAME" | grep -q .; then
if [ "$USE_TIMESTAMP" = true ]; then if [ "$ALLOW_EXISTING" = true ]; then
# Switch to the existing branch instead of failing
if ! git checkout "$BRANCH_NAME" 2>/dev/null; then
>&2 echo "Error: Failed to switch to existing branch '$BRANCH_NAME'. Please resolve any local changes or conflicts and try again."
exit 1
fi
elif [ "$USE_TIMESTAMP" = true ]; then
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name." >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
exit 1
else else
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number." >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
exit 1
fi fi
exit 1
else else
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again." >&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
exit 1 exit 1
@@ -305,13 +317,15 @@ fi
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
mkdir -p "$FEATURE_DIR" mkdir -p "$FEATURE_DIR"
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
SPEC_FILE="$FEATURE_DIR/spec.md" SPEC_FILE="$FEATURE_DIR/spec.md"
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then if [ ! -f "$SPEC_FILE" ]; then
cp "$TEMPLATE" "$SPEC_FILE" TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
else if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
echo "Warning: Spec template not found; created empty spec file" >&2 cp "$TEMPLATE" "$SPEC_FILE"
touch "$SPEC_FILE" else
echo "Warning: Spec template not found; created empty spec file" >&2
touch "$SPEC_FILE"
fi
fi fi
# Inform the user how to persist the feature variable in their own shell # Inform the user how to persist the feature variable in their own shell

View File

@@ -63,7 +63,7 @@ AGENT_TYPE="${1:-}"
# Agent-specific file paths # Agent-specific file paths
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
GEMINI_FILE="$REPO_ROOT/GEMINI.md" GEMINI_FILE="$REPO_ROOT/GEMINI.md"
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md" COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
QWEN_FILE="$REPO_ROOT/QWEN.md" QWEN_FILE="$REPO_ROOT/QWEN.md"
AGENTS_FILE="$REPO_ROOT/AGENTS.md" AGENTS_FILE="$REPO_ROOT/AGENTS.md"

View File

@@ -3,6 +3,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[switch]$Json, [switch]$Json,
[switch]$AllowExistingBranch,
[string]$ShortName, [string]$ShortName,
[Parameter()] [Parameter()]
[long]$Number = 0, [long]$Number = 0,
@@ -15,10 +16,11 @@ $ErrorActionPreference = 'Stop'
# Show help if requested # Show help if requested
if ($Help) { if ($Help) {
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>" Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
Write-Host "" Write-Host ""
Write-Host "Options:" Write-Host "Options:"
Write-Host " -Json Output in JSON format" Write-Host " -Json Output in JSON format"
Write-Host " -AllowExistingBranch Switch to branch if it already exists instead of failing"
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch" Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
Write-Host " -Number N Specify branch number manually (overrides auto-detection)" Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering" Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
@@ -33,7 +35,7 @@ if ($Help) {
# Check if feature description provided # Check if feature description provided
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) { if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>" Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
exit 1 exit 1
} }
@@ -251,12 +253,20 @@ if ($hasGit) {
# Check if branch already exists # Check if branch already exists
$existingBranch = git branch --list $branchName 2>$null $existingBranch = git branch --list $branchName 2>$null
if ($existingBranch) { if ($existingBranch) {
if ($Timestamp) { if ($AllowExistingBranch) {
# Switch to the existing branch instead of failing
git checkout -q $branchName 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Error: Branch '$branchName' exists but could not be checked out. Resolve any uncommitted changes or conflicts and try again."
exit 1
}
} elseif ($Timestamp) {
Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName." Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName."
exit 1
} else { } else {
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number." Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
exit 1
} }
exit 1
} else { } else {
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again." Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
exit 1 exit 1
@@ -269,12 +279,14 @@ if ($hasGit) {
$featureDir = Join-Path $specsDir $branchName $featureDir = Join-Path $specsDir $branchName
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
$specFile = Join-Path $featureDir 'spec.md' $specFile = Join-Path $featureDir 'spec.md'
if ($template -and (Test-Path $template)) { if (-not (Test-Path -PathType Leaf $specFile)) {
Copy-Item $template $specFile -Force $template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
} else { if ($template -and (Test-Path $template)) {
New-Item -ItemType File -Path $specFile | Out-Null Copy-Item $template $specFile -Force
} else {
New-Item -ItemType File -Path $specFile | Out-Null
}
} }
# Set the SPECIFY_FEATURE environment variable for the current session # Set the SPECIFY_FEATURE environment variable for the current session

View File

@@ -46,7 +46,7 @@ $NEW_PLAN = $IMPL_PLAN
# Agent file paths # Agent file paths
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md' $CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md' $GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/copilot-instructions.md' $COPILOT_FILE = Join-Path $REPO_ROOT '.github/copilot-instructions.md'
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc' $CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md' $QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'

View File

@@ -345,6 +345,7 @@ AI_ASSISTANT_HELP = _build_ai_assistant_help()
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude" CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
CLAUDE_NPM_LOCAL_PATH = Path.home() / ".claude" / "local" / "node_modules" / ".bin" / "claude"
BANNER = """ BANNER = """
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗ ███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
@@ -605,13 +606,15 @@ def check_tool(tool: str, tracker: StepTracker = None) -> bool:
Returns: Returns:
True if tool is found, False otherwise True if tool is found, False otherwise
""" """
# Special handling for Claude CLI after `claude migrate-installer` # Special handling for Claude CLI local installs
# See: https://github.com/github/spec-kit/issues/123 # See: https://github.com/github/spec-kit/issues/123
# The migrate-installer command REMOVES the original executable from PATH # See: https://github.com/github/spec-kit/issues/550
# and creates an alias at ~/.claude/local/claude instead # Claude Code can be installed in two local paths:
# This path should be prioritized over other claude executables in PATH # 1. ~/.claude/local/claude (after `claude migrate-installer`)
# 2. ~/.claude/local/node_modules/.bin/claude (npm-local install, e.g. via nvm)
# Neither path may be on the system PATH, so we check them explicitly.
if tool == "claude": if tool == "claude":
if CLAUDE_LOCAL_PATH.exists() and CLAUDE_LOCAL_PATH.is_file(): if CLAUDE_LOCAL_PATH.is_file() or CLAUDE_NPM_LOCAL_PATH.is_file():
if tracker: if tracker:
tracker.complete(tool, "available") tracker.complete(tool, "available")
return True return True

View File

@@ -25,6 +25,49 @@ import yaml
from packaging import version as pkg_version from packaging import version as pkg_version
from packaging.specifiers import SpecifierSet, InvalidSpecifier from packaging.specifiers import SpecifierSet, InvalidSpecifier
_FALLBACK_CORE_COMMAND_NAMES = frozenset({
"analyze",
"checklist",
"clarify",
"constitution",
"implement",
"plan",
"specify",
"tasks",
"taskstoissues",
})
EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$")
def _load_core_command_names() -> frozenset[str]:
"""Discover bundled core command names from the packaged templates.
Prefer the wheel-time ``core_pack`` bundle when present, and fall back to
the source checkout when running from the repository. If neither is
available, use the baked-in fallback set so validation still works.
"""
candidate_dirs = [
Path(__file__).parent / "core_pack" / "commands",
Path(__file__).resolve().parent.parent.parent / "templates" / "commands",
]
for commands_dir in candidate_dirs:
if not commands_dir.is_dir():
continue
command_names = {
command_file.stem
for command_file in commands_dir.iterdir()
if command_file.is_file() and command_file.suffix == ".md"
}
if command_names:
return frozenset(command_names)
return _FALLBACK_CORE_COMMAND_NAMES
CORE_COMMAND_NAMES = _load_core_command_names()
class ExtensionError(Exception): class ExtensionError(Exception):
"""Base exception for extension-related errors.""" """Base exception for extension-related errors."""
@@ -149,7 +192,7 @@ class ExtensionManifest:
raise ValidationError("Command missing 'name' or 'file'") raise ValidationError("Command missing 'name' or 'file'")
# Validate command name format # Validate command name format
if not re.match(r'^speckit\.[a-z0-9-]+\.[a-z0-9-]+$', cmd["name"]): if EXTENSION_COMMAND_NAME_PATTERN.match(cmd["name"]) is None:
raise ValidationError( raise ValidationError(
f"Invalid command name '{cmd['name']}': " f"Invalid command name '{cmd['name']}': "
"must follow pattern 'speckit.{extension}.{command}'" "must follow pattern 'speckit.{extension}.{command}'"
@@ -446,6 +489,126 @@ class ExtensionManager:
self.extensions_dir = project_root / ".specify" / "extensions" self.extensions_dir = project_root / ".specify" / "extensions"
self.registry = ExtensionRegistry(self.extensions_dir) self.registry = ExtensionRegistry(self.extensions_dir)
@staticmethod
def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, str]:
"""Collect command and alias names declared by a manifest.
Performs install-time validation for extension-specific constraints:
- commands and aliases must use the canonical `speckit.{extension}.{command}` shape
- commands and aliases must use this extension's namespace
- command namespaces must not shadow core commands
- duplicate command/alias names inside one manifest are rejected
Args:
manifest: Parsed extension manifest
Returns:
Mapping of declared command/alias name -> kind ("command"/"alias")
Raises:
ValidationError: If any declared name is invalid
"""
if manifest.id in CORE_COMMAND_NAMES:
raise ValidationError(
f"Extension ID '{manifest.id}' conflicts with core command namespace '{manifest.id}'"
)
declared_names: Dict[str, str] = {}
for cmd in manifest.commands:
primary_name = cmd["name"]
aliases = cmd.get("aliases", [])
if aliases is None:
aliases = []
if not isinstance(aliases, list):
raise ValidationError(
f"Aliases for command '{primary_name}' must be a list"
)
for kind, name in [("command", primary_name)] + [
("alias", alias) for alias in aliases
]:
if not isinstance(name, str):
raise ValidationError(
f"{kind.capitalize()} for command '{primary_name}' must be a string"
)
match = EXTENSION_COMMAND_NAME_PATTERN.match(name)
if match is None:
raise ValidationError(
f"Invalid {kind} '{name}': "
"must follow pattern 'speckit.{extension}.{command}'"
)
namespace = match.group(1)
if namespace != manifest.id:
raise ValidationError(
f"{kind.capitalize()} '{name}' must use extension namespace '{manifest.id}'"
)
if namespace in CORE_COMMAND_NAMES:
raise ValidationError(
f"{kind.capitalize()} '{name}' conflicts with core command namespace '{namespace}'"
)
if name in declared_names:
raise ValidationError(
f"Duplicate command or alias '{name}' in extension manifest"
)
declared_names[name] = kind
return declared_names
def _get_installed_command_name_map(
self,
exclude_extension_id: Optional[str] = None,
) -> Dict[str, str]:
"""Return registered command and alias names for installed extensions."""
installed_names: Dict[str, str] = {}
for ext_id in self.registry.keys():
if ext_id == exclude_extension_id:
continue
manifest = self.get_extension(ext_id)
if manifest is None:
continue
for cmd in manifest.commands:
cmd_name = cmd.get("name")
if isinstance(cmd_name, str):
installed_names.setdefault(cmd_name, ext_id)
aliases = cmd.get("aliases", [])
if not isinstance(aliases, list):
continue
for alias in aliases:
if isinstance(alias, str):
installed_names.setdefault(alias, ext_id)
return installed_names
def _validate_install_conflicts(self, manifest: ExtensionManifest) -> None:
"""Reject installs that would shadow core or installed extension commands."""
declared_names = self._collect_manifest_command_names(manifest)
installed_names = self._get_installed_command_name_map(
exclude_extension_id=manifest.id
)
collisions = [
f"{name} (already provided by extension '{installed_names[name]}')"
for name in sorted(declared_names)
if name in installed_names
]
if collisions:
raise ValidationError(
"Extension commands conflict with installed extensions:\n- "
+ "\n- ".join(collisions)
)
@staticmethod @staticmethod
def _load_extensionignore(source_dir: Path) -> Optional[Callable[[str, List[str]], Set[str]]]: def _load_extensionignore(source_dir: Path) -> Optional[Callable[[str, List[str]], Set[str]]]:
"""Load .extensionignore and return an ignore function for shutil.copytree. """Load .extensionignore and return an ignore function for shutil.copytree.
@@ -861,6 +1024,9 @@ class ExtensionManager:
f"Use 'specify extension remove {manifest.id}' first." f"Use 'specify extension remove {manifest.id}' first."
) )
# Reject manifests that would shadow core commands or installed extensions.
self._validate_install_conflicts(manifest)
# Install extension # Install extension
dest_dir = self.extensions_dir / manifest.id dest_dir = self.extensions_dir / manifest.id
if dest_dir.exists(): if dest_dir.exists():

96
tests/test_check_tool.py Normal file
View File

@@ -0,0 +1,96 @@
"""Tests for check_tool() — Claude Code CLI detection across install methods.
Covers issue https://github.com/github/spec-kit/issues/550:
`specify check` reports "Claude Code CLI (not found)" even when claude is
installed via npm-local (the default `claude` installer path).
"""
from unittest.mock import patch, MagicMock
from specify_cli import check_tool
class TestCheckToolClaude:
"""Claude CLI detection must work for all install methods."""
def test_detected_via_migrate_installer_path(self, tmp_path):
"""claude migrate-installer puts binary at ~/.claude/local/claude."""
fake_claude = tmp_path / "claude"
fake_claude.touch()
# Ensure npm-local path is missing so we only exercise migrate-installer path
fake_missing = tmp_path / "nonexistent" / "claude"
with patch("specify_cli.CLAUDE_LOCAL_PATH", fake_claude), \
patch("specify_cli.CLAUDE_NPM_LOCAL_PATH", fake_missing), \
patch("shutil.which", return_value=None):
assert check_tool("claude") is True
def test_detected_via_npm_local_path(self, tmp_path):
"""npm-local install puts binary at ~/.claude/local/node_modules/.bin/claude."""
fake_npm_claude = tmp_path / "node_modules" / ".bin" / "claude"
fake_npm_claude.parent.mkdir(parents=True)
fake_npm_claude.touch()
# Neither the migrate-installer path nor PATH has claude
fake_migrate = tmp_path / "nonexistent" / "claude"
with patch("specify_cli.CLAUDE_LOCAL_PATH", fake_migrate), \
patch("specify_cli.CLAUDE_NPM_LOCAL_PATH", fake_npm_claude), \
patch("shutil.which", return_value=None):
assert check_tool("claude") is True
def test_detected_via_path(self, tmp_path):
"""claude on PATH (global npm install) should still work."""
fake_missing = tmp_path / "nonexistent" / "claude"
with patch("specify_cli.CLAUDE_LOCAL_PATH", fake_missing), \
patch("specify_cli.CLAUDE_NPM_LOCAL_PATH", fake_missing), \
patch("shutil.which", return_value="/usr/local/bin/claude"):
assert check_tool("claude") is True
def test_not_found_when_nowhere(self, tmp_path):
"""Should return False when claude is genuinely not installed."""
fake_missing = tmp_path / "nonexistent" / "claude"
with patch("specify_cli.CLAUDE_LOCAL_PATH", fake_missing), \
patch("specify_cli.CLAUDE_NPM_LOCAL_PATH", fake_missing), \
patch("shutil.which", return_value=None):
assert check_tool("claude") is False
def test_tracker_updated_on_npm_local_detection(self, tmp_path):
"""StepTracker should be marked 'available' for npm-local installs."""
fake_npm_claude = tmp_path / "node_modules" / ".bin" / "claude"
fake_npm_claude.parent.mkdir(parents=True)
fake_npm_claude.touch()
fake_missing = tmp_path / "nonexistent" / "claude"
tracker = MagicMock()
with patch("specify_cli.CLAUDE_LOCAL_PATH", fake_missing), \
patch("specify_cli.CLAUDE_NPM_LOCAL_PATH", fake_npm_claude), \
patch("shutil.which", return_value=None):
result = check_tool("claude", tracker=tracker)
assert result is True
tracker.complete.assert_called_once_with("claude", "available")
class TestCheckToolOther:
"""Non-Claude tools should be unaffected by the fix."""
def test_git_detected_via_path(self):
with patch("shutil.which", return_value="/usr/bin/git"):
assert check_tool("git") is True
def test_missing_tool(self):
with patch("shutil.which", return_value=None):
assert check_tool("nonexistent-tool") is False
def test_kiro_fallback(self):
"""kiro-cli detection should try both kiro-cli and kiro."""
def fake_which(name):
return "/usr/bin/kiro" if name == "kiro" else None
with patch("shutil.which", side_effect=fake_which):
assert check_tool("kiro-cli") is True

View File

@@ -18,6 +18,7 @@ from datetime import datetime, timezone
from specify_cli.extensions import ( from specify_cli.extensions import (
CatalogEntry, CatalogEntry,
CORE_COMMAND_NAMES,
ExtensionManifest, ExtensionManifest,
ExtensionRegistry, ExtensionRegistry,
ExtensionManager, ExtensionManager,
@@ -63,7 +64,7 @@ def valid_manifest_data():
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.test.hello", "name": "speckit.test-ext.hello",
"file": "commands/hello.md", "file": "commands/hello.md",
"description": "Test command", "description": "Test command",
} }
@@ -71,7 +72,7 @@ def valid_manifest_data():
}, },
"hooks": { "hooks": {
"after_tasks": { "after_tasks": {
"command": "speckit.test.hello", "command": "speckit.test-ext.hello",
"optional": True, "optional": True,
"prompt": "Run test?", "prompt": "Run test?",
} }
@@ -189,7 +190,18 @@ class TestExtensionManifest:
assert manifest.version == "1.0.0" assert manifest.version == "1.0.0"
assert manifest.description == "A test extension" assert manifest.description == "A test extension"
assert len(manifest.commands) == 1 assert len(manifest.commands) == 1
assert manifest.commands[0]["name"] == "speckit.test.hello" assert manifest.commands[0]["name"] == "speckit.test-ext.hello"
def test_core_command_names_match_bundled_templates(self):
"""Core command reservations should stay aligned with bundled templates."""
commands_dir = Path(__file__).resolve().parent.parent / "templates" / "commands"
expected = {
command_file.stem
for command_file in commands_dir.iterdir()
if command_file.is_file() and command_file.suffix == ".md"
}
assert CORE_COMMAND_NAMES == expected
def test_missing_required_field(self, temp_dir): def test_missing_required_field(self, temp_dir):
"""Test manifest missing required field.""" """Test manifest missing required field."""
@@ -589,6 +601,172 @@ class TestExtensionManager:
with pytest.raises(ExtensionError, match="already installed"): with pytest.raises(ExtensionError, match="already installed"):
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False) manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
def test_install_rejects_extension_id_in_core_namespace(self, temp_dir, project_dir):
"""Install should reject extension IDs that shadow core commands."""
import yaml
ext_dir = temp_dir / "analyze-ext"
ext_dir.mkdir()
(ext_dir / "commands").mkdir()
manifest_data = {
"schema_version": "1.0",
"extension": {
"id": "analyze",
"name": "Analyze Extension",
"version": "1.0.0",
"description": "Test",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"commands": [
{
"name": "speckit.analyze.extra",
"file": "commands/cmd.md",
}
]
},
}
(ext_dir / "extension.yml").write_text(yaml.dump(manifest_data))
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
manager = ExtensionManager(project_dir)
with pytest.raises(ValidationError, match="conflicts with core command namespace"):
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
def test_install_rejects_alias_without_extension_namespace(self, temp_dir, project_dir):
"""Install should reject legacy short aliases that can shadow core commands."""
import yaml
ext_dir = temp_dir / "alias-shortcut"
ext_dir.mkdir()
(ext_dir / "commands").mkdir()
manifest_data = {
"schema_version": "1.0",
"extension": {
"id": "alias-shortcut",
"name": "Alias Shortcut",
"version": "1.0.0",
"description": "Test",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"commands": [
{
"name": "speckit.alias-shortcut.cmd",
"file": "commands/cmd.md",
"aliases": ["speckit.shortcut"],
}
]
},
}
(ext_dir / "extension.yml").write_text(yaml.dump(manifest_data))
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
manager = ExtensionManager(project_dir)
with pytest.raises(ValidationError, match="Invalid alias 'speckit.shortcut'"):
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
def test_install_rejects_namespace_squatting(self, temp_dir, project_dir):
"""Install should reject commands and aliases outside the extension namespace."""
import yaml
ext_dir = temp_dir / "squat-ext"
ext_dir.mkdir()
(ext_dir / "commands").mkdir()
manifest_data = {
"schema_version": "1.0",
"extension": {
"id": "squat-ext",
"name": "Squat Extension",
"version": "1.0.0",
"description": "Test",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"commands": [
{
"name": "speckit.other-ext.cmd",
"file": "commands/cmd.md",
"aliases": ["speckit.squat-ext.ok"],
}
]
},
}
(ext_dir / "extension.yml").write_text(yaml.dump(manifest_data))
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
manager = ExtensionManager(project_dir)
with pytest.raises(ValidationError, match="must use extension namespace 'squat-ext'"):
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
def test_install_rejects_command_collision_with_installed_extension(self, temp_dir, project_dir):
"""Install should reject names already claimed by an installed legacy extension."""
import yaml
first_dir = temp_dir / "ext-one"
first_dir.mkdir()
(first_dir / "commands").mkdir()
first_manifest = {
"schema_version": "1.0",
"extension": {
"id": "ext-one",
"name": "Extension One",
"version": "1.0.0",
"description": "Test",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"commands": [
{
"name": "speckit.ext-one.sync",
"file": "commands/cmd.md",
"aliases": ["speckit.shared.sync"],
}
]
},
}
(first_dir / "extension.yml").write_text(yaml.dump(first_manifest))
(first_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
installed_ext_dir = project_dir / ".specify" / "extensions" / "ext-one"
installed_ext_dir.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(first_dir, installed_ext_dir)
second_dir = temp_dir / "ext-two"
second_dir.mkdir()
(second_dir / "commands").mkdir()
second_manifest = {
"schema_version": "1.0",
"extension": {
"id": "shared",
"name": "Shared Extension",
"version": "1.0.0",
"description": "Test",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"commands": [
{
"name": "speckit.shared.sync",
"file": "commands/cmd.md",
}
]
},
}
(second_dir / "extension.yml").write_text(yaml.dump(second_manifest))
(second_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
manager = ExtensionManager(project_dir)
manager.registry.add("ext-one", {"version": "1.0.0", "source": "local"})
with pytest.raises(ValidationError, match="already provided by extension 'ext-one'"):
manager.install_from_directory(second_dir, "0.1.0", register_commands=False)
def test_remove_extension(self, extension_dir, project_dir): def test_remove_extension(self, extension_dir, project_dir):
"""Test removing an installed extension.""" """Test removing an installed extension."""
manager = ExtensionManager(project_dir) manager = ExtensionManager(project_dir)
@@ -852,10 +1030,10 @@ $ARGUMENTS
) )
assert len(registered) == 1 assert len(registered) == 1
assert "speckit.test.hello" in registered assert "speckit.test-ext.hello" in registered
# Check command file was created # Check command file was created
cmd_file = claude_dir / "speckit.test.hello.md" cmd_file = claude_dir / "speckit.test-ext.hello.md"
assert cmd_file.exists() assert cmd_file.exists()
content = cmd_file.read_text() content = cmd_file.read_text()
@@ -885,9 +1063,9 @@ $ARGUMENTS
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.alias.cmd", "name": "speckit.ext-alias.cmd",
"file": "commands/cmd.md", "file": "commands/cmd.md",
"aliases": ["speckit.shortcut"], "aliases": ["speckit.ext-alias.shortcut"],
} }
] ]
}, },
@@ -907,10 +1085,10 @@ $ARGUMENTS
registered = registrar.register_commands_for_claude(manifest, ext_dir, project_dir) registered = registrar.register_commands_for_claude(manifest, ext_dir, project_dir)
assert len(registered) == 2 assert len(registered) == 2
assert "speckit.alias.cmd" in registered assert "speckit.ext-alias.cmd" in registered
assert "speckit.shortcut" in registered assert "speckit.ext-alias.shortcut" in registered
assert (claude_dir / "speckit.alias.cmd.md").exists() assert (claude_dir / "speckit.ext-alias.cmd.md").exists()
assert (claude_dir / "speckit.shortcut.md").exists() assert (claude_dir / "speckit.ext-alias.shortcut.md").exists()
def test_unregister_commands_for_codex_skills_uses_mapped_names(self, project_dir): def test_unregister_commands_for_codex_skills_uses_mapped_names(self, project_dir):
"""Codex skill cleanup should use the same mapped names as registration.""" """Codex skill cleanup should use the same mapped names as registration."""
@@ -951,11 +1129,11 @@ $ARGUMENTS
registrar = CommandRegistrar() registrar = CommandRegistrar()
registrar.register_commands_for_agent("codex", manifest, extension_dir, project_dir) registrar.register_commands_for_agent("codex", manifest, extension_dir, project_dir)
skill_file = skills_dir / "speckit-test-hello" / "SKILL.md" skill_file = skills_dir / "speckit-test-ext-hello" / "SKILL.md"
assert skill_file.exists() assert skill_file.exists()
content = skill_file.read_text() content = skill_file.read_text()
assert "name: speckit-test-hello" in content assert "name: speckit-test-ext-hello" in content
assert "description: Test hello command" in content assert "description: Test hello command" in content
assert "compatibility:" in content assert "compatibility:" in content
assert "metadata:" in content assert "metadata:" in content
@@ -982,7 +1160,7 @@ $ARGUMENTS
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.test.plan", "name": "speckit.ext-scripted.plan",
"file": "commands/plan.md", "file": "commands/plan.md",
"description": "Scripted command", "description": "Scripted command",
} }
@@ -1020,7 +1198,7 @@ Agent __AGENT__
registrar = CommandRegistrar() registrar = CommandRegistrar()
registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir) registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir)
skill_file = skills_dir / "speckit-test-plan" / "SKILL.md" skill_file = skills_dir / "speckit-ext-scripted-plan" / "SKILL.md"
assert skill_file.exists() assert skill_file.exists()
content = skill_file.read_text() content = skill_file.read_text()
@@ -1051,9 +1229,9 @@ Agent __AGENT__
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.alias.cmd", "name": "speckit.ext-alias-skill.cmd",
"file": "commands/cmd.md", "file": "commands/cmd.md",
"aliases": ["speckit.shortcut"], "aliases": ["speckit.ext-alias-skill.shortcut"],
} }
] ]
}, },
@@ -1070,13 +1248,13 @@ Agent __AGENT__
registrar = CommandRegistrar() registrar = CommandRegistrar()
registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir) registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir)
primary = skills_dir / "speckit-alias-cmd" / "SKILL.md" primary = skills_dir / "speckit-ext-alias-skill-cmd" / "SKILL.md"
alias = skills_dir / "speckit-shortcut" / "SKILL.md" alias = skills_dir / "speckit-ext-alias-skill-shortcut" / "SKILL.md"
assert primary.exists() assert primary.exists()
assert alias.exists() assert alias.exists()
assert "name: speckit-alias-cmd" in primary.read_text() assert "name: speckit-ext-alias-skill-cmd" in primary.read_text()
assert "name: speckit-shortcut" in alias.read_text() assert "name: speckit-ext-alias-skill-shortcut" in alias.read_text()
def test_codex_skill_registration_uses_fallback_script_variant_without_init_options( def test_codex_skill_registration_uses_fallback_script_variant_without_init_options(
self, project_dir, temp_dir self, project_dir, temp_dir
@@ -1100,7 +1278,7 @@ Agent __AGENT__
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.fallback.plan", "name": "speckit.ext-script-fallback.plan",
"file": "commands/plan.md", "file": "commands/plan.md",
} }
] ]
@@ -1132,7 +1310,7 @@ Then {AGENT_SCRIPT}
registrar = CommandRegistrar() registrar = CommandRegistrar()
registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir) registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir)
skill_file = skills_dir / "speckit-fallback-plan" / "SKILL.md" skill_file = skills_dir / "speckit-ext-script-fallback-plan" / "SKILL.md"
assert skill_file.exists() assert skill_file.exists()
content = skill_file.read_text() content = skill_file.read_text()
@@ -1163,7 +1341,7 @@ Then {AGENT_SCRIPT}
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.list.plan", "name": "speckit.ext-script-list-init.plan",
"file": "commands/plan.md", "file": "commands/plan.md",
} }
] ]
@@ -1194,7 +1372,7 @@ Run {SCRIPT}
registrar = CommandRegistrar() registrar = CommandRegistrar()
registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir) registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir)
content = (skills_dir / "speckit-list-plan" / "SKILL.md").read_text() content = (skills_dir / "speckit-ext-script-list-init-plan" / "SKILL.md").read_text()
assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content
def test_codex_skill_registration_fallback_prefers_powershell_on_windows( def test_codex_skill_registration_fallback_prefers_powershell_on_windows(
@@ -1221,7 +1399,7 @@ Run {SCRIPT}
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.windows.plan", "name": "speckit.ext-script-windows-fallback.plan",
"file": "commands/plan.md", "file": "commands/plan.md",
} }
] ]
@@ -1253,7 +1431,7 @@ Then {AGENT_SCRIPT}
registrar = CommandRegistrar() registrar = CommandRegistrar()
registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir) registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir)
skill_file = skills_dir / "speckit-windows-plan" / "SKILL.md" skill_file = skills_dir / "speckit-ext-script-windows-fallback-plan" / "SKILL.md"
assert skill_file.exists() assert skill_file.exists()
content = skill_file.read_text() content = skill_file.read_text()
@@ -1275,14 +1453,14 @@ Then {AGENT_SCRIPT}
) )
assert len(registered) == 1 assert len(registered) == 1
assert "speckit.test.hello" in registered assert "speckit.test-ext.hello" in registered
# Verify command file uses .agent.md extension # Verify command file uses .agent.md extension
cmd_file = agents_dir / "speckit.test.hello.agent.md" cmd_file = agents_dir / "speckit.test-ext.hello.agent.md"
assert cmd_file.exists() assert cmd_file.exists()
# Verify NO plain .md file was created # Verify NO plain .md file was created
plain_md_file = agents_dir / "speckit.test.hello.md" plain_md_file = agents_dir / "speckit.test-ext.hello.md"
assert not plain_md_file.exists() assert not plain_md_file.exists()
content = cmd_file.read_text() content = cmd_file.read_text()
@@ -1302,12 +1480,12 @@ Then {AGENT_SCRIPT}
) )
# Verify companion .prompt.md file exists # Verify companion .prompt.md file exists
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md" prompt_file = project_dir / ".github" / "prompts" / "speckit.test-ext.hello.prompt.md"
assert prompt_file.exists() assert prompt_file.exists()
# Verify content has correct agent frontmatter # Verify content has correct agent frontmatter
content = prompt_file.read_text() content = prompt_file.read_text()
assert content == "---\nagent: speckit.test.hello\n---\n" assert content == "---\nagent: speckit.test-ext.hello\n---\n"
def test_copilot_aliases_get_companion_prompts(self, project_dir, temp_dir): def test_copilot_aliases_get_companion_prompts(self, project_dir, temp_dir):
"""Test that aliases also get companion .prompt.md files for Copilot.""" """Test that aliases also get companion .prompt.md files for Copilot."""
@@ -1328,9 +1506,9 @@ Then {AGENT_SCRIPT}
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.alias-copilot.cmd", "name": "speckit.ext-alias-copilot.cmd",
"file": "commands/cmd.md", "file": "commands/cmd.md",
"aliases": ["speckit.shortcut-copilot"], "aliases": ["speckit.ext-alias-copilot.shortcut"],
} }
] ]
}, },
@@ -1357,8 +1535,8 @@ Then {AGENT_SCRIPT}
# Both primary and alias get companion .prompt.md # Both primary and alias get companion .prompt.md
prompts_dir = project_dir / ".github" / "prompts" prompts_dir = project_dir / ".github" / "prompts"
assert (prompts_dir / "speckit.alias-copilot.cmd.prompt.md").exists() assert (prompts_dir / "speckit.ext-alias-copilot.cmd.prompt.md").exists()
assert (prompts_dir / "speckit.shortcut-copilot.prompt.md").exists() assert (prompts_dir / "speckit.ext-alias-copilot.shortcut.prompt.md").exists()
def test_non_copilot_agent_no_companion_file(self, extension_dir, project_dir): def test_non_copilot_agent_no_companion_file(self, extension_dir, project_dir):
"""Test that non-copilot agents do NOT create .prompt.md files.""" """Test that non-copilot agents do NOT create .prompt.md files."""
@@ -1431,7 +1609,7 @@ class TestIntegration:
assert installed[0]["id"] == "test-ext" assert installed[0]["id"] == "test-ext"
# Verify command registered # Verify command registered
cmd_file = project_dir / ".claude" / "commands" / "speckit.test.hello.md" cmd_file = project_dir / ".claude" / "commands" / "speckit.test-ext.hello.md"
assert cmd_file.exists() assert cmd_file.exists()
# Verify registry has registered commands (now a dict keyed by agent) # Verify registry has registered commands (now a dict keyed by agent)
@@ -1439,7 +1617,7 @@ class TestIntegration:
registered_commands = metadata["registered_commands"] registered_commands = metadata["registered_commands"]
# Check that the command is registered for at least one agent # Check that the command is registered for at least one agent
assert any( assert any(
"speckit.test.hello" in cmds "speckit.test-ext.hello" in cmds
for cmds in registered_commands.values() for cmds in registered_commands.values()
) )
@@ -1465,8 +1643,8 @@ class TestIntegration:
assert "copilot" in metadata["registered_commands"] assert "copilot" in metadata["registered_commands"]
# Verify files exist before cleanup # Verify files exist before cleanup
agent_file = agents_dir / "speckit.test.hello.agent.md" agent_file = agents_dir / "speckit.test-ext.hello.agent.md"
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md" prompt_file = project_dir / ".github" / "prompts" / "speckit.test-ext.hello.prompt.md"
assert agent_file.exists() assert agent_file.exists()
assert prompt_file.exists() assert prompt_file.exists()
@@ -2776,7 +2954,7 @@ class TestExtensionUpdateCLI:
"provides": { "provides": {
"commands": [ "commands": [
{ {
"name": "speckit.test.hello", "name": "speckit.test-ext.hello",
"file": "commands/hello.md", "file": "commands/hello.md",
"description": "Test command", "description": "Test command",
} }
@@ -2784,7 +2962,7 @@ class TestExtensionUpdateCLI:
}, },
"hooks": { "hooks": {
"after_tasks": { "after_tasks": {
"command": "speckit.test.hello", "command": "speckit.test-ext.hello",
"optional": True, "optional": True,
} }
}, },
@@ -2813,7 +2991,7 @@ class TestExtensionUpdateCLI:
"description": "A test extension", "description": "A test extension",
}, },
"requires": {"speckit_version": ">=0.1.0"}, "requires": {"speckit_version": ">=0.1.0"},
"provides": {"commands": [{"name": "speckit.test.hello", "file": "commands/hello.md"}]}, "provides": {"commands": [{"name": "speckit.test-ext.hello", "file": "commands/hello.md"}]},
} }
with zipfile.ZipFile(zip_path, "w") as zf: with zipfile.ZipFile(zip_path, "w") as zf:
@@ -3442,15 +3620,15 @@ class TestHookInvocationRendering:
[ [
{ {
"extension": "test-ext", "extension": "test-ext",
"command": "speckit.test.hello", "command": "speckit.test-ext.hello",
"optional": False, "optional": False,
} }
], ],
) )
assert "Executing: `/skill:speckit-test-hello`" in message assert "Executing: `/skill:speckit-test-ext-hello`" in message
assert "EXECUTE_COMMAND: speckit.test.hello" in message assert "EXECUTE_COMMAND: speckit.test-ext.hello" in message
assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-test-hello" in message assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-test-ext-hello" in message
def test_hook_executor_caches_init_options_lookup(self, project_dir, monkeypatch): def test_hook_executor_caches_init_options_lookup(self, project_dir, monkeypatch):
"""Init options should be loaded once per executor instance.""" """Init options should be loaded once per executor instance."""

View File

@@ -269,3 +269,146 @@ class TestE2EFlow:
assert (git_repo / "specs" / branch).is_dir() assert (git_repo / "specs" / branch).is_dir()
val = source_and_call(f'check_feature_branch "{branch}" "true"') val = source_and_call(f'check_feature_branch "{branch}" "true"')
assert val.returncode == 0 assert val.returncode == 0
# ── Allow Existing Branch Tests ──────────────────────────────────────────────
class TestAllowExistingBranch:
def test_allow_existing_switches_to_branch(self, git_repo: Path):
"""T006: Pre-create branch, verify script switches to it."""
subprocess.run(
["git", "checkout", "-b", "004-pre-exist"],
cwd=git_repo, check=True, capture_output=True,
)
subprocess.run(
["git", "checkout", "-"],
cwd=git_repo, check=True, capture_output=True,
)
result = run_script(
git_repo, "--allow-existing-branch", "--short-name", "pre-exist",
"--number", "4", "Pre-existing feature",
)
assert result.returncode == 0, result.stderr
current = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=git_repo, capture_output=True, text=True,
).stdout.strip()
assert current == "004-pre-exist", f"expected 004-pre-exist, got {current}"
def test_allow_existing_already_on_branch(self, git_repo: Path):
"""T007: Verify success when already on the target branch."""
subprocess.run(
["git", "checkout", "-b", "005-already-on"],
cwd=git_repo, check=True, capture_output=True,
)
result = run_script(
git_repo, "--allow-existing-branch", "--short-name", "already-on",
"--number", "5", "Already on branch",
)
assert result.returncode == 0, result.stderr
def test_allow_existing_creates_spec_dir(self, git_repo: Path):
"""T008: Verify spec directory created on existing branch."""
subprocess.run(
["git", "checkout", "-b", "006-spec-dir"],
cwd=git_repo, check=True, capture_output=True,
)
subprocess.run(
["git", "checkout", "-"],
cwd=git_repo, check=True, capture_output=True,
)
result = run_script(
git_repo, "--allow-existing-branch", "--short-name", "spec-dir",
"--number", "6", "Spec dir feature",
)
assert result.returncode == 0, result.stderr
assert (git_repo / "specs" / "006-spec-dir").is_dir()
assert (git_repo / "specs" / "006-spec-dir" / "spec.md").exists()
def test_without_flag_still_errors(self, git_repo: Path):
"""T009: Verify backwards compatibility (error without flag)."""
subprocess.run(
["git", "checkout", "-b", "007-no-flag"],
cwd=git_repo, check=True, capture_output=True,
)
subprocess.run(
["git", "checkout", "-"],
cwd=git_repo, check=True, capture_output=True,
)
result = run_script(
git_repo, "--short-name", "no-flag", "--number", "7", "No flag feature",
)
assert result.returncode != 0, "should fail without --allow-existing-branch"
assert "already exists" in result.stderr
def test_allow_existing_no_overwrite_spec(self, git_repo: Path):
"""T010: Pre-create spec.md with content, verify it is preserved."""
subprocess.run(
["git", "checkout", "-b", "008-no-overwrite"],
cwd=git_repo, check=True, capture_output=True,
)
spec_dir = git_repo / "specs" / "008-no-overwrite"
spec_dir.mkdir(parents=True)
spec_file = spec_dir / "spec.md"
spec_file.write_text("# My custom spec content\n")
subprocess.run(
["git", "checkout", "-"],
cwd=git_repo, check=True, capture_output=True,
)
result = run_script(
git_repo, "--allow-existing-branch", "--short-name", "no-overwrite",
"--number", "8", "No overwrite feature",
)
assert result.returncode == 0, result.stderr
assert spec_file.read_text() == "# My custom spec content\n"
def test_allow_existing_creates_branch_if_not_exists(self, git_repo: Path):
"""T011: Verify normal creation when branch doesn't exist."""
result = run_script(
git_repo, "--allow-existing-branch", "--short-name", "new-branch",
"New branch feature",
)
assert result.returncode == 0, result.stderr
current = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=git_repo, capture_output=True, text=True,
).stdout.strip()
assert "new-branch" in current
def test_allow_existing_with_json(self, git_repo: Path):
"""T012: Verify JSON output is correct."""
import json
subprocess.run(
["git", "checkout", "-b", "009-json-test"],
cwd=git_repo, check=True, capture_output=True,
)
subprocess.run(
["git", "checkout", "-"],
cwd=git_repo, check=True, capture_output=True,
)
result = run_script(
git_repo, "--allow-existing-branch", "--json", "--short-name", "json-test",
"--number", "9", "JSON test",
)
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data["BRANCH_NAME"] == "009-json-test"
def test_allow_existing_no_git(self, no_git_dir: Path):
"""T013: Verify flag is silently ignored in non-git repos."""
result = run_script(
no_git_dir, "--allow-existing-branch", "--short-name", "no-git",
"No git feature",
)
assert result.returncode == 0, result.stderr
class TestAllowExistingBranchPowerShell:
def test_powershell_supports_allow_existing_branch_flag(self):
"""Static guard: PS script exposes and uses -AllowExistingBranch."""
contents = CREATE_FEATURE_PS.read_text(encoding="utf-8")
assert "-AllowExistingBranch" in contents
# Ensure the flag is referenced in script logic, not just declared
assert "AllowExistingBranch" in contents.replace("-AllowExistingBranch", "")