mirror of
https://github.com/github/spec-kit.git
synced 2026-03-28 16:23:08 +00:00
Compare commits
1 Commits
main
...
chore/rele
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aff14ea5a0 |
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -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@v5
|
uses: actions/deploy-pages@v4
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -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@v23
|
uses: DavidAnson/markdownlint-cli2-action@v19
|
||||||
with:
|
with:
|
||||||
globs: |
|
globs: |
|
||||||
'**/*.md'
|
'**/*.md'
|
||||||
|
|||||||
@@ -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]$Value)
|
param([string]$Input)
|
||||||
|
|
||||||
if ([string]::IsNullOrEmpty($Value)) {
|
if ([string]::IsNullOrEmpty($Input)) {
|
||||||
return @()
|
return @()
|
||||||
}
|
}
|
||||||
|
|
||||||
$items = $Value -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
|
$items = $Input -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 -Value $Agents
|
$AgentList = Normalize-List -Input $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 -Value $Scripts
|
$ScriptList = Normalize-List -Input $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
|
||||||
}
|
}
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -162,18 +162,9 @@ 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:**
|
**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
|
||||||
|
|
||||||
- `docs` — reads, validates, or generates spec artifacts
|
**Effect:** `Read-only` — produces reports without modifying files · `Read+Write` — modifies files, creates artifacts, or updates specs
|
||||||
- `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 |
|
||||||
|-----------|---------|----------|--------|-----|
|
|-----------|---------|----------|--------|-----|
|
||||||
@@ -190,15 +181,6 @@ 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) |
|
||||||
|
|||||||
@@ -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, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands
|
aliases: [string] # Optional, array of alternate names
|
||||||
|
|
||||||
config: # Optional, array of config files
|
config: # Optional, array of config files
|
||||||
- name: string # Config file name
|
- name: string # Config file name
|
||||||
|
|||||||
@@ -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.my-ext.hi"] # Optional aliases, same pattern
|
aliases: ["speckit.hello"] # Optional aliases
|
||||||
|
|
||||||
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; each must match `speckit.{ext-id}.{command}`)
|
- `aliases`: Alternative command names (optional, array)
|
||||||
|
|
||||||
### Optional Fields
|
### Optional Fields
|
||||||
|
|
||||||
|
|||||||
@@ -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 a namespaced alias (if provided)
|
# Or use short alias (if provided)
|
||||||
> /speckit.jira.sync
|
> /speckit.specstoissues
|
||||||
```
|
```
|
||||||
|
|
||||||
### Extension Configuration
|
### Extension Configuration
|
||||||
|
|||||||
@@ -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.jira.sync"] # Alternate names
|
aliases: ["speckit.specstoissues"] # 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
|
||||||
```
|
```
|
||||||
|
|
||||||
**Migration alias** (if needed):
|
**Compatibility shim** (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.github.sync-taskstoissues"] # Alternate namespaced entry point
|
aliases: ["speckit.taskstoissues"] # Backward compatibility
|
||||||
```
|
```
|
||||||
|
|
||||||
AI agents register both names, so callers can migrate to the alternate alias without relying on deprecated global shortcuts like `/speckit.taskstoissues`.
|
AI agent registers both names, so old scripts work.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-03-27T08:22:30Z",
|
"updated_at": "2026-03-19T12:08:20Z",
|
||||||
"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 \u2014 from vision through implementation.",
|
"description": "A structured 7-step workflow for building new projects from scratch with AI assistants — 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 \u2014 with quality gates, backpropagation verification, and self-healing",
|
"description": "Multi-agent cognitive system with Triadic Model: understanding, internalization, application — 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 \u2014 refine specs mid-implementation and go straight back to building",
|
"description": "Iterate on spec documents with a two-phase define-and-apply workflow — 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,327 +437,6 @@
|
|||||||
"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",
|
||||||
@@ -959,38 +638,6 @@
|
|||||||
"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",
|
||||||
@@ -1064,6 +711,38 @@
|
|||||||
"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",
|
||||||
@@ -1096,6 +775,37 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -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 in the same namespaced format
|
# Optional: Add aliases for shorter command names
|
||||||
aliases: ["speckit.my-extension.example-short"]
|
aliases: ["speckit.example"]
|
||||||
|
|
||||||
# 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"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
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
|
||||||
@@ -15,9 +14,6 @@ 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
|
||||||
@@ -49,11 +45,10 @@ while [ $i -le $# ]; do
|
|||||||
USE_TIMESTAMP=true
|
USE_TIMESTAMP=true
|
||||||
;;
|
;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
echo "Usage: $0 [--json] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
|
echo "Usage: $0 [--json] [--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"
|
||||||
@@ -74,7 +69,7 @@ done
|
|||||||
|
|
||||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||||
echo "Usage: $0 [--json] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
|
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -292,19 +287,12 @@ 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 [ "$ALLOW_EXISTING" = true ]; then
|
if [ "$USE_TIMESTAMP" = 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
|
||||||
@@ -317,15 +305,13 @@ 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 [ ! -f "$SPEC_FILE" ]; then
|
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
|
||||||
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
|
cp "$TEMPLATE" "$SPEC_FILE"
|
||||||
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
|
else
|
||||||
cp "$TEMPLATE" "$SPEC_FILE"
|
echo "Warning: Spec template not found; created empty spec file" >&2
|
||||||
else
|
touch "$SPEC_FILE"
|
||||||
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
|
||||||
|
|||||||
@@ -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/copilot-instructions.md"
|
COPILOT_FILE="$REPO_ROOT/.github/agents/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"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[switch]$Json,
|
[switch]$Json,
|
||||||
[switch]$AllowExistingBranch,
|
|
||||||
[string]$ShortName,
|
[string]$ShortName,
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[long]$Number = 0,
|
[long]$Number = 0,
|
||||||
@@ -16,11 +15,10 @@ $ErrorActionPreference = 'Stop'
|
|||||||
|
|
||||||
# Show help if requested
|
# Show help if requested
|
||||||
if ($Help) {
|
if ($Help) {
|
||||||
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-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"
|
||||||
@@ -35,7 +33,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] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,20 +251,12 @@ 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 ($AllowExistingBranch) {
|
if ($Timestamp) {
|
||||||
# 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
|
||||||
@@ -279,14 +269,12 @@ 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 (-not (Test-Path -PathType Leaf $specFile)) {
|
if ($template -and (Test-Path $template)) {
|
||||||
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
|
Copy-Item $template $specFile -Force
|
||||||
if ($template -and (Test-Path $template)) {
|
} else {
|
||||||
Copy-Item $template $specFile -Force
|
New-Item -ItemType File -Path $specFile | Out-Null
|
||||||
} 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
|
||||||
|
|||||||
@@ -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/copilot-instructions.md'
|
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/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'
|
||||||
|
|||||||
@@ -345,7 +345,6 @@ 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 = """
|
||||||
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
|
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
|
||||||
@@ -606,15 +605,13 @@ 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 local installs
|
# Special handling for Claude CLI after `claude migrate-installer`
|
||||||
# See: https://github.com/github/spec-kit/issues/123
|
# See: https://github.com/github/spec-kit/issues/123
|
||||||
# See: https://github.com/github/spec-kit/issues/550
|
# The migrate-installer command REMOVES the original executable from PATH
|
||||||
# Claude Code can be installed in two local paths:
|
# and creates an alias at ~/.claude/local/claude instead
|
||||||
# 1. ~/.claude/local/claude (after `claude migrate-installer`)
|
# This path should be prioritized over other claude executables in PATH
|
||||||
# 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.is_file() or CLAUDE_NPM_LOCAL_PATH.is_file():
|
if CLAUDE_LOCAL_PATH.exists() and CLAUDE_LOCAL_PATH.is_file():
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.complete(tool, "available")
|
tracker.complete(tool, "available")
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -25,49 +25,6 @@ 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."""
|
||||||
@@ -192,7 +149,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 EXTENSION_COMMAND_NAME_PATTERN.match(cmd["name"]) is None:
|
if not re.match(r'^speckit\.[a-z0-9-]+\.[a-z0-9-]+$', cmd["name"]):
|
||||||
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}'"
|
||||||
@@ -489,126 +446,6 @@ 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.
|
||||||
@@ -1024,9 +861,6 @@ 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():
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -18,7 +18,6 @@ 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,
|
||||||
@@ -64,7 +63,7 @@ def valid_manifest_data():
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.test-ext.hello",
|
"name": "speckit.test.hello",
|
||||||
"file": "commands/hello.md",
|
"file": "commands/hello.md",
|
||||||
"description": "Test command",
|
"description": "Test command",
|
||||||
}
|
}
|
||||||
@@ -72,7 +71,7 @@ def valid_manifest_data():
|
|||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"after_tasks": {
|
"after_tasks": {
|
||||||
"command": "speckit.test-ext.hello",
|
"command": "speckit.test.hello",
|
||||||
"optional": True,
|
"optional": True,
|
||||||
"prompt": "Run test?",
|
"prompt": "Run test?",
|
||||||
}
|
}
|
||||||
@@ -190,18 +189,7 @@ 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-ext.hello"
|
assert manifest.commands[0]["name"] == "speckit.test.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."""
|
||||||
@@ -601,172 +589,6 @@ 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)
|
||||||
@@ -1030,10 +852,10 @@ $ARGUMENTS
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert len(registered) == 1
|
assert len(registered) == 1
|
||||||
assert "speckit.test-ext.hello" in registered
|
assert "speckit.test.hello" in registered
|
||||||
|
|
||||||
# Check command file was created
|
# Check command file was created
|
||||||
cmd_file = claude_dir / "speckit.test-ext.hello.md"
|
cmd_file = claude_dir / "speckit.test.hello.md"
|
||||||
assert cmd_file.exists()
|
assert cmd_file.exists()
|
||||||
|
|
||||||
content = cmd_file.read_text()
|
content = cmd_file.read_text()
|
||||||
@@ -1063,9 +885,9 @@ $ARGUMENTS
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.ext-alias.cmd",
|
"name": "speckit.alias.cmd",
|
||||||
"file": "commands/cmd.md",
|
"file": "commands/cmd.md",
|
||||||
"aliases": ["speckit.ext-alias.shortcut"],
|
"aliases": ["speckit.shortcut"],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1085,10 +907,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.ext-alias.cmd" in registered
|
assert "speckit.alias.cmd" in registered
|
||||||
assert "speckit.ext-alias.shortcut" in registered
|
assert "speckit.shortcut" in registered
|
||||||
assert (claude_dir / "speckit.ext-alias.cmd.md").exists()
|
assert (claude_dir / "speckit.alias.cmd.md").exists()
|
||||||
assert (claude_dir / "speckit.ext-alias.shortcut.md").exists()
|
assert (claude_dir / "speckit.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."""
|
||||||
@@ -1129,11 +951,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-ext-hello" / "SKILL.md"
|
skill_file = skills_dir / "speckit-test-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-ext-hello" in content
|
assert "name: speckit-test-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
|
||||||
@@ -1160,7 +982,7 @@ $ARGUMENTS
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.ext-scripted.plan",
|
"name": "speckit.test.plan",
|
||||||
"file": "commands/plan.md",
|
"file": "commands/plan.md",
|
||||||
"description": "Scripted command",
|
"description": "Scripted command",
|
||||||
}
|
}
|
||||||
@@ -1198,7 +1020,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-ext-scripted-plan" / "SKILL.md"
|
skill_file = skills_dir / "speckit-test-plan" / "SKILL.md"
|
||||||
assert skill_file.exists()
|
assert skill_file.exists()
|
||||||
|
|
||||||
content = skill_file.read_text()
|
content = skill_file.read_text()
|
||||||
@@ -1229,9 +1051,9 @@ Agent __AGENT__
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.ext-alias-skill.cmd",
|
"name": "speckit.alias.cmd",
|
||||||
"file": "commands/cmd.md",
|
"file": "commands/cmd.md",
|
||||||
"aliases": ["speckit.ext-alias-skill.shortcut"],
|
"aliases": ["speckit.shortcut"],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1248,13 +1070,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-ext-alias-skill-cmd" / "SKILL.md"
|
primary = skills_dir / "speckit-alias-cmd" / "SKILL.md"
|
||||||
alias = skills_dir / "speckit-ext-alias-skill-shortcut" / "SKILL.md"
|
alias = skills_dir / "speckit-shortcut" / "SKILL.md"
|
||||||
|
|
||||||
assert primary.exists()
|
assert primary.exists()
|
||||||
assert alias.exists()
|
assert alias.exists()
|
||||||
assert "name: speckit-ext-alias-skill-cmd" in primary.read_text()
|
assert "name: speckit-alias-cmd" in primary.read_text()
|
||||||
assert "name: speckit-ext-alias-skill-shortcut" in alias.read_text()
|
assert "name: speckit-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
|
||||||
@@ -1278,7 +1100,7 @@ Agent __AGENT__
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.ext-script-fallback.plan",
|
"name": "speckit.fallback.plan",
|
||||||
"file": "commands/plan.md",
|
"file": "commands/plan.md",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1310,7 +1132,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-ext-script-fallback-plan" / "SKILL.md"
|
skill_file = skills_dir / "speckit-fallback-plan" / "SKILL.md"
|
||||||
assert skill_file.exists()
|
assert skill_file.exists()
|
||||||
|
|
||||||
content = skill_file.read_text()
|
content = skill_file.read_text()
|
||||||
@@ -1341,7 +1163,7 @@ Then {AGENT_SCRIPT}
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.ext-script-list-init.plan",
|
"name": "speckit.list.plan",
|
||||||
"file": "commands/plan.md",
|
"file": "commands/plan.md",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1372,7 +1194,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-ext-script-list-init-plan" / "SKILL.md").read_text()
|
content = (skills_dir / "speckit-list-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(
|
||||||
@@ -1399,7 +1221,7 @@ Run {SCRIPT}
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.ext-script-windows-fallback.plan",
|
"name": "speckit.windows.plan",
|
||||||
"file": "commands/plan.md",
|
"file": "commands/plan.md",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1431,7 +1253,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-ext-script-windows-fallback-plan" / "SKILL.md"
|
skill_file = skills_dir / "speckit-windows-plan" / "SKILL.md"
|
||||||
assert skill_file.exists()
|
assert skill_file.exists()
|
||||||
|
|
||||||
content = skill_file.read_text()
|
content = skill_file.read_text()
|
||||||
@@ -1453,14 +1275,14 @@ Then {AGENT_SCRIPT}
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert len(registered) == 1
|
assert len(registered) == 1
|
||||||
assert "speckit.test-ext.hello" in registered
|
assert "speckit.test.hello" in registered
|
||||||
|
|
||||||
# Verify command file uses .agent.md extension
|
# Verify command file uses .agent.md extension
|
||||||
cmd_file = agents_dir / "speckit.test-ext.hello.agent.md"
|
cmd_file = agents_dir / "speckit.test.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-ext.hello.md"
|
plain_md_file = agents_dir / "speckit.test.hello.md"
|
||||||
assert not plain_md_file.exists()
|
assert not plain_md_file.exists()
|
||||||
|
|
||||||
content = cmd_file.read_text()
|
content = cmd_file.read_text()
|
||||||
@@ -1480,12 +1302,12 @@ Then {AGENT_SCRIPT}
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Verify companion .prompt.md file exists
|
# Verify companion .prompt.md file exists
|
||||||
prompt_file = project_dir / ".github" / "prompts" / "speckit.test-ext.hello.prompt.md"
|
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.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-ext.hello\n---\n"
|
assert content == "---\nagent: speckit.test.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."""
|
||||||
@@ -1506,9 +1328,9 @@ Then {AGENT_SCRIPT}
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.ext-alias-copilot.cmd",
|
"name": "speckit.alias-copilot.cmd",
|
||||||
"file": "commands/cmd.md",
|
"file": "commands/cmd.md",
|
||||||
"aliases": ["speckit.ext-alias-copilot.shortcut"],
|
"aliases": ["speckit.shortcut-copilot"],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1535,8 +1357,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.ext-alias-copilot.cmd.prompt.md").exists()
|
assert (prompts_dir / "speckit.alias-copilot.cmd.prompt.md").exists()
|
||||||
assert (prompts_dir / "speckit.ext-alias-copilot.shortcut.prompt.md").exists()
|
assert (prompts_dir / "speckit.shortcut-copilot.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."""
|
||||||
@@ -1609,7 +1431,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-ext.hello.md"
|
cmd_file = project_dir / ".claude" / "commands" / "speckit.test.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)
|
||||||
@@ -1617,7 +1439,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-ext.hello" in cmds
|
"speckit.test.hello" in cmds
|
||||||
for cmds in registered_commands.values()
|
for cmds in registered_commands.values()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1643,8 +1465,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-ext.hello.agent.md"
|
agent_file = agents_dir / "speckit.test.hello.agent.md"
|
||||||
prompt_file = project_dir / ".github" / "prompts" / "speckit.test-ext.hello.prompt.md"
|
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
|
||||||
assert agent_file.exists()
|
assert agent_file.exists()
|
||||||
assert prompt_file.exists()
|
assert prompt_file.exists()
|
||||||
|
|
||||||
@@ -2954,7 +2776,7 @@ class TestExtensionUpdateCLI:
|
|||||||
"provides": {
|
"provides": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"name": "speckit.test-ext.hello",
|
"name": "speckit.test.hello",
|
||||||
"file": "commands/hello.md",
|
"file": "commands/hello.md",
|
||||||
"description": "Test command",
|
"description": "Test command",
|
||||||
}
|
}
|
||||||
@@ -2962,7 +2784,7 @@ class TestExtensionUpdateCLI:
|
|||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"after_tasks": {
|
"after_tasks": {
|
||||||
"command": "speckit.test-ext.hello",
|
"command": "speckit.test.hello",
|
||||||
"optional": True,
|
"optional": True,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2991,7 +2813,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-ext.hello", "file": "commands/hello.md"}]},
|
"provides": {"commands": [{"name": "speckit.test.hello", "file": "commands/hello.md"}]},
|
||||||
}
|
}
|
||||||
|
|
||||||
with zipfile.ZipFile(zip_path, "w") as zf:
|
with zipfile.ZipFile(zip_path, "w") as zf:
|
||||||
@@ -3620,15 +3442,15 @@ class TestHookInvocationRendering:
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"extension": "test-ext",
|
"extension": "test-ext",
|
||||||
"command": "speckit.test-ext.hello",
|
"command": "speckit.test.hello",
|
||||||
"optional": False,
|
"optional": False,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "Executing: `/skill:speckit-test-ext-hello`" in message
|
assert "Executing: `/skill:speckit-test-hello`" in message
|
||||||
assert "EXECUTE_COMMAND: speckit.test-ext.hello" in message
|
assert "EXECUTE_COMMAND: speckit.test.hello" in message
|
||||||
assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-test-ext-hello" in message
|
assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-test-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."""
|
||||||
|
|||||||
@@ -269,146 +269,3 @@ 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", "")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user