Compare commits

..

12 Commits

Author SHA1 Message Date
github-actions[bot]
2499c9a1a6 chore: bump version to 0.6.1 2026-04-10 21:19:25 +00:00
Manfred Riem
43cb0fa7ab feat: add bundled lean preset with minimal workflow commands (#2161)
* feat: add bundled lean preset with minimal workflow commands

Add a lean preset that overrides the 5 core workflow commands (specify,
plan, tasks, implement, constitution) with minimal prompts that produce
exactly one artifact each — no extension hooks, no scripts, no git
branching, no templates.

Bundled preset infrastructure:
- Add _locate_bundled_preset() mirroring _locate_bundled_extension()
- Update 'specify init --preset' to try bundled -> catalog fallback
- Update 'specify preset add' to try bundled -> catalog fallback
- Add bundled guard in download_pack() for presets without download URLs
- Add lean to presets/catalog.json with 'bundled: true' marker
- Add lean to pyproject.toml force-include for wheel packaging
- Align error messages with bundled extension error pattern

Tests: 15 new tests (TestLeanPreset + TestBundledPresetLocator)

* refactor: address review — clean up unused imports, strengthen test assertions

- Remove unused MagicMock import and cache_dir setup in download test
- Assert 'bundled' and 'reinstall' in CLI error output (not just exit code)
- Mock catalog in missing-locally test for deterministic bundled error path
- Fix test versions to satisfy updated speckit_version >=0.6.0 requirement

* refactor: address review — fix constitution paths, add REINSTALL_COMMAND to presets.py

- Fix constitution path to .specify/memory/constitution.md in plan, tasks,
  implement commands (matching core command convention)
- Include REINSTALL_COMMAND in download_pack() bundled guard for consistent
  recovery instructions across bundled extensions and presets

* refactor: address review — explicit feature_directory paths, ZIP cleanup in finally

- Prefix spec.md/plan.md/tasks.md with <feature_directory>/ in plan, tasks,
  and implement commands so the agent doesn't operate on repo root by mistake
- Move ZIP unlink into finally block in init --preset path so cleanup runs
  even when install_from_zip raises (matching preset_add pattern)

* refactor: address review — replace Unicode em dashes with ASCII, fix grammar

- Replace all Unicode em dashes with ASCII hyphens in preset.yml and
  catalog.json to avoid decode errors on non-UTF-8 environments
- Fix grammar: 'store it in tasks.md' -> 'store them in tasks.md'

* refactor: address review - align task format between tasks and implement

- Remove undefined [P] marker from implement (lean uses sequential execution)
- Clarify checkbox update: 'change - [ ] to - [x]' instead of ambiguous '[X]'
- Simplify implement to execute tasks in order without parallel complexity

* refactor: address review - parse frontmatter instead of raw substring search

- Use CommandRegistrar.parse_frontmatter() to check for scripts/agent_scripts
  keys in YAML frontmatter instead of brittle 'scripts:' substring search
2026-04-10 16:18:06 -05:00
Quratulain-bilal
74e3f45aa9 Add Brownfield Bootstrap extension to community catalog (#2145)
- 4 commands: scan, bootstrap, validate, migrate for existing codebases
- 1 hook: after_init for auto-scanning project after spec-kit initialization
- Addresses community request in issue #1436 (30+ reactions)
2026-04-10 13:16:26 -05:00
Quratulain-bilal
97ea7cf6a0 Add CI Guard extension to community catalog (#2157)
* Add CI Guard extension to community catalog

Adds spec-kit-ci-guard: spec compliance gates for CI/CD pipelines.

5 commands:
- /speckit.ci.check — run all compliance checks with pass/fail
- /speckit.ci.report — generate requirement traceability matrix
- /speckit.ci.gate — configure merge gate rules and thresholds
- /speckit.ci.drift — detect bidirectional spec-to-code drift
- /speckit.ci.badge — generate spec compliance badges

2 hooks: before_implement, after_implement

Bridges the gap between SDD workflow and CI/CD enforcement.

* Fix updated_at to monotonically increase per Copilot review

Set to 2026-04-10T15:00:00Z (later than previous 2026-04-10T12:34:56Z).
2026-04-10 12:20:55 -05:00
Quratulain-bilal
f43b85096c Add SpecTest extension to community catalog (#2159)
* Add SpecTest extension to community catalog

Adds spec-kit-spectest: auto-generate test scaffolds from spec criteria.

4 commands:
- /speckit.test.generate — generate framework-native test scaffolds
- /speckit.test.coverage — map spec requirements to test coverage
- /speckit.test.gaps — find untested requirements with suggestions
- /speckit.test.plan — generate structured test plan documents

1 hook: after_implement (gap detection)

Bridges the spec-to-test gap in the SDD workflow.

* Update extensions/catalog.community.json

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

* Fix spectest created_at/updated_at to use current timestamp per Copilot review

Set both to 2026-04-10T16:00:00Z instead of midnight.

---------

Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 12:11:25 -05:00
Manfred Riem
d1b95c2f59 fix: bundled extensions should not have download URLs (#2155)
* fix: bundled extensions should not have download URLs (#2151)

- Remove selftest from default catalog (not a published extension)
- Replace download_url with 'bundled: true' flag for git extension
- Add bundled check in extension add flow with clear error message
  when bundled extension is missing from installed package
- Add bundled check in download_extension() with specific error
- Direct users to reinstall via uv with full GitHub URL
- Add 3 regression tests for bundled extension handling

* refactor: address review - move bundled check up-front, extract reinstall constant

- Move bundled check before download_url inspection in download_extension()
  so bundled extensions can never be downloaded even with a URL present
- Extract REINSTALL_COMMAND constant to avoid duplicated install strings

* fix: allow bundled extensions with download_url to be updated

Bundled extensions should only be blocked from download when they have
no download_url. If a newer version is published to the catalog with a
URL, users should be able to install it to get bug fixes.

Add test for bundled-with-URL download path.
2026-04-10 11:29:18 -05:00
Quratulain-bilal
8bb08ae1a0 Add PR Bridge extension to community catalog (#2148)
- 3 commands: generate PR descriptions, reviewer checklists, and change summaries
- 1 hook: after_implement for auto-generating PR description
- Closes the SDD workflow loop: specify → plan → tasks → implement → PR
2026-04-10 09:51:20 -05:00
Gabriel Henrique
5732de60d0 feat(cursor-agent): migrate from .cursor/commands to .cursor/skills (#2156)
Use SkillsIntegration so workflows ship as speckit-*/SKILL.md. Update
init next-steps, extension hook invocation, docs, and tests.

Made-with: Cursor
2026-04-10 09:35:19 -05:00
Quratulain-bilal
b6e19b49ec Add TinySpec extension to community catalog (#2147)
* Add TinySpec extension to community catalog

- 3 commands: tinyspec, implement, classify for lightweight single-file workflow
- 1 hook: before_specify for auto-classifying task complexity
- Addresses community request in issue #1174 (22+ reactions)

* Potential fix for pull request finding

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

---------

Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-10 09:10:42 -05:00
Ismael
7f1e38491f chore: bump spec-kit-verify to 1.0.3 and spec-kit-review to 1.0.1 (#2146)
* chore: bump spec-kit-verify to 1.0.3 and spec-kit-review to 1.0.1

*  Removed invalid aliases

* Change updated at
2026-04-10 07:32:48 -05:00
Wes Etheredge
bc0288832e Add Status Report extension to community catalog (#2123)
- Extension ID: status-report
- Version: 1.2.5
- Author: Open-Agent-Tools
- Description: Project status, feature progress, and next-action recommendations for spec-driven workflows

Co-authored-by: Unserious AI <121459476+unseriousAI@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:27:15 -05:00
Manfred Riem
e70495c2b8 chore: release 0.6.0, begin 0.6.1.dev0 development (#2144)
* chore: bump version to 0.6.0

* chore: begin 0.6.1.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-09 14:17:11 -05:00
20 changed files with 878 additions and 76 deletions

View File

@@ -2,6 +2,22 @@
<!-- 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

View File

@@ -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) |
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
| Branch Convention | Configurable branch and folder naming conventions for /specify with presets and custom patterns | `process` | Read+Write | [spec-kit-branch-convention](https://github.com/Quratulain-bilal/spec-kit-branch-convention) |
| Brownfield Bootstrap | Bootstrap spec-kit for existing codebases — auto-discover architecture and adopt SDD incrementally | `process` | Read+Write | [spec-kit-brownfield](https://github.com/Quratulain-bilal/spec-kit-brownfield) |
| Bugfix Workflow | Structured bugfix workflow — capture bugs, trace to spec artifacts, and patch specs surgically | `process` | Read+Write | [spec-kit-bugfix](https://github.com/Quratulain-bilal/spec-kit-bugfix) |
| Canon | Adds canon-driven (baseline-driven) workflows: spec-first, code-first, spec-drift. Requires Canon Core preset installation. | `process` | Read+Write | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon/tree/master/extension) |
| CI Guard | Spec compliance gates for CI/CD — verify specs exist, check drift, and block merges on gaps | `process` | Read-only | [spec-kit-ci-guard](https://github.com/Quratulain-bilal/spec-kit-ci-guard) |
| Checkpoint Extension | Commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end | `code` | Read+Write | [spec-kit-checkpoint](https://github.com/aaronrsun/spec-kit-checkpoint) |
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | `code` | Read+Write | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
| Conduct Extension | Orchestrates spec-kit phases via sub-agent delegation to reduce context pollution. | `process` | Read+Write | [spec-kit-conduct-ext](https://github.com/twbrandon7/spec-kit-conduct-ext) |
@@ -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) |
| Optimize | Audit and optimize AI governance for context efficiency — token budgets, rule health, interpretability, compression, coherence, and echo detection | `process` | Read+Write | [spec-kit-optimize](https://github.com/sakitA/spec-kit-optimize) |
| Plan Review Gate | Require spec.md and plan.md to be merged via MR/PR before allowing task generation | `process` | Read-only | [spec-kit-plan-review-gate](https://github.com/luno/spec-kit-plan-review-gate) |
| PR Bridge | Auto-generate pull request descriptions, checklists, and summaries from spec artifacts | `process` | Read-only | [spec-kit-pr-bridge-](https://github.com/Quratulain-bilal/spec-kit-pr-bridge-) |
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
| Product Forge | Full product lifecycle: research → product spec → SpecKit → implement → verify → test | `process` | Read+Write | [speckit-product-forge](https://github.com/VaiYav/speckit-product-forge) |
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
@@ -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 Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
| SpecTest | Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements | `code` | Read+Write | [spec-kit-spectest](https://github.com/Quratulain-bilal/spec-kit-spectest) |
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
| Status Report | Project status, feature progress, and next-action recommendations for spec-driven workflows | `visibility` | Read-only | [Open-Agent-Tools/spec-kit-status](https://github.com/Open-Agent-Tools/spec-kit-status) |
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
| TinySpec | Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process | `process` | Read+Write | [spec-kit-tinyspec](https://github.com/Quratulain-bilal/spec-kit-tinyspec) |
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |

View File

@@ -292,7 +292,7 @@ This tells Spec Kit which feature directory to use when creating specs, plans, a
```bash
ls -la .claude/commands/ # Claude Code
ls -la .gemini/commands/ # Gemini
ls -la .cursor/commands/ # Cursor
ls -la .cursor/skills/ # Cursor
ls -la .pi/prompts/ # Pi Coding Agent
```

View File

@@ -1,6 +1,6 @@
{
"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",
"extensions": {
"aide": {
@@ -138,6 +138,38 @@
"created_at": "2026-04-08T00:00:00Z",
"updated_at": "2026-04-08T00:00:00Z"
},
"brownfield": {
"name": "Brownfield Bootstrap",
"id": "brownfield",
"description": "Bootstrap spec-kit for existing codebases — auto-discover architecture and adopt SDD incrementally.",
"author": "Quratulain-bilal",
"version": "1.0.0",
"download_url": "https://github.com/Quratulain-bilal/spec-kit-brownfield/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/Quratulain-bilal/spec-kit-brownfield",
"homepage": "https://github.com/Quratulain-bilal/spec-kit-brownfield",
"documentation": "https://github.com/Quratulain-bilal/spec-kit-brownfield/blob/main/README.md",
"changelog": "https://github.com/Quratulain-bilal/spec-kit-brownfield/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.4.0"
},
"provides": {
"commands": 4,
"hooks": 1
},
"tags": [
"brownfield",
"bootstrap",
"existing-project",
"migration",
"onboarding"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-10T00:00:00Z",
"updated_at": "2026-04-10T00:00:00Z"
},
"bugfix": {
"name": "Bugfix Workflow",
"id": "bugfix",
@@ -205,6 +237,39 @@
"created_at": "2026-03-29T00:00:00Z",
"updated_at": "2026-03-29T00:00:00Z"
},
"ci-guard": {
"name": "CI Guard",
"id": "ci-guard",
"description": "Spec compliance gates for CI/CD — verify specs exist, check drift, and block merges on gaps.",
"author": "Quratulain-bilal",
"version": "1.0.0",
"download_url": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/Quratulain-bilal/spec-kit-ci-guard",
"homepage": "https://github.com/Quratulain-bilal/spec-kit-ci-guard",
"documentation": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/blob/main/README.md",
"changelog": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.4.0"
},
"provides": {
"commands": 5,
"hooks": 2
},
"tags": [
"ci-cd",
"compliance",
"governance",
"quality-gate",
"drift-detection",
"automation"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-10T17:00:00Z",
"updated_at": "2026-04-10T17:00:00Z"
},
"checkpoint": {
"name": "Checkpoint Extension",
"id": "checkpoint",
@@ -1030,6 +1095,38 @@
"created_at": "2026-03-27T08:22:30Z",
"updated_at": "2026-03-27T08:22:30Z"
},
"pr-bridge": {
"name": "PR Bridge",
"id": "pr-bridge",
"description": "Auto-generate pull request descriptions, checklists, and summaries from spec artifacts.",
"author": "Quratulain-bilal",
"version": "1.0.0",
"download_url": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-",
"homepage": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-",
"documentation": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/blob/main/README.md",
"changelog": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.4.0"
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"pull-request",
"automation",
"traceability",
"workflow",
"review"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-10T00:00:00Z",
"updated_at": "2026-04-10T00:00:00Z"
},
"presetify": {
"name": "Presetify",
"id": "presetify",
@@ -1331,8 +1428,8 @@
"id": "review",
"description": "Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification.",
"author": "ismaelJimenez",
"version": "1.0.0",
"download_url": "https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.0.zip",
"version": "1.0.1",
"download_url": "https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.1.zip",
"repository": "https://github.com/ismaelJimenez/spec-kit-review",
"homepage": "https://github.com/ismaelJimenez/spec-kit-review",
"documentation": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/README.md",
@@ -1358,7 +1455,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-03-06T00:00:00Z",
"updated_at": "2026-03-06T00:00:00Z"
"updated_at": "2026-04-09T00:00:00Z"
},
"security-review": {
"name": "Security Review",
@@ -1454,6 +1551,39 @@
"created_at": "2026-03-18T00:00:00Z",
"updated_at": "2026-03-18T00:00:00Z"
},
"spectest": {
"name": "SpecTest",
"id": "spectest",
"description": "Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements.",
"author": "Quratulain-bilal",
"version": "1.0.0",
"download_url": "https://github.com/Quratulain-bilal/spec-kit-spectest/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/Quratulain-bilal/spec-kit-spectest",
"homepage": "https://github.com/Quratulain-bilal/spec-kit-spectest",
"documentation": "https://github.com/Quratulain-bilal/spec-kit-spectest/blob/main/README.md",
"changelog": "https://github.com/Quratulain-bilal/spec-kit-spectest/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.4.0"
},
"provides": {
"commands": 4,
"hooks": 1
},
"tags": [
"testing",
"test-generation",
"coverage",
"quality",
"automation",
"traceability"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-10T16:00:00Z",
"updated_at": "2026-04-10T16:00:00Z"
},
"staff-review": {
"name": "Staff Review Extension",
"id": "staff-review",
@@ -1516,6 +1646,36 @@
"created_at": "2026-03-16T00:00:00Z",
"updated_at": "2026-03-16T00:00:00Z"
},
"status-report": {
"name": "Status Report",
"id": "status-report",
"description": "Project status, feature progress, and next-action recommendations for spec-driven workflows.",
"author": "Open-Agent-Tools",
"version": "1.2.5",
"download_url": "https://github.com/Open-Agent-Tools/spec-kit-status/archive/refs/tags/v1.2.5.zip",
"repository": "https://github.com/Open-Agent-Tools/spec-kit-status",
"homepage": "https://github.com/Open-Agent-Tools/spec-kit-status",
"documentation": "https://github.com/Open-Agent-Tools/spec-kit-status/blob/main/README.md",
"changelog": "https://github.com/Open-Agent-Tools/spec-kit-status/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 0
},
"tags": [
"workflow",
"project-management",
"status"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-08T15:05:14Z",
"updated_at": "2026-04-08T15:05:14Z"
},
"superb": {
"name": "Superpowers Bridge",
"id": "superb",
@@ -1591,6 +1751,38 @@
"created_at": "2026-03-02T00:00:00Z",
"updated_at": "2026-03-02T00:00:00Z"
},
"tinyspec": {
"name": "TinySpec",
"id": "tinyspec",
"description": "Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process.",
"author": "Quratulain-bilal",
"version": "1.0.0",
"download_url": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/Quratulain-bilal/spec-kit-tinyspec",
"homepage": "https://github.com/Quratulain-bilal/spec-kit-tinyspec",
"documentation": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/blob/main/README.md",
"changelog": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.4.0"
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"lightweight",
"small-tasks",
"workflow",
"productivity",
"efficiency"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-10T00:00:00Z",
"updated_at": "2026-04-10T00:00:00Z"
},
"v-model": {
"name": "V-Model Extension Pack",
"id": "v-model",
@@ -1628,8 +1820,8 @@
"id": "verify",
"description": "Post-implementation quality gate that validates implemented code against specification artifacts.",
"author": "ismaelJimenez",
"version": "1.0.0",
"download_url": "https://github.com/ismaelJimenez/spec-kit-verify/archive/refs/tags/v1.0.0.zip",
"version": "1.0.3",
"download_url": "https://github.com/ismaelJimenez/spec-kit-verify/archive/refs/tags/v1.0.3.zip",
"repository": "https://github.com/ismaelJimenez/spec-kit-verify",
"homepage": "https://github.com/ismaelJimenez/spec-kit-verify",
"documentation": "https://github.com/ismaelJimenez/spec-kit-verify/blob/main/README.md",
@@ -1653,7 +1845,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-03-03T00:00:00Z",
"updated_at": "2026-03-03T00:00:00Z"
"updated_at": "2026-04-09T00:00:00Z"
},
"verify-tasks": {
"name": "Verify Tasks Extension",

View File

@@ -1,6 +1,6 @@
{
"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",
"extensions": {
"git": {
@@ -10,27 +10,13 @@
"description": "Feature branch creation, numbering (sequential/timestamp), validation, and Git remote detection",
"author": "spec-kit-core",
"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": [
"git",
"branching",
"workflow",
"core"
]
},
"selftest": {
"name": "Spec Kit Self-Test Utility",
"id": "selftest",
"version": "1.0.0",
"description": "Verifies catalog extensions by programmatically walking through the discovery, installation, and registration lifecycle.",
"author": "spec-kit-core",
"repository": "https://github.com/github/spec-kit",
"download_url": "https://github.com/github/spec-kit/releases/download/selftest-v1.0.0/selftest.zip",
"tags": [
"testing",
"core",
"utility"
]
}
}
}

View File

@@ -1,6 +1,22 @@
{
"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",
"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"
]
}
}
}

View 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)

View 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.

View 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

View 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

View 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
View 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"

View File

@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.6.0"
version = "0.6.1"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [
@@ -41,6 +41,8 @@ packages = ["src/specify_cli"]
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
# Bundled extensions (installable via `specify extension add <name>`)
"extensions/git" = "specify_cli/core_pack/extensions/git"
# Bundled presets (installable via `specify preset add <name>` or `specify init --preset <name>`)
"presets/lean" = "specify_cli/core_pack/presets/lean"
[project.optional-dependencies]
test = [

View File

@@ -621,6 +621,31 @@ def _locate_bundled_extension(extension_id: str) -> Path | None:
return None
def _locate_bundled_preset(preset_id: str) -> Path | None:
"""Return the path to a bundled preset, or None.
Checks the wheel's core_pack first, then falls back to the
source-checkout ``presets/<id>/`` directory.
"""
import re as _re
if not _re.match(r'^[a-z0-9-]+$', preset_id):
return None
core = _locate_core_pack()
if core is not None:
candidate = core / "presets" / preset_id
if (candidate / "preset.yml").is_file():
return candidate
# Source-checkout / editable install: look relative to repo root
repo_root = Path(__file__).parent.parent.parent
candidate = repo_root / "presets" / preset_id
if (candidate / "preset.yml").is_file():
return candidate
return None
def _install_shared_infra(
project_path: Path,
script_type: str,
@@ -1266,27 +1291,44 @@ def init(
preset_manager = PresetManager(project_path)
speckit_ver = get_speckit_version()
# Try local directory first, then catalog
# Try local directory first, then bundled, then catalog
local_path = Path(preset).resolve()
if local_path.is_dir() and (local_path / "preset.yml").exists():
preset_manager.install_from_directory(local_path, speckit_ver)
else:
preset_catalog = PresetCatalog(project_path)
pack_info = preset_catalog.get_pack_info(preset)
if not pack_info:
console.print(f"[yellow]Warning:[/yellow] Preset '{preset}' not found in catalog. Skipping.")
bundled_path = _locate_bundled_preset(preset)
if bundled_path:
preset_manager.install_from_directory(bundled_path, speckit_ver)
else:
try:
zip_path = preset_catalog.download_pack(preset)
preset_manager.install_from_zip(zip_path, speckit_ver)
# Clean up downloaded ZIP to avoid cache accumulation
preset_catalog = PresetCatalog(project_path)
pack_info = preset_catalog.get_pack_info(preset)
if not pack_info:
console.print(f"[yellow]Warning:[/yellow] Preset '{preset}' not found in catalog. Skipping.")
elif pack_info.get("bundled") and not pack_info.get("download_url"):
from .extensions import REINSTALL_COMMAND
console.print(
f"[yellow]Warning:[/yellow] Preset '{preset}' is bundled with spec-kit "
f"but could not be found in the installed package."
)
console.print(
"This usually means the spec-kit installation is incomplete or corrupted."
)
console.print(f"Try reinstalling: {REINSTALL_COMMAND}")
else:
zip_path = None
try:
zip_path.unlink(missing_ok=True)
except OSError:
# Best-effort cleanup; failure to delete is non-fatal
pass
except PresetError as preset_err:
console.print(f"[yellow]Warning:[/yellow] Failed to install preset '{preset}': {preset_err}")
zip_path = preset_catalog.download_pack(preset)
preset_manager.install_from_zip(zip_path, speckit_ver)
except PresetError as preset_err:
console.print(f"[yellow]Warning:[/yellow] Failed to install preset '{preset}': {preset_err}")
finally:
if zip_path is not None:
# Clean up downloaded ZIP to avoid cache accumulation
try:
zip_path.unlink(missing_ok=True)
except OSError:
# Best-effort cleanup; failure to delete is non-fatal
pass
except Exception as preset_err:
console.print(f"[yellow]Warning:[/yellow] Failed to install preset: {preset_err}")
@@ -1338,7 +1380,7 @@ def init(
step_num = 2
# 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
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)
@@ -1347,7 +1389,8 @@ def init(
kimi_skill_mode = selected_ai == "kimi"
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
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:
# Integration path installed skills; show the helpful notice
@@ -1356,6 +1399,9 @@ def init(
if claude_skill_mode and not ai_skills:
steps_lines.append(f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]")
step_num += 1
if cursor_agent_skill_mode and not ai_skills:
steps_lines.append(f"{step_num}. Start Cursor Agent in this project directory; spec-kit skills were installed to [cyan].cursor/skills[/cyan]")
step_num += 1
usage_label = "skills" if native_skill_mode else "slash commands"
def _display_cmd(name: str) -> str:
@@ -1365,6 +1411,8 @@ def init(
return f"/speckit-{name}"
if kimi_skill_mode:
return f"/skill:speckit-{name}"
if cursor_agent_skill_mode:
return f"/speckit-{name}"
return f"/speckit.{name}"
steps_lines.append(f"{step_num}. Start using {usage_label} with your AI agent:")
@@ -2134,28 +2182,50 @@ def preset_add(
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
elif pack_id:
catalog = PresetCatalog(project_root)
pack_info = catalog.get_pack_info(pack_id)
if not pack_info:
console.print(f"[red]Error:[/red] Preset '{pack_id}' not found in catalog")
raise typer.Exit(1)
if not pack_info.get("_install_allowed", True):
catalog_name = pack_info.get("_catalog_name", "unknown")
console.print(f"[red]Error:[/red] Preset '{pack_id}' is from the '{catalog_name}' catalog which is discovery-only (install not allowed).")
console.print("Add the catalog with --install-allowed or install from the preset's repository directly with --from.")
raise typer.Exit(1)
console.print(f"Installing preset [cyan]{pack_info.get('name', pack_id)}[/cyan]...")
try:
zip_path = catalog.download_pack(pack_id)
manifest = manager.install_from_zip(zip_path, speckit_version, priority)
# Try bundled preset first, then catalog
bundled_path = _locate_bundled_preset(pack_id)
if bundled_path:
console.print(f"Installing bundled preset [cyan]{pack_id}[/cyan]...")
manifest = manager.install_from_directory(bundled_path, speckit_version, priority)
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:
catalog = PresetCatalog(project_root)
pack_info = catalog.get_pack_info(pack_id)
if not pack_info:
console.print(f"[red]Error:[/red] Preset '{pack_id}' not found in catalog")
raise typer.Exit(1)
# Bundled presets should have been caught above; if we reach
# here the bundled files are missing from the installation.
if pack_info.get("bundled") and not pack_info.get("download_url"):
from .extensions import REINSTALL_COMMAND
console.print(
f"[red]Error:[/red] Preset '{pack_id}' is bundled with spec-kit "
f"but could not be found in the installed package."
)
console.print(
"\nThis usually means the spec-kit installation is incomplete or corrupted."
)
console.print("Try reinstalling spec-kit:")
console.print(f" {REINSTALL_COMMAND}")
raise typer.Exit(1)
if not pack_info.get("_install_allowed", True):
catalog_name = pack_info.get("_catalog_name", "unknown")
console.print(f"[red]Error:[/red] Preset '{pack_id}' is from the '{catalog_name}' catalog which is discovery-only (install not allowed).")
console.print("Add the catalog with --install-allowed or install from the preset's repository directly with --from.")
raise typer.Exit(1)
console.print(f"Installing preset [cyan]{pack_info.get('name', pack_id)}[/cyan]...")
try:
zip_path = catalog.download_pack(pack_id)
manifest = manager.install_from_zip(zip_path, speckit_version, priority)
console.print(f"[green]✓[/green] Preset '{manifest.name}' v{manifest.version} installed (priority {priority})")
finally:
if 'zip_path' in locals() and zip_path.exists():
zip_path.unlink(missing_ok=True)
else:
console.print("[red]Error:[/red] Specify a preset ID, --from URL, or --dev path")
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)"),
):
"""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()
@@ -3103,6 +3173,19 @@ def extension_add(
manifest = manager.install_from_directory(bundled_path, speckit_version, priority=priority)
if bundled_path is None:
# Bundled extensions without a download URL must come from the local package
if ext_info.get("bundled") and not ext_info.get("download_url"):
console.print(
f"[red]Error:[/red] Extension '{ext_info['id']}' is bundled with spec-kit "
f"but could not be found in the installed package."
)
console.print(
"\nThis usually means the spec-kit installation is incomplete or corrupted."
)
console.print("Try reinstalling spec-kit:")
console.print(f" {REINSTALL_COMMAND}")
raise typer.Exit(1)
# Enforce install_allowed policy
if not ext_info.get("_install_allowed", True):
catalog_name = ext_info.get("_catalog_name", "community")

View File

@@ -38,6 +38,8 @@ _FALLBACK_CORE_COMMAND_NAMES = frozenset({
})
EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$")
REINSTALL_COMMAND = "uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git"
def _load_core_command_names() -> frozenset[str]:
"""Discover bundled core command names from the packaged templates.
@@ -1870,6 +1872,14 @@ class ExtensionCatalog:
if not ext_info:
raise ExtensionError(f"Extension '{extension_id}' not found in catalog")
# Bundled extensions without a download URL must be installed locally
if ext_info.get("bundled") and not ext_info.get("download_url"):
raise ExtensionError(
f"Extension '{extension_id}' is bundled with spec-kit and has no download URL. "
f"It should be installed from the local package. "
f"Try reinstalling: {REINSTALL_COMMAND}"
)
download_url = ext_info.get("download_url")
if not download_url:
raise ExtensionError(f"Extension '{extension_id}' has no download URL")
@@ -2170,6 +2180,7 @@ class HookExecutor:
codex_skill_mode = selected_ai == "codex" and bool(init_options.get("ai_skills"))
claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills"))
kimi_skill_mode = selected_ai == "kimi"
cursor_skill_mode = selected_ai == "cursor-agent" and bool(init_options.get("ai_skills"))
skill_name = self._skill_name_from_command(command_id)
if codex_skill_mode and skill_name:
@@ -2178,6 +2189,8 @@ class HookExecutor:
return f"/{skill_name}"
if kimi_skill_mode and skill_name:
return f"/skill:{skill_name}"
if cursor_skill_mode and skill_name:
return f"/{skill_name}"
return f"/{command_id}"

View File

@@ -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"
config = {
"name": "Cursor",
"folder": ".cursor/",
"commands_subdir": "commands",
"commands_subdir": "skills",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".cursor/commands",
"dir": ".cursor/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
"extension": "/SKILL.md",
}
context_file = ".cursor/rules/specify-rules.mdc"
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills (recommended for Cursor)",
),
]

View File

@@ -1587,6 +1587,16 @@ class PresetCatalog:
f"Preset '{pack_id}' not found in catalog"
)
# Bundled presets without a download URL must be installed locally
if pack_info.get("bundled") and not pack_info.get("download_url"):
from .extensions import REINSTALL_COMMAND
raise PresetError(
f"Preset '{pack_id}' is bundled with spec-kit and has no download URL. "
f"It should be installed from the local package. "
f"Use 'specify preset add {pack_id}' to install from the bundled package, "
f"or reinstall spec-kit if the bundled files are missing: {REINSTALL_COMMAND}"
)
if not pack_info.get("_install_allowed", True):
catalog_name = pack_info.get("_catalog_name", "unknown")
raise PresetError(

View File

@@ -1,11 +1,28 @@
"""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"
FOLDER = ".cursor/"
COMMANDS_SUBDIR = "commands"
REGISTRAR_DIR = ".cursor/commands"
COMMANDS_SUBDIR = "skills"
REGISTRAR_DIR = ".cursor/skills"
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()

View File

@@ -2995,6 +2995,122 @@ class TestExtensionAddCLI:
f"but was called with '{download_called_with[0]}'"
)
def test_add_bundled_extension_not_found_gives_clear_error(self, tmp_path):
"""extension add should give a clear error when a bundled extension is not found locally."""
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from specify_cli import app
runner = CliRunner()
# Create project structure
project_dir = tmp_path / "test-project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
(project_dir / ".specify" / "extensions").mkdir(parents=True)
# Mock catalog that returns a bundled extension without download_url
mock_catalog = MagicMock()
mock_catalog.get_extension_info.return_value = {
"id": "git",
"name": "Git Branching Workflow",
"version": "1.0.0",
"description": "Git branching extension",
"bundled": True,
"_install_allowed": True,
}
mock_catalog.search.return_value = []
with patch("specify_cli.extensions.ExtensionCatalog", return_value=mock_catalog), \
patch("specify_cli._locate_bundled_extension", return_value=None), \
patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(
app,
["extension", "add", "git"],
catch_exceptions=True,
)
assert result.exit_code != 0
assert "bundled with spec-kit" in result.output
assert "reinstall" in result.output.lower()
class TestDownloadExtensionBundled:
"""Tests for download_extension handling of bundled extensions."""
def test_download_extension_raises_for_bundled(self, temp_dir):
"""download_extension should raise a clear error for bundled extensions without a URL."""
from unittest.mock import patch
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
bundled_ext_info = {
"name": "Git Branching Workflow",
"id": "git",
"version": "1.0.0",
"description": "Git workflow",
"bundled": True,
}
with patch.object(catalog, "get_extension_info", return_value=bundled_ext_info):
with pytest.raises(ExtensionError, match="bundled with spec-kit"):
catalog.download_extension("git")
def test_download_extension_allows_bundled_with_url(self, temp_dir):
"""download_extension should allow bundled extensions that have a download_url (newer version)."""
from unittest.mock import patch, MagicMock
import urllib.request
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
bundled_with_url = {
"name": "Git Branching Workflow",
"id": "git",
"version": "2.0.0",
"description": "Git workflow",
"bundled": True,
"download_url": "https://example.com/git-2.0.0.zip",
}
mock_response = MagicMock()
mock_response.read.return_value = b"fake zip data"
mock_response.__enter__ = lambda s: s
mock_response.__exit__ = MagicMock(return_value=False)
with patch.object(catalog, "get_extension_info", return_value=bundled_with_url), \
patch.object(urllib.request, "urlopen", return_value=mock_response):
result = catalog.download_extension("git")
assert result.name == "git-2.0.0.zip"
def test_download_extension_raises_no_url_for_non_bundled(self, temp_dir):
"""download_extension should raise 'no download URL' for non-bundled extensions without URL."""
from unittest.mock import patch
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / ".specify").mkdir()
catalog = ExtensionCatalog(project_dir)
non_bundled_ext_info = {
"name": "Some Extension",
"id": "some-ext",
"version": "1.0.0",
"description": "Test",
}
with patch.object(catalog, "get_extension_info", return_value=non_bundled_ext_info):
with pytest.raises(ExtensionError, match="has no download URL"):
catalog.download_extension("some-ext")
class TestExtensionUpdateCLI:
"""CLI integration tests for extension update command."""

View File

@@ -2865,3 +2865,182 @@ class TestPresetEnableDisable:
assert result.exit_code == 1
assert "corrupted state" in result.output.lower()
# ===== Lean Preset Tests =====
LEAN_PRESET_DIR = Path(__file__).parent.parent / "presets" / "lean"
LEAN_COMMAND_NAMES = [
"speckit.specify",
"speckit.plan",
"speckit.tasks",
"speckit.implement",
"speckit.constitution",
]
class TestLeanPreset:
"""Tests for the lean preset that ships with the repo."""
def test_lean_preset_exists(self):
"""Verify the lean preset directory and manifest exist."""
assert LEAN_PRESET_DIR.exists()
assert (LEAN_PRESET_DIR / "preset.yml").exists()
def test_lean_manifest_valid(self):
"""Verify the lean preset manifest is valid."""
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
assert manifest.id == "lean"
assert manifest.name == "Lean Workflow"
assert manifest.version == "1.0.0"
assert len(manifest.templates) == 5 # 5 commands
def test_lean_provides_core_workflow_commands(self):
"""Verify the lean preset provides overrides for core workflow commands."""
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
provided_names = {t["name"] for t in manifest.templates}
for name in LEAN_COMMAND_NAMES:
assert name in provided_names, f"Lean preset missing command: {name}"
def test_lean_command_files_exist(self):
"""Verify that all declared command files actually exist on disk."""
manifest = PresetManifest(LEAN_PRESET_DIR / "preset.yml")
for tmpl in manifest.templates:
tmpl_path = LEAN_PRESET_DIR / tmpl["file"]
assert tmpl_path.exists(), f"Missing command file: {tmpl['file']}"
def test_lean_commands_have_no_scripts(self):
"""Verify lean commands have no scripts or agent_scripts in frontmatter."""
from specify_cli.agents import CommandRegistrar
for name in LEAN_COMMAND_NAMES:
cmd_path = LEAN_PRESET_DIR / "commands" / f"speckit.{name.split('.')[-1]}.md"
content = cmd_path.read_text()
frontmatter, _ = CommandRegistrar.parse_frontmatter(content)
assert "scripts" not in frontmatter, f"{name} should not have scripts in frontmatter"
assert "agent_scripts" not in frontmatter, f"{name} should not have agent_scripts in frontmatter"
def test_lean_commands_have_no_hooks(self):
"""Verify lean commands do not contain extension hook boilerplate."""
for name in LEAN_COMMAND_NAMES:
cmd_path = LEAN_PRESET_DIR / "commands" / f"speckit.{name.split('.')[-1]}.md"
content = cmd_path.read_text()
assert "hooks." not in content, f"{name} should not reference extension hooks"
assert "extensions.yml" not in content, f"{name} should not reference extensions.yml"
def test_install_lean_preset(self, project_dir):
"""Test installing the lean preset from its directory."""
manager = PresetManager(project_dir)
manifest = manager.install_from_directory(LEAN_PRESET_DIR, "0.6.0")
assert manifest.id == "lean"
assert manager.registry.is_installed("lean")
def test_lean_overrides_commands(self, project_dir):
"""Test that lean preset overrides are resolved correctly."""
manager = PresetManager(project_dir)
manager.install_from_directory(LEAN_PRESET_DIR, "0.6.0")
resolver = PresetResolver(project_dir)
for name in LEAN_COMMAND_NAMES:
result = resolver.resolve(name, template_type="command")
assert result is not None, f"Lean override for {name} not resolved"
# ===== Bundled Preset Locator Tests =====
class TestBundledPresetLocator:
"""Tests for _locate_bundled_preset discovery function."""
def test_locate_bundled_lean_preset(self):
"""_locate_bundled_preset finds the lean preset."""
from specify_cli import _locate_bundled_preset
path = _locate_bundled_preset("lean")
assert path is not None
assert (path / "preset.yml").is_file()
def test_locate_bundled_preset_not_found(self):
"""_locate_bundled_preset returns None for nonexistent preset."""
from specify_cli import _locate_bundled_preset
path = _locate_bundled_preset("nonexistent-preset")
assert path is None
def test_locate_bundled_preset_rejects_invalid_id(self):
"""_locate_bundled_preset rejects IDs with invalid characters."""
from specify_cli import _locate_bundled_preset
assert _locate_bundled_preset("../escape") is None
assert _locate_bundled_preset("UPPERCASE") is None
assert _locate_bundled_preset("has spaces") is None
def test_bundled_preset_add_via_cli(self, project_dir):
"""Test that 'specify preset add lean' installs the bundled preset."""
from typer.testing import CliRunner
from unittest.mock import patch
from specify_cli import app
runner = CliRunner()
with patch.object(Path, "cwd", return_value=project_dir), \
patch("specify_cli.get_speckit_version", return_value="0.6.0"):
result = runner.invoke(app, ["preset", "add", "lean"])
assert result.exit_code == 0, result.output
assert "Lean Workflow" in result.output
assert "installed" in result.output.lower()
def test_bundled_preset_in_catalog(self):
"""Verify the lean preset is listed in catalog.json with bundled marker."""
catalog_path = Path(__file__).parent.parent / "presets" / "catalog.json"
catalog = json.loads(catalog_path.read_text())
assert "lean" in catalog["presets"]
assert catalog["presets"]["lean"]["bundled"] is True
assert "download_url" not in catalog["presets"]["lean"]
def test_bundled_preset_download_raises_error(self, project_dir):
"""download_pack raises PresetError for bundled presets without download_url."""
catalog = PresetCatalog(project_dir)
catalog_data = {
"test-bundled": {
"name": "Test Bundled",
"version": "1.0.0",
"bundled": True,
}
}
from unittest.mock import patch
with patch.object(catalog, "_get_merged_packs", return_value=catalog_data):
with pytest.raises(PresetError, match="bundled with spec-kit"):
catalog.download_pack("test-bundled")
def test_bundled_preset_missing_locally_cli_error(self, project_dir):
"""CLI shows clear error when bundled preset cannot be found locally."""
from typer.testing import CliRunner
from unittest.mock import patch
from specify_cli import app
runner = CliRunner()
# Patch _locate_bundled_preset to return None (simulating missing files)
# and mock the catalog to return a bundled entry for "lean"
fake_pack_info = {
"id": "lean",
"name": "Lean Workflow",
"version": "1.0.0",
"bundled": True,
"_install_allowed": True,
}
with patch.object(Path, "cwd", return_value=project_dir), \
patch("specify_cli._locate_bundled_preset", return_value=None), \
patch("specify_cli.presets.PresetCatalog") as MockCatalog:
MockCatalog.return_value.get_pack_info.return_value = fake_pack_info
result = runner.invoke(app, ["preset", "add", "lean"])
# Should fail with a helpful error explaining this is a bundled preset
# and suggesting how to recover.
assert result.exit_code == 1
output = strip_ansi(result.output).lower()
assert "bundled" in output, result.output
assert "reinstall" in output, result.output