mirror of
https://github.com/github/spec-kit.git
synced 2026-07-08 23:59:55 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2499c9a1a6 | ||
|
|
43cb0fa7ab | ||
|
|
74e3f45aa9 | ||
|
|
97ea7cf6a0 | ||
|
|
f43b85096c | ||
|
|
d1b95c2f59 | ||
|
|
8bb08ae1a0 | ||
|
|
5732de60d0 | ||
|
|
b6e19b49ec | ||
|
|
7f1e38491f | ||
|
|
bc0288832e | ||
|
|
e70495c2b8 |
29
CHANGELOG.md
29
CHANGELOG.md
@@ -2,6 +2,35 @@
|
|||||||
|
|
||||||
<!-- insert new changelog below this comment -->
|
<!-- insert new changelog below this comment -->
|
||||||
|
|
||||||
|
## [0.6.1] - 2026-04-10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- feat: add bundled lean preset with minimal workflow commands (#2161)
|
||||||
|
- Add Brownfield Bootstrap extension to community catalog (#2145)
|
||||||
|
- Add CI Guard extension to community catalog (#2157)
|
||||||
|
- Add SpecTest extension to community catalog (#2159)
|
||||||
|
- fix: bundled extensions should not have download URLs (#2155)
|
||||||
|
- Add PR Bridge extension to community catalog (#2148)
|
||||||
|
- feat(cursor-agent): migrate from .cursor/commands to .cursor/skills (#2156)
|
||||||
|
- Add TinySpec extension to community catalog (#2147)
|
||||||
|
- chore: bump spec-kit-verify to 1.0.3 and spec-kit-review to 1.0.1 (#2146)
|
||||||
|
- Add Status Report extension to community catalog (#2123)
|
||||||
|
- chore: release 0.6.0, begin 0.6.1.dev0 development (#2144)
|
||||||
|
|
||||||
|
## [0.6.0] - 2026-04-09
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Add Bugfix Workflow community extension to catalog and README (#2135)
|
||||||
|
- Add Worktree Isolation extension to community catalog (#2143)
|
||||||
|
- Add multi-repo-branching preset to community catalog (#2139)
|
||||||
|
- Readme clarity (#2013)
|
||||||
|
- Rewrite AGENTS.md for integration architecture (#2119)
|
||||||
|
- docs: add SpecKit Companion to Community Friends section (#2140)
|
||||||
|
- feat: add memorylint extension to community catalog (#2138)
|
||||||
|
- chore: release 0.5.1, begin 0.5.2.dev0 development (#2137)
|
||||||
|
|
||||||
## [0.5.1] - 2026-04-08
|
## [0.5.1] - 2026-04-08
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -186,8 +186,10 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
|
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
|
||||||
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
||||||
| Branch Convention | Configurable branch and folder naming conventions for /specify with presets and custom patterns | `process` | Read+Write | [spec-kit-branch-convention](https://github.com/Quratulain-bilal/spec-kit-branch-convention) |
|
| Branch Convention | Configurable branch and folder naming conventions for /specify with presets and custom patterns | `process` | Read+Write | [spec-kit-branch-convention](https://github.com/Quratulain-bilal/spec-kit-branch-convention) |
|
||||||
|
| Brownfield Bootstrap | Bootstrap spec-kit for existing codebases — auto-discover architecture and adopt SDD incrementally | `process` | Read+Write | [spec-kit-brownfield](https://github.com/Quratulain-bilal/spec-kit-brownfield) |
|
||||||
| Bugfix Workflow | Structured bugfix workflow — capture bugs, trace to spec artifacts, and patch specs surgically | `process` | Read+Write | [spec-kit-bugfix](https://github.com/Quratulain-bilal/spec-kit-bugfix) |
|
| Bugfix Workflow | Structured bugfix workflow — capture bugs, trace to spec artifacts, and patch specs surgically | `process` | Read+Write | [spec-kit-bugfix](https://github.com/Quratulain-bilal/spec-kit-bugfix) |
|
||||||
| Canon | Adds canon-driven (baseline-driven) workflows: spec-first, code-first, spec-drift. Requires Canon Core preset installation. | `process` | Read+Write | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon/tree/master/extension) |
|
| Canon | Adds canon-driven (baseline-driven) workflows: spec-first, code-first, spec-drift. Requires Canon Core preset installation. | `process` | Read+Write | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon/tree/master/extension) |
|
||||||
|
| CI Guard | Spec compliance gates for CI/CD — verify specs exist, check drift, and block merges on gaps | `process` | Read-only | [spec-kit-ci-guard](https://github.com/Quratulain-bilal/spec-kit-ci-guard) |
|
||||||
| Checkpoint Extension | Commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end | `code` | Read+Write | [spec-kit-checkpoint](https://github.com/aaronrsun/spec-kit-checkpoint) |
|
| Checkpoint Extension | Commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end | `code` | Read+Write | [spec-kit-checkpoint](https://github.com/aaronrsun/spec-kit-checkpoint) |
|
||||||
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | `code` | Read+Write | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | `code` | Read+Write | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
||||||
| Conduct Extension | Orchestrates spec-kit phases via sub-agent delegation to reduce context pollution. | `process` | Read+Write | [spec-kit-conduct-ext](https://github.com/twbrandon7/spec-kit-conduct-ext) |
|
| Conduct Extension | Orchestrates spec-kit phases via sub-agent delegation to reduce context pollution. | `process` | Read+Write | [spec-kit-conduct-ext](https://github.com/twbrandon7/spec-kit-conduct-ext) |
|
||||||
@@ -211,6 +213,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
| 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) |
|
| Onboard | Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step | `process` | Read+Write | [spec-kit-onboard](https://github.com/dmux/spec-kit-onboard) |
|
||||||
| Optimize | Audit and optimize AI governance for context efficiency — token budgets, rule health, interpretability, compression, coherence, and echo detection | `process` | Read+Write | [spec-kit-optimize](https://github.com/sakitA/spec-kit-optimize) |
|
| Optimize | Audit and optimize AI governance for context efficiency — token budgets, rule health, interpretability, compression, coherence, and echo detection | `process` | Read+Write | [spec-kit-optimize](https://github.com/sakitA/spec-kit-optimize) |
|
||||||
| Plan Review Gate | Require spec.md and plan.md to be merged via MR/PR before allowing task generation | `process` | Read-only | [spec-kit-plan-review-gate](https://github.com/luno/spec-kit-plan-review-gate) |
|
| Plan Review Gate | Require spec.md and plan.md to be merged via MR/PR before allowing task generation | `process` | Read-only | [spec-kit-plan-review-gate](https://github.com/luno/spec-kit-plan-review-gate) |
|
||||||
|
| PR Bridge | Auto-generate pull request descriptions, checklists, and summaries from spec artifacts | `process` | Read-only | [spec-kit-pr-bridge-](https://github.com/Quratulain-bilal/spec-kit-pr-bridge-) |
|
||||||
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
|
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
|
||||||
| Product Forge | Full product lifecycle: research → product spec → SpecKit → implement → verify → test | `process` | Read+Write | [speckit-product-forge](https://github.com/VaiYav/speckit-product-forge) |
|
| Product Forge | Full product lifecycle: research → product spec → SpecKit → implement → verify → test | `process` | Read+Write | [speckit-product-forge](https://github.com/VaiYav/speckit-product-forge) |
|
||||||
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
|
| 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) |
|
||||||
@@ -229,8 +232,11 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
| Spec Diagram | Auto-generate Mermaid diagrams of SDD workflow state, feature progress, and task dependencies | `visibility` | Read-only | [spec-kit-diagram-](https://github.com/Quratulain-bilal/spec-kit-diagram-) |
|
| Spec Diagram | Auto-generate Mermaid diagrams of SDD workflow state, feature progress, and task dependencies | `visibility` | Read-only | [spec-kit-diagram-](https://github.com/Quratulain-bilal/spec-kit-diagram-) |
|
||||||
| Spec Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
|
| Spec Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
|
||||||
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
||||||
|
| SpecTest | Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements | `code` | Read+Write | [spec-kit-spectest](https://github.com/Quratulain-bilal/spec-kit-spectest) |
|
||||||
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
|
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
|
||||||
|
| Status Report | Project status, feature progress, and next-action recommendations for spec-driven workflows | `visibility` | Read-only | [Open-Agent-Tools/spec-kit-status](https://github.com/Open-Agent-Tools/spec-kit-status) |
|
||||||
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
|
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
|
||||||
|
| TinySpec | Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process | `process` | Read+Write | [spec-kit-tinyspec](https://github.com/Quratulain-bilal/spec-kit-tinyspec) |
|
||||||
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
||||||
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
||||||
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
|
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ This tells Spec Kit which feature directory to use when creating specs, plans, a
|
|||||||
```bash
|
```bash
|
||||||
ls -la .claude/commands/ # Claude Code
|
ls -la .claude/commands/ # Claude Code
|
||||||
ls -la .gemini/commands/ # Gemini
|
ls -la .gemini/commands/ # Gemini
|
||||||
ls -la .cursor/commands/ # Cursor
|
ls -la .cursor/skills/ # Cursor
|
||||||
ls -la .pi/prompts/ # Pi Coding Agent
|
ls -la .pi/prompts/ # Pi Coding Agent
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-04-09T00:00:00Z",
|
"updated_at": "2026-04-10T17:00:00Z",
|
||||||
"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": {
|
||||||
@@ -138,6 +138,38 @@
|
|||||||
"created_at": "2026-04-08T00:00:00Z",
|
"created_at": "2026-04-08T00:00:00Z",
|
||||||
"updated_at": "2026-04-08T00:00:00Z"
|
"updated_at": "2026-04-08T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"brownfield": {
|
||||||
|
"name": "Brownfield Bootstrap",
|
||||||
|
"id": "brownfield",
|
||||||
|
"description": "Bootstrap spec-kit for existing codebases — auto-discover architecture and adopt SDD incrementally.",
|
||||||
|
"author": "Quratulain-bilal",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/Quratulain-bilal/spec-kit-brownfield/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/Quratulain-bilal/spec-kit-brownfield",
|
||||||
|
"homepage": "https://github.com/Quratulain-bilal/spec-kit-brownfield",
|
||||||
|
"documentation": "https://github.com/Quratulain-bilal/spec-kit-brownfield/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/Quratulain-bilal/spec-kit-brownfield/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.4.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 4,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"brownfield",
|
||||||
|
"bootstrap",
|
||||||
|
"existing-project",
|
||||||
|
"migration",
|
||||||
|
"onboarding"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-10T00:00:00Z",
|
||||||
|
"updated_at": "2026-04-10T00:00:00Z"
|
||||||
|
},
|
||||||
"bugfix": {
|
"bugfix": {
|
||||||
"name": "Bugfix Workflow",
|
"name": "Bugfix Workflow",
|
||||||
"id": "bugfix",
|
"id": "bugfix",
|
||||||
@@ -205,6 +237,39 @@
|
|||||||
"created_at": "2026-03-29T00:00:00Z",
|
"created_at": "2026-03-29T00:00:00Z",
|
||||||
"updated_at": "2026-03-29T00:00:00Z"
|
"updated_at": "2026-03-29T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"ci-guard": {
|
||||||
|
"name": "CI Guard",
|
||||||
|
"id": "ci-guard",
|
||||||
|
"description": "Spec compliance gates for CI/CD — verify specs exist, check drift, and block merges on gaps.",
|
||||||
|
"author": "Quratulain-bilal",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/Quratulain-bilal/spec-kit-ci-guard",
|
||||||
|
"homepage": "https://github.com/Quratulain-bilal/spec-kit-ci-guard",
|
||||||
|
"documentation": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.4.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 5,
|
||||||
|
"hooks": 2
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"ci-cd",
|
||||||
|
"compliance",
|
||||||
|
"governance",
|
||||||
|
"quality-gate",
|
||||||
|
"drift-detection",
|
||||||
|
"automation"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-10T17:00:00Z",
|
||||||
|
"updated_at": "2026-04-10T17:00:00Z"
|
||||||
|
},
|
||||||
"checkpoint": {
|
"checkpoint": {
|
||||||
"name": "Checkpoint Extension",
|
"name": "Checkpoint Extension",
|
||||||
"id": "checkpoint",
|
"id": "checkpoint",
|
||||||
@@ -1030,6 +1095,38 @@
|
|||||||
"created_at": "2026-03-27T08:22:30Z",
|
"created_at": "2026-03-27T08:22:30Z",
|
||||||
"updated_at": "2026-03-27T08:22:30Z"
|
"updated_at": "2026-03-27T08:22:30Z"
|
||||||
},
|
},
|
||||||
|
"pr-bridge": {
|
||||||
|
"name": "PR Bridge",
|
||||||
|
"id": "pr-bridge",
|
||||||
|
"description": "Auto-generate pull request descriptions, checklists, and summaries from spec artifacts.",
|
||||||
|
"author": "Quratulain-bilal",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-",
|
||||||
|
"homepage": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-",
|
||||||
|
"documentation": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.4.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 3,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"pull-request",
|
||||||
|
"automation",
|
||||||
|
"traceability",
|
||||||
|
"workflow",
|
||||||
|
"review"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-10T00:00:00Z",
|
||||||
|
"updated_at": "2026-04-10T00:00:00Z"
|
||||||
|
},
|
||||||
"presetify": {
|
"presetify": {
|
||||||
"name": "Presetify",
|
"name": "Presetify",
|
||||||
"id": "presetify",
|
"id": "presetify",
|
||||||
@@ -1331,8 +1428,8 @@
|
|||||||
"id": "review",
|
"id": "review",
|
||||||
"description": "Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification.",
|
"description": "Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification.",
|
||||||
"author": "ismaelJimenez",
|
"author": "ismaelJimenez",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"download_url": "https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.0.zip",
|
"download_url": "https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.1.zip",
|
||||||
"repository": "https://github.com/ismaelJimenez/spec-kit-review",
|
"repository": "https://github.com/ismaelJimenez/spec-kit-review",
|
||||||
"homepage": "https://github.com/ismaelJimenez/spec-kit-review",
|
"homepage": "https://github.com/ismaelJimenez/spec-kit-review",
|
||||||
"documentation": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/README.md",
|
"documentation": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/README.md",
|
||||||
@@ -1358,7 +1455,7 @@
|
|||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"stars": 0,
|
"stars": 0,
|
||||||
"created_at": "2026-03-06T00:00:00Z",
|
"created_at": "2026-03-06T00:00:00Z",
|
||||||
"updated_at": "2026-03-06T00:00:00Z"
|
"updated_at": "2026-04-09T00:00:00Z"
|
||||||
},
|
},
|
||||||
"security-review": {
|
"security-review": {
|
||||||
"name": "Security Review",
|
"name": "Security Review",
|
||||||
@@ -1454,6 +1551,39 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
|
"spectest": {
|
||||||
|
"name": "SpecTest",
|
||||||
|
"id": "spectest",
|
||||||
|
"description": "Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements.",
|
||||||
|
"author": "Quratulain-bilal",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/Quratulain-bilal/spec-kit-spectest/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/Quratulain-bilal/spec-kit-spectest",
|
||||||
|
"homepage": "https://github.com/Quratulain-bilal/spec-kit-spectest",
|
||||||
|
"documentation": "https://github.com/Quratulain-bilal/spec-kit-spectest/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/Quratulain-bilal/spec-kit-spectest/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.4.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 4,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"testing",
|
||||||
|
"test-generation",
|
||||||
|
"coverage",
|
||||||
|
"quality",
|
||||||
|
"automation",
|
||||||
|
"traceability"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-10T16:00:00Z",
|
||||||
|
"updated_at": "2026-04-10T16:00:00Z"
|
||||||
|
},
|
||||||
"staff-review": {
|
"staff-review": {
|
||||||
"name": "Staff Review Extension",
|
"name": "Staff Review Extension",
|
||||||
"id": "staff-review",
|
"id": "staff-review",
|
||||||
@@ -1516,6 +1646,36 @@
|
|||||||
"created_at": "2026-03-16T00:00:00Z",
|
"created_at": "2026-03-16T00:00:00Z",
|
||||||
"updated_at": "2026-03-16T00:00:00Z"
|
"updated_at": "2026-03-16T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"status-report": {
|
||||||
|
"name": "Status Report",
|
||||||
|
"id": "status-report",
|
||||||
|
"description": "Project status, feature progress, and next-action recommendations for spec-driven workflows.",
|
||||||
|
"author": "Open-Agent-Tools",
|
||||||
|
"version": "1.2.5",
|
||||||
|
"download_url": "https://github.com/Open-Agent-Tools/spec-kit-status/archive/refs/tags/v1.2.5.zip",
|
||||||
|
"repository": "https://github.com/Open-Agent-Tools/spec-kit-status",
|
||||||
|
"homepage": "https://github.com/Open-Agent-Tools/spec-kit-status",
|
||||||
|
"documentation": "https://github.com/Open-Agent-Tools/spec-kit-status/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/Open-Agent-Tools/spec-kit-status/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 1,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"workflow",
|
||||||
|
"project-management",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-08T15:05:14Z",
|
||||||
|
"updated_at": "2026-04-08T15:05:14Z"
|
||||||
|
},
|
||||||
"superb": {
|
"superb": {
|
||||||
"name": "Superpowers Bridge",
|
"name": "Superpowers Bridge",
|
||||||
"id": "superb",
|
"id": "superb",
|
||||||
@@ -1591,6 +1751,38 @@
|
|||||||
"created_at": "2026-03-02T00:00:00Z",
|
"created_at": "2026-03-02T00:00:00Z",
|
||||||
"updated_at": "2026-03-02T00:00:00Z"
|
"updated_at": "2026-03-02T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"tinyspec": {
|
||||||
|
"name": "TinySpec",
|
||||||
|
"id": "tinyspec",
|
||||||
|
"description": "Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process.",
|
||||||
|
"author": "Quratulain-bilal",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/Quratulain-bilal/spec-kit-tinyspec",
|
||||||
|
"homepage": "https://github.com/Quratulain-bilal/spec-kit-tinyspec",
|
||||||
|
"documentation": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.4.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 3,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"lightweight",
|
||||||
|
"small-tasks",
|
||||||
|
"workflow",
|
||||||
|
"productivity",
|
||||||
|
"efficiency"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-10T00:00:00Z",
|
||||||
|
"updated_at": "2026-04-10T00:00:00Z"
|
||||||
|
},
|
||||||
"v-model": {
|
"v-model": {
|
||||||
"name": "V-Model Extension Pack",
|
"name": "V-Model Extension Pack",
|
||||||
"id": "v-model",
|
"id": "v-model",
|
||||||
@@ -1628,8 +1820,8 @@
|
|||||||
"id": "verify",
|
"id": "verify",
|
||||||
"description": "Post-implementation quality gate that validates implemented code against specification artifacts.",
|
"description": "Post-implementation quality gate that validates implemented code against specification artifacts.",
|
||||||
"author": "ismaelJimenez",
|
"author": "ismaelJimenez",
|
||||||
"version": "1.0.0",
|
"version": "1.0.3",
|
||||||
"download_url": "https://github.com/ismaelJimenez/spec-kit-verify/archive/refs/tags/v1.0.0.zip",
|
"download_url": "https://github.com/ismaelJimenez/spec-kit-verify/archive/refs/tags/v1.0.3.zip",
|
||||||
"repository": "https://github.com/ismaelJimenez/spec-kit-verify",
|
"repository": "https://github.com/ismaelJimenez/spec-kit-verify",
|
||||||
"homepage": "https://github.com/ismaelJimenez/spec-kit-verify",
|
"homepage": "https://github.com/ismaelJimenez/spec-kit-verify",
|
||||||
"documentation": "https://github.com/ismaelJimenez/spec-kit-verify/blob/main/README.md",
|
"documentation": "https://github.com/ismaelJimenez/spec-kit-verify/blob/main/README.md",
|
||||||
@@ -1653,7 +1845,7 @@
|
|||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"stars": 0,
|
"stars": 0,
|
||||||
"created_at": "2026-03-03T00:00:00Z",
|
"created_at": "2026-03-03T00:00:00Z",
|
||||||
"updated_at": "2026-03-03T00:00:00Z"
|
"updated_at": "2026-04-09T00:00:00Z"
|
||||||
},
|
},
|
||||||
"verify-tasks": {
|
"verify-tasks": {
|
||||||
"name": "Verify Tasks Extension",
|
"name": "Verify Tasks Extension",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-04-06T00:00:00Z",
|
"updated_at": "2026-04-10T00:00:00Z",
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"git": {
|
"git": {
|
||||||
@@ -10,27 +10,13 @@
|
|||||||
"description": "Feature branch creation, numbering (sequential/timestamp), validation, and Git remote detection",
|
"description": "Feature branch creation, numbering (sequential/timestamp), validation, and Git remote detection",
|
||||||
"author": "spec-kit-core",
|
"author": "spec-kit-core",
|
||||||
"repository": "https://github.com/github/spec-kit",
|
"repository": "https://github.com/github/spec-kit",
|
||||||
"download_url": "https://github.com/github/spec-kit/releases/download/ext-git-v1.0.0/git.zip",
|
"bundled": true,
|
||||||
"tags": [
|
"tags": [
|
||||||
"git",
|
"git",
|
||||||
"branching",
|
"branching",
|
||||||
"workflow",
|
"workflow",
|
||||||
"core"
|
"core"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"selftest": {
|
|
||||||
"name": "Spec Kit Self-Test Utility",
|
|
||||||
"id": "selftest",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Verifies catalog extensions by programmatically walking through the discovery, installation, and registration lifecycle.",
|
|
||||||
"author": "spec-kit-core",
|
|
||||||
"repository": "https://github.com/github/spec-kit",
|
|
||||||
"download_url": "https://github.com/github/spec-kit/releases/download/selftest-v1.0.0/selftest.zip",
|
|
||||||
"tags": [
|
|
||||||
"testing",
|
|
||||||
"core",
|
|
||||||
"utility"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,22 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-03-10T00:00:00Z",
|
"updated_at": "2026-04-10T00:00:00Z",
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.json",
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.json",
|
||||||
"presets": {}
|
"presets": {
|
||||||
|
"lean": {
|
||||||
|
"name": "Lean Workflow",
|
||||||
|
"id": "lean",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Minimal core workflow commands - just the prompt, just the artifact",
|
||||||
|
"author": "github",
|
||||||
|
"repository": "https://github.com/github/spec-kit",
|
||||||
|
"bundled": true,
|
||||||
|
"tags": [
|
||||||
|
"lean",
|
||||||
|
"minimal",
|
||||||
|
"workflow",
|
||||||
|
"core"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
presets/lean/commands/speckit.constitution.md
Normal file
15
presets/lean/commands/speckit.constitution.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
description: Create or update the project constitution.
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outline
|
||||||
|
|
||||||
|
1. Create or update the project constitution and store it in `.specify/memory/constitution.md`.
|
||||||
|
- Project name, guiding principles, non-negotiable rules
|
||||||
|
- Derive from user input and existing repo context (README, docs)
|
||||||
22
presets/lean/commands/speckit.implement.md
Normal file
22
presets/lean/commands/speckit.implement.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
description: Execute the implementation plan by processing all tasks in tasks.md.
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outline
|
||||||
|
|
||||||
|
1. Read `.specify/feature.json` to get the feature directory path.
|
||||||
|
|
||||||
|
2. **Load context**: `.specify/memory/constitution.md` and `<feature_directory>/spec.md` and `<feature_directory>/plan.md` and `<feature_directory>/tasks.md`.
|
||||||
|
|
||||||
|
3. **Execute tasks** in order:
|
||||||
|
- Complete each task before moving to the next
|
||||||
|
- Mark completed tasks by changing `- [ ]` to `- [x]` in `<feature_directory>/tasks.md`
|
||||||
|
- Halt on failure and report the issue
|
||||||
|
|
||||||
|
4. **Validate**: Verify all tasks are completed and the implementation matches the spec.
|
||||||
19
presets/lean/commands/speckit.plan.md
Normal file
19
presets/lean/commands/speckit.plan.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
description: Create a plan and store it in plan.md.
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outline
|
||||||
|
|
||||||
|
1. Read `.specify/feature.json` to get the feature directory path.
|
||||||
|
|
||||||
|
2. **Load context**: `.specify/memory/constitution.md` and `<feature_directory>/spec.md`.
|
||||||
|
|
||||||
|
3. Create an implementation plan and store it in `<feature_directory>/plan.md`.
|
||||||
|
- Technical context: tech stack, dependencies, project structure
|
||||||
|
- Design decisions, architecture, file structure
|
||||||
23
presets/lean/commands/speckit.specify.md
Normal file
23
presets/lean/commands/speckit.specify.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
description: Create a specification and store it in spec.md.
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outline
|
||||||
|
|
||||||
|
1. **Ask the user** for the feature directory path (e.g., `specs/my-feature`). Do not proceed until provided.
|
||||||
|
|
||||||
|
2. Create the directory and write `.specify/feature.json`:
|
||||||
|
```json
|
||||||
|
{ "feature_directory": "<feature_directory>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a specification from the user input and store it in `<feature_directory>/spec.md`.
|
||||||
|
- Overview, functional requirements, user scenarios, success criteria
|
||||||
|
- Every requirement must be testable
|
||||||
|
- Make informed defaults for unspecified details
|
||||||
19
presets/lean/commands/speckit.tasks.md
Normal file
19
presets/lean/commands/speckit.tasks.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
description: Create the tasks needed for implementation and store them in tasks.md.
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outline
|
||||||
|
|
||||||
|
1. Read `.specify/feature.json` to get the feature directory path.
|
||||||
|
|
||||||
|
2. **Load context**: `.specify/memory/constitution.md` and `<feature_directory>/spec.md` and `<feature_directory>/plan.md`.
|
||||||
|
|
||||||
|
3. Create dependency-ordered implementation tasks and store them in `<feature_directory>/tasks.md`.
|
||||||
|
- Every task uses checklist format: `- [ ] [TaskID] Description with file path`
|
||||||
|
- Organized by phase: setup, foundational, user stories in priority order, polish
|
||||||
50
presets/lean/preset.yml
Normal file
50
presets/lean/preset.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
schema_version: "1.0"
|
||||||
|
|
||||||
|
preset:
|
||||||
|
id: "lean"
|
||||||
|
name: "Lean Workflow"
|
||||||
|
version: "1.0.0"
|
||||||
|
description: "Minimal core workflow commands - just the prompt, just the artifact"
|
||||||
|
author: "github"
|
||||||
|
repository: "https://github.com/github/spec-kit"
|
||||||
|
license: "MIT"
|
||||||
|
|
||||||
|
requires:
|
||||||
|
speckit_version: ">=0.6.0"
|
||||||
|
|
||||||
|
provides:
|
||||||
|
templates:
|
||||||
|
- type: "command"
|
||||||
|
name: "speckit.specify"
|
||||||
|
file: "commands/speckit.specify.md"
|
||||||
|
description: "Lean specify - create spec.md from a feature description"
|
||||||
|
replaces: "speckit.specify"
|
||||||
|
|
||||||
|
- type: "command"
|
||||||
|
name: "speckit.plan"
|
||||||
|
file: "commands/speckit.plan.md"
|
||||||
|
description: "Lean plan - create plan.md from the spec"
|
||||||
|
replaces: "speckit.plan"
|
||||||
|
|
||||||
|
- type: "command"
|
||||||
|
name: "speckit.tasks"
|
||||||
|
file: "commands/speckit.tasks.md"
|
||||||
|
description: "Lean tasks - create tasks.md from plan and spec"
|
||||||
|
replaces: "speckit.tasks"
|
||||||
|
|
||||||
|
- type: "command"
|
||||||
|
name: "speckit.implement"
|
||||||
|
file: "commands/speckit.implement.md"
|
||||||
|
description: "Lean implement - execute tasks from tasks.md"
|
||||||
|
replaces: "speckit.implement"
|
||||||
|
|
||||||
|
- type: "command"
|
||||||
|
name: "speckit.constitution"
|
||||||
|
file: "commands/speckit.constitution.md"
|
||||||
|
description: "Lean constitution - create or update project constitution"
|
||||||
|
replaces: "speckit.constitution"
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- "lean"
|
||||||
|
- "minimal"
|
||||||
|
- "workflow"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.5.2.dev0"
|
version = "0.6.1"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -41,6 +41,8 @@ packages = ["src/specify_cli"]
|
|||||||
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
||||||
# Bundled extensions (installable via `specify extension add <name>`)
|
# Bundled extensions (installable via `specify extension add <name>`)
|
||||||
"extensions/git" = "specify_cli/core_pack/extensions/git"
|
"extensions/git" = "specify_cli/core_pack/extensions/git"
|
||||||
|
# Bundled presets (installable via `specify preset add <name>` or `specify init --preset <name>`)
|
||||||
|
"presets/lean" = "specify_cli/core_pack/presets/lean"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = [
|
test = [
|
||||||
|
|||||||
@@ -621,6 +621,31 @@ def _locate_bundled_extension(extension_id: str) -> Path | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _locate_bundled_preset(preset_id: str) -> Path | None:
|
||||||
|
"""Return the path to a bundled preset, or None.
|
||||||
|
|
||||||
|
Checks the wheel's core_pack first, then falls back to the
|
||||||
|
source-checkout ``presets/<id>/`` directory.
|
||||||
|
"""
|
||||||
|
import re as _re
|
||||||
|
if not _re.match(r'^[a-z0-9-]+$', preset_id):
|
||||||
|
return None
|
||||||
|
|
||||||
|
core = _locate_core_pack()
|
||||||
|
if core is not None:
|
||||||
|
candidate = core / "presets" / preset_id
|
||||||
|
if (candidate / "preset.yml").is_file():
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
# Source-checkout / editable install: look relative to repo root
|
||||||
|
repo_root = Path(__file__).parent.parent.parent
|
||||||
|
candidate = repo_root / "presets" / preset_id
|
||||||
|
if (candidate / "preset.yml").is_file():
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _install_shared_infra(
|
def _install_shared_infra(
|
||||||
project_path: Path,
|
project_path: Path,
|
||||||
script_type: str,
|
script_type: str,
|
||||||
@@ -1266,27 +1291,44 @@ def init(
|
|||||||
preset_manager = PresetManager(project_path)
|
preset_manager = PresetManager(project_path)
|
||||||
speckit_ver = get_speckit_version()
|
speckit_ver = get_speckit_version()
|
||||||
|
|
||||||
# Try local directory first, then catalog
|
# Try local directory first, then bundled, then catalog
|
||||||
local_path = Path(preset).resolve()
|
local_path = Path(preset).resolve()
|
||||||
if local_path.is_dir() and (local_path / "preset.yml").exists():
|
if local_path.is_dir() and (local_path / "preset.yml").exists():
|
||||||
preset_manager.install_from_directory(local_path, speckit_ver)
|
preset_manager.install_from_directory(local_path, speckit_ver)
|
||||||
else:
|
else:
|
||||||
preset_catalog = PresetCatalog(project_path)
|
bundled_path = _locate_bundled_preset(preset)
|
||||||
pack_info = preset_catalog.get_pack_info(preset)
|
if bundled_path:
|
||||||
if not pack_info:
|
preset_manager.install_from_directory(bundled_path, speckit_ver)
|
||||||
console.print(f"[yellow]Warning:[/yellow] Preset '{preset}' not found in catalog. Skipping.")
|
|
||||||
else:
|
else:
|
||||||
try:
|
preset_catalog = PresetCatalog(project_path)
|
||||||
zip_path = preset_catalog.download_pack(preset)
|
pack_info = preset_catalog.get_pack_info(preset)
|
||||||
preset_manager.install_from_zip(zip_path, speckit_ver)
|
if not pack_info:
|
||||||
# Clean up downloaded ZIP to avoid cache accumulation
|
console.print(f"[yellow]Warning:[/yellow] Preset '{preset}' not found in catalog. Skipping.")
|
||||||
|
elif pack_info.get("bundled") and not pack_info.get("download_url"):
|
||||||
|
from .extensions import REINSTALL_COMMAND
|
||||||
|
console.print(
|
||||||
|
f"[yellow]Warning:[/yellow] Preset '{preset}' is bundled with spec-kit "
|
||||||
|
f"but could not be found in the installed package."
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
"This usually means the spec-kit installation is incomplete or corrupted."
|
||||||
|
)
|
||||||
|
console.print(f"Try reinstalling: {REINSTALL_COMMAND}")
|
||||||
|
else:
|
||||||
|
zip_path = None
|
||||||
try:
|
try:
|
||||||
zip_path.unlink(missing_ok=True)
|
zip_path = preset_catalog.download_pack(preset)
|
||||||
except OSError:
|
preset_manager.install_from_zip(zip_path, speckit_ver)
|
||||||
# Best-effort cleanup; failure to delete is non-fatal
|
except PresetError as preset_err:
|
||||||
pass
|
console.print(f"[yellow]Warning:[/yellow] Failed to install preset '{preset}': {preset_err}")
|
||||||
except PresetError as preset_err:
|
finally:
|
||||||
console.print(f"[yellow]Warning:[/yellow] Failed to install preset '{preset}': {preset_err}")
|
if zip_path is not None:
|
||||||
|
# Clean up downloaded ZIP to avoid cache accumulation
|
||||||
|
try:
|
||||||
|
zip_path.unlink(missing_ok=True)
|
||||||
|
except OSError:
|
||||||
|
# Best-effort cleanup; failure to delete is non-fatal
|
||||||
|
pass
|
||||||
except Exception as preset_err:
|
except Exception as preset_err:
|
||||||
console.print(f"[yellow]Warning:[/yellow] Failed to install preset: {preset_err}")
|
console.print(f"[yellow]Warning:[/yellow] Failed to install preset: {preset_err}")
|
||||||
|
|
||||||
@@ -1338,7 +1380,7 @@ def init(
|
|||||||
step_num = 2
|
step_num = 2
|
||||||
|
|
||||||
# Determine skill display mode for the next-steps panel.
|
# Determine skill display mode for the next-steps panel.
|
||||||
# Skills integrations (codex, kimi, agy, trae) should show skill invocation syntax.
|
# Skills integrations (codex, kimi, agy, trae, cursor-agent) should show skill invocation syntax.
|
||||||
from .integrations.base import SkillsIntegration as _SkillsInt
|
from .integrations.base import SkillsIntegration as _SkillsInt
|
||||||
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)
|
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)
|
||||||
|
|
||||||
@@ -1347,7 +1389,8 @@ def init(
|
|||||||
kimi_skill_mode = selected_ai == "kimi"
|
kimi_skill_mode = selected_ai == "kimi"
|
||||||
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
|
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
|
||||||
trae_skill_mode = selected_ai == "trae"
|
trae_skill_mode = selected_ai == "trae"
|
||||||
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode
|
cursor_agent_skill_mode = selected_ai == "cursor-agent" and (ai_skills or _is_skills_integration)
|
||||||
|
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode
|
||||||
|
|
||||||
if codex_skill_mode and not ai_skills:
|
if codex_skill_mode and not ai_skills:
|
||||||
# Integration path installed skills; show the helpful notice
|
# Integration path installed skills; show the helpful notice
|
||||||
@@ -1356,6 +1399,9 @@ def init(
|
|||||||
if claude_skill_mode and not ai_skills:
|
if claude_skill_mode and not ai_skills:
|
||||||
steps_lines.append(f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]")
|
steps_lines.append(f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]")
|
||||||
step_num += 1
|
step_num += 1
|
||||||
|
if cursor_agent_skill_mode and not ai_skills:
|
||||||
|
steps_lines.append(f"{step_num}. Start Cursor Agent in this project directory; spec-kit skills were installed to [cyan].cursor/skills[/cyan]")
|
||||||
|
step_num += 1
|
||||||
usage_label = "skills" if native_skill_mode else "slash commands"
|
usage_label = "skills" if native_skill_mode else "slash commands"
|
||||||
|
|
||||||
def _display_cmd(name: str) -> str:
|
def _display_cmd(name: str) -> str:
|
||||||
@@ -1365,6 +1411,8 @@ def init(
|
|||||||
return f"/speckit-{name}"
|
return f"/speckit-{name}"
|
||||||
if kimi_skill_mode:
|
if kimi_skill_mode:
|
||||||
return f"/skill:speckit-{name}"
|
return f"/skill:speckit-{name}"
|
||||||
|
if cursor_agent_skill_mode:
|
||||||
|
return f"/speckit-{name}"
|
||||||
return f"/speckit.{name}"
|
return f"/speckit.{name}"
|
||||||
|
|
||||||
steps_lines.append(f"{step_num}. Start using {usage_label} with your AI agent:")
|
steps_lines.append(f"{step_num}. Start using {usage_label} with your AI agent:")
|
||||||
@@ -2134,28 +2182,50 @@ def preset_add(
|
|||||||
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
|
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
|
||||||
|
|
||||||
elif pack_id:
|
elif pack_id:
|
||||||
catalog = PresetCatalog(project_root)
|
# Try bundled preset first, then catalog
|
||||||
pack_info = catalog.get_pack_info(pack_id)
|
bundled_path = _locate_bundled_preset(pack_id)
|
||||||
|
if bundled_path:
|
||||||
if not pack_info:
|
console.print(f"Installing bundled preset [cyan]{pack_id}[/cyan]...")
|
||||||
console.print(f"[red]Error:[/red] Preset '{pack_id}' not found in catalog")
|
manifest = manager.install_from_directory(bundled_path, speckit_version, priority)
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
if not pack_info.get("_install_allowed", True):
|
|
||||||
catalog_name = pack_info.get("_catalog_name", "unknown")
|
|
||||||
console.print(f"[red]Error:[/red] Preset '{pack_id}' is from the '{catalog_name}' catalog which is discovery-only (install not allowed).")
|
|
||||||
console.print("Add the catalog with --install-allowed or install from the preset's repository directly with --from.")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
console.print(f"Installing preset [cyan]{pack_info.get('name', pack_id)}[/cyan]...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
zip_path = catalog.download_pack(pack_id)
|
|
||||||
manifest = manager.install_from_zip(zip_path, speckit_version, priority)
|
|
||||||
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
|
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
|
||||||
finally:
|
else:
|
||||||
if 'zip_path' in locals() and zip_path.exists():
|
catalog = PresetCatalog(project_root)
|
||||||
zip_path.unlink(missing_ok=True)
|
pack_info = catalog.get_pack_info(pack_id)
|
||||||
|
|
||||||
|
if not pack_info:
|
||||||
|
console.print(f"[red]Error:[/red] Preset '{pack_id}' not found in catalog")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# Bundled presets should have been caught above; if we reach
|
||||||
|
# here the bundled files are missing from the installation.
|
||||||
|
if pack_info.get("bundled") and not pack_info.get("download_url"):
|
||||||
|
from .extensions import REINSTALL_COMMAND
|
||||||
|
console.print(
|
||||||
|
f"[red]Error:[/red] Preset '{pack_id}' is bundled with spec-kit "
|
||||||
|
f"but could not be found in the installed package."
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
"\nThis usually means the spec-kit installation is incomplete or corrupted."
|
||||||
|
)
|
||||||
|
console.print("Try reinstalling spec-kit:")
|
||||||
|
console.print(f" {REINSTALL_COMMAND}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
if not pack_info.get("_install_allowed", True):
|
||||||
|
catalog_name = pack_info.get("_catalog_name", "unknown")
|
||||||
|
console.print(f"[red]Error:[/red] Preset '{pack_id}' is from the '{catalog_name}' catalog which is discovery-only (install not allowed).")
|
||||||
|
console.print("Add the catalog with --install-allowed or install from the preset's repository directly with --from.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
console.print(f"Installing preset [cyan]{pack_info.get('name', pack_id)}[/cyan]...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
zip_path = catalog.download_pack(pack_id)
|
||||||
|
manifest = manager.install_from_zip(zip_path, speckit_version, priority)
|
||||||
|
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
|
||||||
|
finally:
|
||||||
|
if 'zip_path' in locals() and zip_path.exists():
|
||||||
|
zip_path.unlink(missing_ok=True)
|
||||||
else:
|
else:
|
||||||
console.print("[red]Error:[/red] Specify a preset ID, --from URL, or --dev path")
|
console.print("[red]Error:[/red] Specify a preset ID, --from URL, or --dev path")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
@@ -3001,7 +3071,7 @@ def extension_add(
|
|||||||
priority: int = typer.Option(10, "--priority", help="Resolution priority (lower = higher precedence, default 10)"),
|
priority: int = typer.Option(10, "--priority", help="Resolution priority (lower = higher precedence, default 10)"),
|
||||||
):
|
):
|
||||||
"""Install an extension."""
|
"""Install an extension."""
|
||||||
from .extensions import ExtensionManager, ExtensionCatalog, ExtensionError, ValidationError, CompatibilityError
|
from .extensions import ExtensionManager, ExtensionCatalog, ExtensionError, ValidationError, CompatibilityError, REINSTALL_COMMAND
|
||||||
|
|
||||||
project_root = Path.cwd()
|
project_root = Path.cwd()
|
||||||
|
|
||||||
@@ -3103,6 +3173,19 @@ def extension_add(
|
|||||||
manifest = manager.install_from_directory(bundled_path, speckit_version, priority=priority)
|
manifest = manager.install_from_directory(bundled_path, speckit_version, priority=priority)
|
||||||
|
|
||||||
if bundled_path is None:
|
if bundled_path is None:
|
||||||
|
# Bundled extensions without a download URL must come from the local package
|
||||||
|
if ext_info.get("bundled") and not ext_info.get("download_url"):
|
||||||
|
console.print(
|
||||||
|
f"[red]Error:[/red] Extension '{ext_info['id']}' is bundled with spec-kit "
|
||||||
|
f"but could not be found in the installed package."
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
"\nThis usually means the spec-kit installation is incomplete or corrupted."
|
||||||
|
)
|
||||||
|
console.print("Try reinstalling spec-kit:")
|
||||||
|
console.print(f" {REINSTALL_COMMAND}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Enforce install_allowed policy
|
# Enforce install_allowed policy
|
||||||
if not ext_info.get("_install_allowed", True):
|
if not ext_info.get("_install_allowed", True):
|
||||||
catalog_name = ext_info.get("_catalog_name", "community")
|
catalog_name = ext_info.get("_catalog_name", "community")
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ _FALLBACK_CORE_COMMAND_NAMES = frozenset({
|
|||||||
})
|
})
|
||||||
EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$")
|
EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$")
|
||||||
|
|
||||||
|
REINSTALL_COMMAND = "uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git"
|
||||||
|
|
||||||
|
|
||||||
def _load_core_command_names() -> frozenset[str]:
|
def _load_core_command_names() -> frozenset[str]:
|
||||||
"""Discover bundled core command names from the packaged templates.
|
"""Discover bundled core command names from the packaged templates.
|
||||||
@@ -1870,6 +1872,14 @@ class ExtensionCatalog:
|
|||||||
if not ext_info:
|
if not ext_info:
|
||||||
raise ExtensionError(f"Extension '{extension_id}' not found in catalog")
|
raise ExtensionError(f"Extension '{extension_id}' not found in catalog")
|
||||||
|
|
||||||
|
# Bundled extensions without a download URL must be installed locally
|
||||||
|
if ext_info.get("bundled") and not ext_info.get("download_url"):
|
||||||
|
raise ExtensionError(
|
||||||
|
f"Extension '{extension_id}' is bundled with spec-kit and has no download URL. "
|
||||||
|
f"It should be installed from the local package. "
|
||||||
|
f"Try reinstalling: {REINSTALL_COMMAND}"
|
||||||
|
)
|
||||||
|
|
||||||
download_url = ext_info.get("download_url")
|
download_url = ext_info.get("download_url")
|
||||||
if not download_url:
|
if not download_url:
|
||||||
raise ExtensionError(f"Extension '{extension_id}' has no download URL")
|
raise ExtensionError(f"Extension '{extension_id}' has no download URL")
|
||||||
@@ -2170,6 +2180,7 @@ class HookExecutor:
|
|||||||
codex_skill_mode = selected_ai == "codex" and bool(init_options.get("ai_skills"))
|
codex_skill_mode = selected_ai == "codex" and bool(init_options.get("ai_skills"))
|
||||||
claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills"))
|
claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills"))
|
||||||
kimi_skill_mode = selected_ai == "kimi"
|
kimi_skill_mode = selected_ai == "kimi"
|
||||||
|
cursor_skill_mode = selected_ai == "cursor-agent" and bool(init_options.get("ai_skills"))
|
||||||
|
|
||||||
skill_name = self._skill_name_from_command(command_id)
|
skill_name = self._skill_name_from_command(command_id)
|
||||||
if codex_skill_mode and skill_name:
|
if codex_skill_mode and skill_name:
|
||||||
@@ -2178,6 +2189,8 @@ class HookExecutor:
|
|||||||
return f"/{skill_name}"
|
return f"/{skill_name}"
|
||||||
if kimi_skill_mode and skill_name:
|
if kimi_skill_mode and skill_name:
|
||||||
return f"/skill:{skill_name}"
|
return f"/skill:{skill_name}"
|
||||||
|
if cursor_skill_mode and skill_name:
|
||||||
|
return f"/{skill_name}"
|
||||||
|
|
||||||
return f"/{command_id}"
|
return f"/{command_id}"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,39 @@
|
|||||||
"""Cursor IDE integration."""
|
"""Cursor IDE integration.
|
||||||
|
|
||||||
from ..base import MarkdownIntegration
|
Cursor Agent uses the ``.cursor/skills/speckit-<name>/SKILL.md`` layout.
|
||||||
|
Commands are deprecated; ``--skills`` defaults to ``True``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..base import IntegrationOption, SkillsIntegration
|
||||||
|
|
||||||
|
|
||||||
class CursorAgentIntegration(MarkdownIntegration):
|
class CursorAgentIntegration(SkillsIntegration):
|
||||||
key = "cursor-agent"
|
key = "cursor-agent"
|
||||||
config = {
|
config = {
|
||||||
"name": "Cursor",
|
"name": "Cursor",
|
||||||
"folder": ".cursor/",
|
"folder": ".cursor/",
|
||||||
"commands_subdir": "commands",
|
"commands_subdir": "skills",
|
||||||
"install_url": None,
|
"install_url": None,
|
||||||
"requires_cli": False,
|
"requires_cli": False,
|
||||||
}
|
}
|
||||||
registrar_config = {
|
registrar_config = {
|
||||||
"dir": ".cursor/commands",
|
"dir": ".cursor/skills",
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"args": "$ARGUMENTS",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md",
|
"extension": "/SKILL.md",
|
||||||
}
|
}
|
||||||
|
|
||||||
context_file = ".cursor/rules/specify-rules.mdc"
|
context_file = ".cursor/rules/specify-rules.mdc"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def options(cls) -> list[IntegrationOption]:
|
||||||
|
return [
|
||||||
|
IntegrationOption(
|
||||||
|
"--skills",
|
||||||
|
is_flag=True,
|
||||||
|
default=True,
|
||||||
|
help="Install as agent skills (recommended for Cursor)",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -1587,6 +1587,16 @@ class PresetCatalog:
|
|||||||
f"Preset '{pack_id}' not found in catalog"
|
f"Preset '{pack_id}' not found in catalog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Bundled presets without a download URL must be installed locally
|
||||||
|
if pack_info.get("bundled") and not pack_info.get("download_url"):
|
||||||
|
from .extensions import REINSTALL_COMMAND
|
||||||
|
raise PresetError(
|
||||||
|
f"Preset '{pack_id}' is bundled with spec-kit and has no download URL. "
|
||||||
|
f"It should be installed from the local package. "
|
||||||
|
f"Use 'specify preset add {pack_id}' to install from the bundled package, "
|
||||||
|
f"or reinstall spec-kit if the bundled files are missing: {REINSTALL_COMMAND}"
|
||||||
|
)
|
||||||
|
|
||||||
if not pack_info.get("_install_allowed", True):
|
if not pack_info.get("_install_allowed", True):
|
||||||
catalog_name = pack_info.get("_catalog_name", "unknown")
|
catalog_name = pack_info.get("_catalog_name", "unknown")
|
||||||
raise PresetError(
|
raise PresetError(
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
"""Tests for CursorAgentIntegration."""
|
"""Tests for CursorAgentIntegration."""
|
||||||
|
|
||||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
from .test_integration_base_skills import SkillsIntegrationTests
|
||||||
|
|
||||||
|
|
||||||
class TestCursorAgentIntegration(MarkdownIntegrationTests):
|
class TestCursorAgentIntegration(SkillsIntegrationTests):
|
||||||
KEY = "cursor-agent"
|
KEY = "cursor-agent"
|
||||||
FOLDER = ".cursor/"
|
FOLDER = ".cursor/"
|
||||||
COMMANDS_SUBDIR = "commands"
|
COMMANDS_SUBDIR = "skills"
|
||||||
REGISTRAR_DIR = ".cursor/commands"
|
REGISTRAR_DIR = ".cursor/skills"
|
||||||
CONTEXT_FILE = ".cursor/rules/specify-rules.mdc"
|
CONTEXT_FILE = ".cursor/rules/specify-rules.mdc"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCursorAgentAutoPromote:
|
||||||
|
"""--ai cursor-agent auto-promotes to integration path."""
|
||||||
|
|
||||||
|
def test_ai_cursor_agent_without_ai_skills_auto_promotes(self, tmp_path):
|
||||||
|
"""--ai cursor-agent should work the same as --integration cursor-agent."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
from specify_cli import app
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
target = tmp_path / "test-proj"
|
||||||
|
result = runner.invoke(app, ["init", str(target), "--ai", "cursor-agent", "--no-git", "--ignore-agent-tools", "--script", "sh"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0, f"init --ai cursor-agent failed: {result.output}"
|
||||||
|
assert (target / ".cursor" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||||
|
|
||||||
|
|||||||
@@ -2995,6 +2995,122 @@ class TestExtensionAddCLI:
|
|||||||
f"but was called with '{download_called_with[0]}'"
|
f"but was called with '{download_called_with[0]}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_add_bundled_extension_not_found_gives_clear_error(self, tmp_path):
|
||||||
|
"""extension add should give a clear error when a bundled extension is not found locally."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from specify_cli import app
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
# Create project structure
|
||||||
|
project_dir = tmp_path / "test-project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
(project_dir / ".specify" / "extensions").mkdir(parents=True)
|
||||||
|
|
||||||
|
# Mock catalog that returns a bundled extension without download_url
|
||||||
|
mock_catalog = MagicMock()
|
||||||
|
mock_catalog.get_extension_info.return_value = {
|
||||||
|
"id": "git",
|
||||||
|
"name": "Git Branching Workflow",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Git branching extension",
|
||||||
|
"bundled": True,
|
||||||
|
"_install_allowed": True,
|
||||||
|
}
|
||||||
|
mock_catalog.search.return_value = []
|
||||||
|
|
||||||
|
with patch("specify_cli.extensions.ExtensionCatalog", return_value=mock_catalog), \
|
||||||
|
patch("specify_cli._locate_bundled_extension", return_value=None), \
|
||||||
|
patch.object(Path, "cwd", return_value=project_dir):
|
||||||
|
result = runner.invoke(
|
||||||
|
app,
|
||||||
|
["extension", "add", "git"],
|
||||||
|
catch_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "bundled with spec-kit" in result.output
|
||||||
|
assert "reinstall" in result.output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDownloadExtensionBundled:
|
||||||
|
"""Tests for download_extension handling of bundled extensions."""
|
||||||
|
|
||||||
|
def test_download_extension_raises_for_bundled(self, temp_dir):
|
||||||
|
"""download_extension should raise a clear error for bundled extensions without a URL."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
bundled_ext_info = {
|
||||||
|
"name": "Git Branching Workflow",
|
||||||
|
"id": "git",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Git workflow",
|
||||||
|
"bundled": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(catalog, "get_extension_info", return_value=bundled_ext_info):
|
||||||
|
with pytest.raises(ExtensionError, match="bundled with spec-kit"):
|
||||||
|
catalog.download_extension("git")
|
||||||
|
|
||||||
|
def test_download_extension_allows_bundled_with_url(self, temp_dir):
|
||||||
|
"""download_extension should allow bundled extensions that have a download_url (newer version)."""
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
bundled_with_url = {
|
||||||
|
"name": "Git Branching Workflow",
|
||||||
|
"id": "git",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Git workflow",
|
||||||
|
"bundled": True,
|
||||||
|
"download_url": "https://example.com/git-2.0.0.zip",
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.read.return_value = b"fake zip data"
|
||||||
|
mock_response.__enter__ = lambda s: s
|
||||||
|
mock_response.__exit__ = MagicMock(return_value=False)
|
||||||
|
|
||||||
|
with patch.object(catalog, "get_extension_info", return_value=bundled_with_url), \
|
||||||
|
patch.object(urllib.request, "urlopen", return_value=mock_response):
|
||||||
|
result = catalog.download_extension("git")
|
||||||
|
assert result.name == "git-2.0.0.zip"
|
||||||
|
|
||||||
|
def test_download_extension_raises_no_url_for_non_bundled(self, temp_dir):
|
||||||
|
"""download_extension should raise 'no download URL' for non-bundled extensions without URL."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
non_bundled_ext_info = {
|
||||||
|
"name": "Some Extension",
|
||||||
|
"id": "some-ext",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Test",
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(catalog, "get_extension_info", return_value=non_bundled_ext_info):
|
||||||
|
with pytest.raises(ExtensionError, match="has no download URL"):
|
||||||
|
catalog.download_extension("some-ext")
|
||||||
|
|
||||||
|
|
||||||
class TestExtensionUpdateCLI:
|
class TestExtensionUpdateCLI:
|
||||||
"""CLI integration tests for extension update command."""
|
"""CLI integration tests for extension update command."""
|
||||||
|
|||||||
@@ -2865,3 +2865,182 @@ class TestPresetEnableDisable:
|
|||||||
|
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
assert "corrupted state" in result.output.lower()
|
assert "corrupted state" in result.output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Lean Preset Tests =====
|
||||||
|
|
||||||
|
|
||||||
|
LEAN_PRESET_DIR = Path(__file__).parent.parent / "presets" / "lean"
|
||||||
|
|
||||||
|
LEAN_COMMAND_NAMES = [
|
||||||
|
"speckit.specify",
|
||||||
|
"speckit.plan",
|
||||||
|
"speckit.tasks",
|
||||||
|
"speckit.implement",
|
||||||
|
"speckit.constitution",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestLeanPreset:
|
||||||
|
"""Tests for the lean preset that ships with the repo."""
|
||||||
|
|
||||||
|
def test_lean_preset_exists(self):
|
||||||
|
"""Verify the lean preset directory and manifest exist."""
|
||||||
|
assert LEAN_PRESET_DIR.exists()
|
||||||
|
assert (LEAN_PRESET_DIR / "preset.yml").exists()
|
||||||
|
|
||||||
|
def test_lean_manifest_valid(self):
|
||||||
|
"""Verify the lean preset manifest is valid."""
|
||||||
|
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
|
||||||
|
assert manifest.id == "lean"
|
||||||
|
assert manifest.name == "Lean Workflow"
|
||||||
|
assert manifest.version == "1.0.0"
|
||||||
|
assert len(manifest.templates) == 5 # 5 commands
|
||||||
|
|
||||||
|
def test_lean_provides_core_workflow_commands(self):
|
||||||
|
"""Verify the lean preset provides overrides for core workflow commands."""
|
||||||
|
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
|
||||||
|
provided_names = {t["name"] for t in manifest.templates}
|
||||||
|
for name in LEAN_COMMAND_NAMES:
|
||||||
|
assert name in provided_names, f"Lean preset missing command: {name}"
|
||||||
|
|
||||||
|
def test_lean_command_files_exist(self):
|
||||||
|
"""Verify that all declared command files actually exist on disk."""
|
||||||
|
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
|
||||||
|
for tmpl in manifest.templates:
|
||||||
|
tmpl_path = LEAN_PRESET_DIR / tmpl["file"]
|
||||||
|
assert tmpl_path.exists(), f"Missing command file: {tmpl['file']}"
|
||||||
|
|
||||||
|
def test_lean_commands_have_no_scripts(self):
|
||||||
|
"""Verify lean commands have no scripts or agent_scripts in frontmatter."""
|
||||||
|
from specify_cli.agents import CommandRegistrar
|
||||||
|
|
||||||
|
for name in LEAN_COMMAND_NAMES:
|
||||||
|
cmd_path = LEAN_PRESET_DIR / "commands" / f"speckit.{name.split('.')[-1]}.md"
|
||||||
|
content = cmd_path.read_text()
|
||||||
|
frontmatter, _ = CommandRegistrar.parse_frontmatter(content)
|
||||||
|
assert "scripts" not in frontmatter, f"{name} should not have scripts in frontmatter"
|
||||||
|
assert "agent_scripts" not in frontmatter, f"{name} should not have agent_scripts in frontmatter"
|
||||||
|
|
||||||
|
def test_lean_commands_have_no_hooks(self):
|
||||||
|
"""Verify lean commands do not contain extension hook boilerplate."""
|
||||||
|
for name in LEAN_COMMAND_NAMES:
|
||||||
|
cmd_path = LEAN_PRESET_DIR / "commands" / f"speckit.{name.split('.')[-1]}.md"
|
||||||
|
content = cmd_path.read_text()
|
||||||
|
assert "hooks." not in content, f"{name} should not reference extension hooks"
|
||||||
|
assert "extensions.yml" not in content, f"{name} should not reference extensions.yml"
|
||||||
|
|
||||||
|
def test_install_lean_preset(self, project_dir):
|
||||||
|
"""Test installing the lean preset from its directory."""
|
||||||
|
manager = PresetManager(project_dir)
|
||||||
|
manifest = manager.install_from_directory(LEAN_PRESET_DIR, "0.6.0")
|
||||||
|
assert manifest.id == "lean"
|
||||||
|
assert manager.registry.is_installed("lean")
|
||||||
|
|
||||||
|
def test_lean_overrides_commands(self, project_dir):
|
||||||
|
"""Test that lean preset overrides are resolved correctly."""
|
||||||
|
manager = PresetManager(project_dir)
|
||||||
|
manager.install_from_directory(LEAN_PRESET_DIR, "0.6.0")
|
||||||
|
|
||||||
|
resolver = PresetResolver(project_dir)
|
||||||
|
for name in LEAN_COMMAND_NAMES:
|
||||||
|
result = resolver.resolve(name, template_type="command")
|
||||||
|
assert result is not None, f"Lean override for {name} not resolved"
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Bundled Preset Locator Tests =====
|
||||||
|
|
||||||
|
|
||||||
|
class TestBundledPresetLocator:
|
||||||
|
"""Tests for _locate_bundled_preset discovery function."""
|
||||||
|
|
||||||
|
def test_locate_bundled_lean_preset(self):
|
||||||
|
"""_locate_bundled_preset finds the lean preset."""
|
||||||
|
from specify_cli import _locate_bundled_preset
|
||||||
|
|
||||||
|
path = _locate_bundled_preset("lean")
|
||||||
|
assert path is not None
|
||||||
|
assert (path / "preset.yml").is_file()
|
||||||
|
|
||||||
|
def test_locate_bundled_preset_not_found(self):
|
||||||
|
"""_locate_bundled_preset returns None for nonexistent preset."""
|
||||||
|
from specify_cli import _locate_bundled_preset
|
||||||
|
|
||||||
|
path = _locate_bundled_preset("nonexistent-preset")
|
||||||
|
assert path is None
|
||||||
|
|
||||||
|
def test_locate_bundled_preset_rejects_invalid_id(self):
|
||||||
|
"""_locate_bundled_preset rejects IDs with invalid characters."""
|
||||||
|
from specify_cli import _locate_bundled_preset
|
||||||
|
|
||||||
|
assert _locate_bundled_preset("../escape") is None
|
||||||
|
assert _locate_bundled_preset("UPPERCASE") is None
|
||||||
|
assert _locate_bundled_preset("has spaces") is None
|
||||||
|
|
||||||
|
def test_bundled_preset_add_via_cli(self, project_dir):
|
||||||
|
"""Test that 'specify preset add lean' installs the bundled preset."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
from unittest.mock import patch
|
||||||
|
from specify_cli import app
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
with patch.object(Path, "cwd", return_value=project_dir), \
|
||||||
|
patch("specify_cli.get_speckit_version", return_value="0.6.0"):
|
||||||
|
result = runner.invoke(app, ["preset", "add", "lean"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert "Lean Workflow" in result.output
|
||||||
|
assert "installed" in result.output.lower()
|
||||||
|
|
||||||
|
def test_bundled_preset_in_catalog(self):
|
||||||
|
"""Verify the lean preset is listed in catalog.json with bundled marker."""
|
||||||
|
catalog_path = Path(__file__).parent.parent / "presets" / "catalog.json"
|
||||||
|
catalog = json.loads(catalog_path.read_text())
|
||||||
|
assert "lean" in catalog["presets"]
|
||||||
|
assert catalog["presets"]["lean"]["bundled"] is True
|
||||||
|
assert "download_url" not in catalog["presets"]["lean"]
|
||||||
|
|
||||||
|
def test_bundled_preset_download_raises_error(self, project_dir):
|
||||||
|
"""download_pack raises PresetError for bundled presets without download_url."""
|
||||||
|
catalog = PresetCatalog(project_dir)
|
||||||
|
|
||||||
|
catalog_data = {
|
||||||
|
"test-bundled": {
|
||||||
|
"name": "Test Bundled",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"bundled": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
from unittest.mock import patch
|
||||||
|
with patch.object(catalog, "_get_merged_packs", return_value=catalog_data):
|
||||||
|
with pytest.raises(PresetError, match="bundled with spec-kit"):
|
||||||
|
catalog.download_pack("test-bundled")
|
||||||
|
|
||||||
|
def test_bundled_preset_missing_locally_cli_error(self, project_dir):
|
||||||
|
"""CLI shows clear error when bundled preset cannot be found locally."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
from unittest.mock import patch
|
||||||
|
from specify_cli import app
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
# Patch _locate_bundled_preset to return None (simulating missing files)
|
||||||
|
# and mock the catalog to return a bundled entry for "lean"
|
||||||
|
fake_pack_info = {
|
||||||
|
"id": "lean",
|
||||||
|
"name": "Lean Workflow",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"bundled": True,
|
||||||
|
"_install_allowed": True,
|
||||||
|
}
|
||||||
|
with patch.object(Path, "cwd", return_value=project_dir), \
|
||||||
|
patch("specify_cli._locate_bundled_preset", return_value=None), \
|
||||||
|
patch("specify_cli.presets.PresetCatalog") as MockCatalog:
|
||||||
|
MockCatalog.return_value.get_pack_info.return_value = fake_pack_info
|
||||||
|
result = runner.invoke(app, ["preset", "add", "lean"])
|
||||||
|
|
||||||
|
# Should fail with a helpful error explaining this is a bundled preset
|
||||||
|
# and suggesting how to recover.
|
||||||
|
assert result.exit_code == 1
|
||||||
|
output = strip_ansi(result.output).lower()
|
||||||
|
assert "bundled" in output, result.output
|
||||||
|
assert "reinstall" in output, result.output
|
||||||
|
|||||||
Reference in New Issue
Block a user