Compare commits

..

88 Commits

Author SHA1 Message Date
Manfred Riem
9b10d440b1 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-13 15:14:46 -05:00
github-actions[bot]
2656ee1df7 chore: bump version to 0.3.0 2026-03-13 20:11:58 +00:00
Copilot
69ee7a836e feat(presets): Pluggable preset system with catalog, resolver, and skills propagation (#1787)
* Initial plan

* feat(templates): add pluggable template system with packs, catalog, resolver, and CLI commands

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* test(templates): add comprehensive unit tests for template pack system

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* feat(presets): pluggable preset system with template/command overrides, catalog, and resolver

- Rename 'template packs' to 'presets' to avoid naming collision with core templates
- PresetManifest, PresetRegistry, PresetManager, PresetCatalog, PresetResolver in presets.py
- Extract CommandRegistrar to agents.py as shared infrastructure
- CLI: specify preset list/add/remove/search/resolve/info
- CLI: specify preset catalog list/add/remove
- --preset option on specify init
- Priority-based preset stacking (--priority, lower = higher precedence)
- Command overrides registered into all detected agent directories (17+ agents)
- Extension command safety: skip registration if target extension not installed
- Multi-catalog support: env var, project config, user config, built-in defaults
- resolve_template() / Resolve-Template in bash/PowerShell scripts
- Self-test preset: overrides all 6 core templates + 1 command
- Scaffold with 4 examples: core/extension template and command overrides
- Preset catalog (catalog.json, catalog.community.json)
- Documentation: README.md, ARCHITECTURE.md, PUBLISHING.md
- 110 preset tests, 253 total tests passing

* feat(presets): propagate command overrides to skills via init-options

- Add save_init_options() / load_init_options() helpers that persist
  CLI flags from 'specify init' to .specify/init-options.json
- PresetManager._register_skills() overwrites SKILL.md files when
  --ai-skills was used during init and corresponding skill dirs exist
- PresetManager._unregister_skills() restores core template content
  on preset removal
- registered_skills stored in preset registry metadata
- 8 new tests covering skill override, skip conditions, and restore

* fix: address PR check failures (ruff F541, CodeQL URL substring)

- Remove extraneous f-prefix from two f-strings without placeholders
- Replace substring URL check in test with startswith/endswith assertions
  to satisfy CodeQL incomplete URL substring sanitization rule

* fix: address Copilot PR review comments

- Move save_init_options() before preset install so skills propagation
  works during 'specify init --preset --ai-skills'
- Clean up downloaded ZIP after successful preset install during init
- Validate --from URL scheme (require HTTPS, HTTP only for localhost)
- Expose unregister_commands() on extensions.py CommandRegistrar wrapper
  instead of reaching into private _registrar field
- Use _get_merged_packs() for search() and get_pack_info() so all
  active catalogs are searched, not just the highest-priority one
- Fix fetch_catalog() cache to verify cached URL matches current URL
- Fix PresetResolver: script resolution uses .sh extension, consistent
  file extensions throughout resolve(), and resolve_with_source()
  delegates to resolve() to honor template_type parameter
- Fix bash common.sh: fall through to directory scan when python3
  returns empty preset list
- Fix PowerShell Resolve-Template: filter out dot-folders and sort
  extensions deterministically

* fix: narrow empty except blocks and add explanatory comments

* fix: address Copilot PR review comments (round 2)

- Fix init --preset error masking: distinguish "not found" from real errors
- Fix bash resolve_template: skip hidden dirs in extensions (match Python/PS)
- Fix temp dir leaks in tests: use temp_dir fixture instead of mkdtemp
- Fix self-test catalog entry: add note that it's local-only (no download_url)
- Fix Windows path issue in resolve_with_source: use Path.relative_to()
- Fix skill restore path: use project's .specify/templates/commands/ not source tree
- Add encoding="utf-8" to all file read/write in agents.py
- Update test to set up core command templates for skill restoration

* fix: remove self-test from catalog.json (local-only preset)

* fix: address Copilot PR review comments (round 3)

- Fix PS Resolve-Template fallback to skip dot-prefixed dirs (.cache)
- Rename _catalog to _catalog_name for consistency with extension system
- Enforce install_allowed policy in CLI preset add and download_pack()
- Fix shell injection: pass registry path via env var instead of string interpolation

* fix: correct PresetError docstring from template to preset

* Removed CHANGELOG requirement

* Applying review recommendations

* Applying review recommendations

* Applying review recommendations

* Applying review recommendations

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
2026-03-13 15:09:14 -05:00
eason
c883952b43 fix: match 'Last updated' timestamp with or without bold markers (#1836)
The template outputs plain text `Last updated: [DATE]` but both
update-agent-context scripts only matched `**Last updated**: [DATE]`
(bold Markdown). Make the bold markers optional in the regex so the
timestamp is refreshed regardless of formatting.

Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Claude (claude-opus-4-6) <noreply@anthropic.com>
2026-03-13 11:46:21 -05:00
KhawarHabibKhan
b9c1a1c7bb Add specify doctor command for project health diagnostics (#1828)
* Add specify doctor command for project health diagnostics

* Add tests for specify doctor command

* Document specify doctor command in README

* Revert "Document specify doctor command in README"

This reverts commit c1cfd06129.

* Revert "Add tests for specify doctor command"

This reverts commit 65e12fb62b.

* Revert "Add specify doctor command for project health diagnostics"

This reverts commit d5bd93248a.

* Add doctor extension to community catalog

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-13 10:50:07 -05:00
Pierluigi Lenoci
46bc65b1ce fix: harden bash scripts against shell injection and improve robustness (#1809)
- Replace eval of unquoted get_feature_paths output with safe pattern:
  capture into variable, check return code, then eval quoted result
- Use printf '%q' in get_feature_paths to safely emit shell assignments,
  preventing injection via paths containing quotes or metacharacters
- Add json_escape() helper for printf JSON fallback paths, handling
  backslash, double-quote, and control characters when jq is unavailable
- Use jq -cn for safe JSON construction with proper escaping when
  available, with printf + json_escape() fallback
- Replace declare -A (bash 4+) with indexed array for bash 3.2
  compatibility (macOS default)
- Use inline command -v jq check in create-new-feature.sh since it
  does not source common.sh
- Guard trap cleanup against re-entrant invocation by disarming traps
  at entry
- Use printf '%q' for shell-escaped branch names in user-facing output
- Return failure instead of silently returning wrong path on ambiguous
  spec directory matches
- Deduplicate agent file updates via realpath to prevent multiple writes
  to the same file (e.g. AGENTS.md aliased by multiple variables)
2026-03-13 10:47:17 -05:00
Pierluigi Lenoci
017e1c4c2f fix: clean up command templates (specify, analyze) (#1810)
* fix: clean up command templates (specify, analyze)

- specify.md: fix self-referential step number (step 6c → proceed to step 7)
- specify.md: remove empty "General Guidelines" heading before "Quick Guidelines"
- analyze.md: deduplicate {ARGS} — already present in "User Input" section at top

* fix: restore ## Context heading in analyze template

Address PR review feedback from @dhilipkumars: keep the ## Context
markdown heading to preserve structural hierarchy for LLM parsing.
2026-03-13 08:21:55 -05:00
fuyongde
7562664fd1 fix: migrate Qwen Code CLI from TOML to Markdown format (#1589) (#1730)
* fix: migrate Qwen Code CLI from TOML to Markdown format (#1589)

Qwen Code CLI v0.10.0 deprecated TOML format and fully switched to
Markdown as the core format for configuration and interaction files.

- Update create-release-packages.sh: generate .md files with $ARGUMENTS
  instead of .toml files with {{args}} for qwen agent
- Update create-release-packages.ps1: same change for PowerShell script
- Update AGENTS.md: reflect Qwen's new Markdown format in docs and
  remove Qwen from TOML format section
- Update tests/test_ai_skills.py: add commands_dir_qwen fixture and
  tests covering Markdown-format skills installation for Qwen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: update CommandRegistrar qwen config to Markdown format

extensions.py CommandRegistrar.AGENT_CONFIGS['qwen'] was still set to
TOML format, causing `specify extension` to write .toml files into
.qwen/commands, conflicting with Qwen Code CLI v0.10.0+ expectations.

- Change qwen format from toml to markdown
- Change qwen args from {{args}} to $ARGUMENTS
- Change qwen extension from .toml to .md
- Add test to assert qwen config is Markdown format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 07:43:14 -05:00
Dhilip
976c9981a4 fix(cli): deprecate explicit command support for agy (#1798) (#1808)
* fix(cli): deprecate explicit command support for agy (#1798)

* docs(cli): add tests and docs for agy deprecation (#1798)

* fix: address review comments for agy deprecation

* fix: address round 2 review comments for agy deprecation

* fix: address round 3 review comments for agy deprecation

* fix: address round 4 review comments for agy deprecation

* fix: address round 5 review comments for agy deprecation

* docs: add inline contextual comments to explain agy deprecation

* docs: clarify historical context in agy deprecation docstring

* fix: correct skills path in deprecation comment and make test mock fully deterministic
2026-03-13 07:35:30 -05:00
Dhilip
d3fc056743 Add /selftest.extension core extension to test other extensions (#1758)
* test(commands): create extension-commands LLM playground sandbox

* update(tests): format LLM evaluation as an automated test runner

* test(commands): map extension-commands python script with timestamps

* test(commands): map extension-commands python script with timestamps

* test(commands): update TESTING.md to evaluate discovery, lint, and deploy explicitly

* test(commands): simplify execution expectations and add timestamp calculation

* fix(tests): address copilot review comments on prompt formatting and relative paths

* fix(tests): resolve copilot PR feedback regarding extension schema structure and argparse mutually exclusive groups

* feat(extensions): add core selftest utility and migrate away from manual tests sandbox

* fix(selftest): update command name array to match spec-kit validation schema

* fix(selftest): wrap arguments in quotes to support multi-word extension names

* update the command to be more meaningful

* fix: if the extension is discovery only, it should not be installable

* Address review comments for selftest documentation

* address review comments

* address review comments

* Update extensions/selftest/commands/selftest.md

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

---------

Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-13 07:26:01 -05:00
Michal Bachorik
58ce653908 feat(extensions): Quality of life improvements for RFC-aligned catalog integration (#1776)
* feat(extensions): implement automatic updates with atomic backup/restore

- Implement automatic extension updates with download from catalog
- Add comprehensive backup/restore mechanism for failed updates:
  - Backup registry entry before update
  - Backup extension directory
  - Backup command files for all AI agents
  - Backup hooks from extensions.yml
- Add extension ID verification after install
- Add KeyboardInterrupt handling to allow clean cancellation
- Fix enable/disable to preserve installed_at timestamp by using
  direct registry manipulation instead of registry.add()
- Add rollback on any update failure with command file,
  hook, and registry restoration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(extensions): comprehensive name resolution and error handling improvements

- Add shared _resolve_installed_extension helper for ID/display name resolution
  with proper ambiguous name handling (shows table of matches)
- Add _resolve_catalog_extension helper for catalog lookups by ID or display name
- Update enable/disable/update/remove commands to use name resolution helpers
- Fix extension_info to handle catalog errors gracefully:
  - Fallback to local installed info when catalog unavailable
  - Distinguish "catalog unavailable" from "not found in catalog"
  - Support display name lookup for both installed and catalog extensions
- Use resolved display names in all status messages for consistency
- Extract _print_extension_info helper for DRY catalog info printing

Addresses reviewer feedback:
- Ambiguous name handling in enable/disable/update
- Catalog error fallback for installed extensions
- UX message clarity (catalog unavailable vs not found)
- Resolved ID in status messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(extensions): properly detect ambiguous names in extension_info

The extension_info command was breaking on the first name match without
checking for ambiguity. This fix separates ID matching from name matching
and checks for ambiguity before selecting a match, consistent with the
_resolve_installed_extension() helper used by other commands.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(extensions): add public update() method to ExtensionRegistry

Add a proper public API for updating registry metadata while preserving
installed_at timestamp, instead of directly mutating internal registry
data and calling private _save() method.

Changes:
- Add ExtensionRegistry.update() method that preserves installed_at
- Update enable/disable commands to use registry.update()
- Update rollback logic to use registry.update()

This decouples the CLI from registry internals and maintains proper
encapsulation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(extensions): safely access optional author field in extension_info

ExtensionManifest doesn't expose an author property - the author field
is optional in extension.yml and stored in data["extension"]["author"].
Use safe dict access to avoid AttributeError.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(extensions): address multiple reviewer comments

- ExtensionRegistry.update() now preserves original installed_at timestamp
- Add ExtensionRegistry.restore() for rollback (entry was removed)
- Clean up wrongly installed extension on ID mismatch before rollback
- Remove unused catalog_error parameter from _print_extension_info()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(extensions): check _install_allowed for updates, preserve backup on failed rollback

- Skip automatic updates for extensions from catalogs with install_allowed=false
- Only delete backup directory on successful rollback, preserve it on failure
  for manual recovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(extensions): address reviewer feedback on update/rollback logic

- Hook rollback: handle empty backup_hooks by checking `is not None`
  instead of truthiness (falsy empty dict would skip hook cleanup)
- extension_info: use resolved_installed_id for catalog lookup when
  extension was found by display name (prevents wrong catalog match)
- Rollback: always remove extension dir first, then restore if backup
  exists (handles case when no original dir existed before update)
- Validate extension ID from ZIP before installing, not after
  (avoids side effects of installing wrong extension before rollback)
- Preserve enabled state during updates: re-apply disabled state and
  hook enabled flags after successful update
- Optimize _resolve_catalog_extension: pass query to catalog.search()
  instead of fetching all extensions
- update() now merges metadata with existing entry instead of replacing
  (preserves fields like registered_commands when only updating enabled)
- Add tests for ExtensionRegistry.update() and restore() methods:
  - test_update_preserves_installed_at
  - test_update_merges_with_existing
  - test_update_raises_for_missing_extension
  - test_restore_overwrites_completely
  - test_restore_can_recreate_removed_entry

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs(extensions): update RFC to reflect implemented status

- Change status from "Draft" to "Implemented"
- Update all Implementation Phases to show completed items
- Add new features implemented beyond original RFC:
  - Display name resolution for all commands
  - Ambiguous name handling with tables
  - Atomic update with rollback
  - Pre-install ID validation
  - Enabled state preservation
  - Registry update/restore methods
  - Catalog error fallback
  - _install_allowed flag
  - Cache invalidation
- Convert Open Questions to Resolved Questions with decisions
- Add remaining Open Questions (sandboxing, signatures) as future work
- Fix table of contents links

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(extensions): address third round of PR review comments

- Refactor extension_info to use _resolve_installed_extension() helper
  with new allow_not_found parameter instead of duplicating resolution logic
- Fix rollback hook restoration to not create empty hooks: {} in config
  when original config had no hooks section
- Fix ZIP pre-validation to handle nested extension.yml files (GitHub
  auto-generated ZIPs have structure like repo-name-branch/extension.yml)
- Replace unused installed_manifest variable with _ placeholder
- Add display name to update status messages for better UX

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(extensions): address fourth round of PR review comments

Rollback fixes:
- Preserve installed_at timestamp after successful update (was reset by
  install_from_zip calling registry.add)
- Fix rollback to only delete extension_dir if backup exists (avoids
  destroying valid installation when failure happens before modification)
- Fix rollback to remove NEW command files created by failed install
  (files that weren't in original backup are now cleaned up)
- Fix rollback to delete hooks key entirely when backup_hooks is None
  (original config had no hooks key, so restore should remove it)

Cross-command consistency fix:
- Add display name resolution to `extension add` command using
  _resolve_catalog_extension() helper (was only in `extension info`)
- Use resolved extension ID for download_extension() call, not original
  argument which may be a display name

Security fix (fail-closed):
- Malformed catalog config (empty/missing URLs) now raises ValidationError
  instead of silently falling back to built-in catalogs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(lint): address ruff linting errors and registry.update() semantics

- Remove unused import ExtensionError in extension_info
- Remove extraneous f-prefix from strings without placeholders
- Use registry.restore() instead of registry.update() for installed_at
  preservation (update() always preserves existing installed_at, ignoring
  our override)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: iamaeroplane <michal.bachorik@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 07:23:37 -05:00
Manfred Riem
82f8a13f83 Add Java brownfield walkthrough to community walkthroughs (#1820) 2026-03-12 14:02:02 -05:00
Manfred Riem
0f1cbd74fe chore: bump version to 0.2.1 (#1813)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-11 17:41:40 -05:00
Manfred Riem
ec60c5b2fe Added February 2026 newsletter (#1812) 2026-03-11 15:33:30 -05:00
Fanch Daniel
e56d37db8c feat: add Kimi Code CLI agent support (#1790)
* feat: add Kimi Code (kimi) CLI agent support

- Register kimi in AGENT_CONFIG with folder `.kimi/`, markdown format, requires_cli=True
- Register kimi in CommandRegistrar.AGENT_CONFIGS
- Add kimi to supported agents table in AGENTS.md and README.md
- Add kimi to release packaging scripts (bash and PowerShell)
- Add kimi CLI installation to devcontainer post-create script
- Add kimi support to update-agent-context scripts (bash and PowerShell)
- Add 4 consistency tests covering all kimi integration surfaces
- Bump version to 0.1.14 and update CHANGELOG

* fix: include .specify/templates/ and real command files in release ZIPs

- Copy real command files from templates/commands/ (with speckit. prefix)
  instead of generating stubs, so slash commands have actual content
- Add .specify/templates/ to every ZIP so ensure_constitution_from_template
  can find constitution-template.md on init
- Add .vscode/settings.json to every ZIP
- Having 3 top-level dirs prevents the extraction flatten heuristic from
  incorrectly stripping the agent config folder (.kimi/, .claude/, etc.)
- Bump version to 0.1.14.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(kimi): use .kimi/skills/<name>/SKILL.md structure for Kimi Code CLI

Kimi Code CLI uses a skills system, not flat command files:
- Skills live in .kimi/skills/<name>/SKILL.md (project-level)
- Invoked with /skill:<name> (e.g. /skill:speckit.specify)
- Each skill is a directory containing SKILL.md with YAML frontmatter

Changes:
- AGENT_CONFIG["kimi"]["commands_subdir"] = "skills" (was "commands")
- create-release-packages.sh: new create_kimi_skills() function creates
  skill directories with SKILL.md from real template content
- Bump version to 0.1.14.2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): align kimi commands_subdir assertion with skills structure

* fix: use forward slashes for tabnine path in create-release-packages.ps1

* fix: align kimi to .kimi/skills convention and fix ARGUMENTS unbound variable

* fix: address PR review comments for kimi agent support

  - Fix VERSION_NO_V undefined variable in create-github-release.sh
  - Restore version $1 argument handling in create-release-packages.sh
  - Fix tabnine/vibe/generic cases calling undefined generate_commands
  - Align roo path .roo/rules -> .roo/commands with AGENT_CONFIG
  - Fix kimi extension to use per-skill SKILL.md directory structure
  - Add parent mkdir before dest_file.write_text for nested paths
  - Restore devcontainer tools removed by regression + add Kimi CLI
  - Strengthen test_kimi_in_powershell_validate_set assertion

* fix: restore release scripts and address all PR review comments

  - Restore create-release-packages.sh to original with full generate_commands/
    rewrite_paths logic; add kimi case using create_kimi_skills function
  - Restore create-release-packages.ps1 to original with full Generate-Commands/
    Rewrite-Paths logic; add kimi case using New-KimiSkills function
  - Restore create-github-release.sh to original with proper $1 argument
    handling and VERSION_NO_V; add kimi zip entries
  - Add test_ai_help_includes_kimi for consistency with other agents
  - Strengthen test_kimi_in_powershell_validate_set to check ValidateSet

* fix: address second round of PR review comments

  - Add __AGENT__ and {AGENT_SCRIPT} substitutions in create_kimi_skills (bash)
  - Add __AGENT__ and {AGENT_SCRIPT} substitutions in New-KimiSkills (PowerShell)
  - Replace curl|bash Kimi installer with pipx install kimi-cli in post-create.sh

* fix: align kimi skill naming and add extension registrar test

  - Fix install_ai_skills() to use speckit.<cmd> naming for kimi (dot
    separator) instead of speckit-<cmd>, matching /skill:speckit.<cmd>
    invocation convention and packaging scripts
  - Add test_kimi_in_extension_registrar to verify CommandRegistrar.AGENT_CONFIGS
    includes kimi with correct dir and SKILL.md extension

* fix(test): align kimi skill name assertion with dot-separator convention

  test_skills_install_for_all_agents now expects speckit.specify (dot) for
  kimi and speckit-specify (hyphen) for all other agents, matching the
  install_ai_skills() implementation added in the previous commit.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 07:57:18 -05:00
Dhilip
33e853e9c9 docs: fix broken links in quickstart guide (#1759) (#1797) 2026-03-11 07:51:04 -05:00
Dhilip
929fab5d98 docs: add catalog cli help documentation (#1793) (#1794)
* docs: add catalog cli help documentation (#1793)

* Fix code block formatting in user guide

Corrected code block syntax for CLI commands in user guide.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 06:54:07 -05:00
LifeIsAnAbstraction
56095f06d2 fix: use quiet checkout to avoid exception on git checkout (#1792) 2026-03-10 15:41:27 -05:00
Ben Lawson
2632a0f52d feat(extensions): support .extensionignore to exclude files during install (#1781)
* feat(extensions): support .extensionignore to exclude files during install

Add .extensionignore support so extension authors can exclude files and
folders from being copied when users run 'specify extension add'.

The file uses glob-style patterns (one per line), supports comments (#),
blank lines, trailing-slash directory patterns, and relative path matching.
The .extensionignore file itself is always excluded from the copy.

- Add _load_extensionignore() to ExtensionManager
- Integrate ignore function into shutil.copytree in install_from_directory
- Document .extensionignore in EXTENSION-DEVELOPMENT-GUIDE.md
- Add 6 tests covering all pattern matching scenarios
- Bump version to 0.1.14

* fix(extensions): use pathspec for gitignore-compatible .extensionignore matching

Replace fnmatch with pathspec.GitIgnoreSpec to get proper .gitignore
semantics where * does not cross directory boundaries. This addresses
review feedback on #1781.

Changes:
- Switch from fnmatch to pathspec>=0.12.0 (GitIgnoreSpec.from_lines)
- Normalize backslashes in patterns for cross-platform compatibility
- Distinguish directories from files for trailing-slash patterns
- Update docs to accurately describe supported pattern semantics
- Add edge-case tests: .., absolute paths, empty file, backslashes,
  * vs ** boundary behavior, and ! negation
- Move changelog entry to [Unreleased] section
2026-03-10 12:02:04 -05:00
Adrián Luján Muñoz
4ab91fbadf feat: add Codex support for extension command registration (#1767)
* feat: add Codex support for extension command registration

* test: add codex command registrar mapping check

* test: add codex consistency check to test_agent_config_consistency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:50:42 -05:00
Manfred Riem
5c0bedb410 chore: bump version to 0.2.0 (#1786)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-09 14:54:08 -05:00
Pavel-tabnine
d92798d5b0 fix: sync agent list comments with actual supported agents (#1785)
Several comment and documentation strings were not updated when
Mistral Vibe support was added, leaving them out of sync with the
code. This fixes:
- update-agent-context.sh: add Generic to Supports header comment
- update-agent-context.ps1: add generic to Multi-Agent header comment
- README.md: add Mistral Vibe to CLI tool-check text
2026-03-09 14:34:54 -05:00
Copilot
ee922cbde9 feat(extensions): support multiple active catalogs simultaneously (#1720)
* Initial plan

* feat(extensions): implement multi-catalog stack support

- Add CatalogEntry dataclass to represent catalog entries
- Add get_active_catalogs() reading SPECKIT_CATALOG_URL, project config,
  user config, or built-in default stack (org-approved + community)
- Add _load_catalog_config() to parse .specify/extension-catalogs.yml
- Add _validate_catalog_url() HTTPS validation helper
- Add _fetch_single_catalog() with per-URL caching, backward-compat for DEFAULT_CATALOG_URL
- Add _get_merged_extensions() that merges all catalogs (priority wins on conflict)
- Update search() and get_extension_info() to use merged results
  annotated with _catalog_name and _install_allowed
- Update clear_cache() to also remove per-URL hash cache files
- Add extension_catalogs CLI command to list active catalogs
- Add catalog add/remove sub-commands for .specify/extension-catalogs.yml
- Update extension_add to enforce install_allowed=false policy
- Update extension_search to show source catalog per result
- Update extension_info to show source catalog with install_allowed status
- Add 13 new tests covering catalog stack, merge conflict resolution,
  install_allowed enforcement, and catalog metadata

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* docs: update RFC, user guide, and API reference for multi-catalog support

- RFC: replace FUTURE FEATURE section with full implementation docs,
  add catalog stack resolution order, config file examples, merge
  conflict resolution, and install_allowed behavior
- EXTENSION-USER-GUIDE.md: add multi-catalog section with CLI examples
  for catalogs/catalog-add/catalog-remove, update catalog config docs
- EXTENSION-API-REFERENCE.md: add CatalogEntry class docs, update
  ExtensionCatalog docs with new methods and result annotations,
  add catalog CLI commands (catalogs, catalog add, catalog remove)

Also fix extension_catalogs command to correctly show "Using built-in
default catalog stack" when config file exists but has empty catalogs

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* Potential fix for pull request finding 'Empty except'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: remove extraneous f-string prefixes (ruff F541)

Remove f-prefix from strings with no placeholders in catalog_remove
and extension_search commands.

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* fix: address PR review feedback for multi-catalog support

- Rename 'org-approved' catalog to 'default'
- Move 'catalogs' command to 'catalog list' for consistency
- Add 'description' field to CatalogEntry dataclass
- Add --description option to 'catalog add' CLI command
- Align install_allowed default to False in _load_catalog_config
- Add user-level config detection in catalog list footer
- Fix _load_catalog_config docstring (document ValidationError)
- Fix test isolation for test_search_by_tag, test_search_by_query,
  test_search_verified_only, test_get_extension_info
- Update version to 0.1.14 and CHANGELOG
- Update all docs (RFC, User Guide, API Reference)

* fix: wrap _load_catalog_config() calls in catalog_list with try/except

- Check SPECKIT_CATALOG_URL first (matching get_active_catalogs() resolution order)
- Wrap both _load_catalog_config() calls in try/except ValidationError so a
  malformed config file cannot crash `specify extension catalog list` after
  the active catalogs have already been printed successfully

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-03-09 14:30:27 -05:00
Pavel-tabnine
1df24f1953 Pavel/add tabnine cli support (#1503)
* feat: add Tabnine CLI agent support

Tabnine CLI is a Gemini fork that uses TOML commands with the
.tabnine/agent/ directory structure and TABNINE.md context files.

Changes:
- Add 'tabnine' to AGENT_CONFIG in __init__.py
- Update release scripts (bash + PowerShell) for TOML command generation
- Update agent context scripts (bash + PowerShell)
- Add to GitHub release packages
- Update README.md and AGENTS.md documentation
- Bump version to 0.1.14
- Add 8 new tests for cross-file consistency

* fix: add missing generic to agent-context script usage string
2026-03-09 14:04:02 -05:00
LADISLAV BIHARI
3033834d64 Add Understanding extension to community catalog (#1778)
* Add Understanding extension to community catalog

31 deterministic requirements quality metrics based on IEEE/ISO standards.
Catches ambiguity, missing testability, and structural issues before implementation.
Includes experimental energy-based ambiguity detection.

Repository: https://github.com/Testimonial/understanding
Commands: scan, validate, energy
Hook: after_tasks validation prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Sort README table and catalog entries alphabetically

Move Understanding extension entry between Spec Sync and V-Model
to maintain alphabetical ordering in both files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Ladislav Bihari <ladislav.bihari@statsperform.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 10:18:41 -05:00
Ben Lawson
4b00078907 Add ralph extension to community catalog (#1780)
- Extension ID: ralph
- Version: 1.0.0
- Author: Rubiss
- Description: Autonomous implementation loop using AI agent CLI
2026-03-09 10:01:34 -05:00
Lautaro Lubatti
2d72f85790 Update README with project initialization instructions (#1772)
* Update README with project initialization instructions

Added instructions for creating a new project and initializing in an existing project.

* Update README.md with alternative one-time usage command for existing projects

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

* Added --ai option to prevent interactive AI selection

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 09:56:49 -05:00
Ismael
855ac838b8 feat: add review extension to community catalog (#1775)
Add spec-kit-review to catalog.community.json
and the Available Community Extensions table in extensions/README.md.

Co-authored-by: Ismael <ismael.jimenez-martinez@bmw.de>
2026-03-09 09:10:59 -05:00
Sharath Satish
a8ec87e3c2 Add fleet extension to community catalog (#1771)
- Extension ID: fleet
- Version: 1.0.0
- Author: sharathsatish
- Description: Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases
2026-03-09 08:13:36 -05:00
Gaël
9d6c05ad5b Integration of Mistral vibe support into speckit (#1725)
* Add Mistral Vibe support to Spec Kit

This commit adds comprehensive support for Mistral Vibe as an AI agent in the
Spec Kit project. The integration includes:

- Added Mistral Vibe to AGENT_CONFIG with proper CLI tool configuration
- Updated README.md with Mistral Vibe in supported agents table and examples
- Modified release package scripts to generate Mistral Vibe templates
- Updated both bash and PowerShell agent context update scripts
- Added appropriate CLI help text and documentation

Mistral Vibe is now fully supported with the same level of integration as
other CLI-based agents like Claude Code, Gemini CLI, etc.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* Add Mistral Vibe support to Spec Kit

- Added Mistral Vibe (vibe) to AGENT_CONFIG with proper TOML format support
- Updated CLI help text to include vibe as a valid AI assistant option
- Added Mistral Vibe to release scripts with correct .vibe/agents/ directory structure
- Updated agent context scripts (bash and PowerShell) with proper TOML file paths
- Added Mistral Vibe to README.md supported agents table with v2.0 slash command notes
- Used correct argument syntax {{args}} for Mistral Vibe TOML configurations

Mistral Vibe is now fully integrated with the same level of support as other
CLI-based agents like Gemini and Qwen. Users can now use specify init --ai vibe
to create projects with Mistral Vibe support.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

* Add Vibe templates to GitHub release script

creation of Mistral vibe zip

* Add 'vibe' agent to release package script

* Add 'vibe' to the list of agents in create-release-packages.sh

* chore: bump version to v1.0.1 [skip ci]

* Add generic spec kit templates to release script

* chore: bump version to v1.0.2 [skip ci]

* Update project version to 0.1.5

* Add generic spec kit templates to release script

* Add 'generic' and 'qodercli' to agent list to be aligned

* Update supported agents in update-agent-context.sh to be aligned

* Update README with new AI assistant options to be aligned

* Document --ai-commands-dir option in README to be aligned

Added new option for AI commands directory in README.

* Fix formatting in README.md for init arguments to be aligned

* Update README with AI assistant options to be aligned

Added AI options to specify init arguments in README.

* Fix formatting in README.md for project-name argument

* Update expected agent types in update-agent-context.sh to be aligned

* Update agent types and usage in update-agent-context.ps1 to be aligned

* Add support for generic AI assistant configuration to be aligned

* Fix formatting in __init__.py clean space

* Update AI assistant options in init command to be aligned

* Add 'qodercli' to expected agent types to be aligned

* Remove 'vibe' case from release package script

Removed the 'vibe' case from the create-release-packages script.

* Update README.md

ok for this

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

* Update .github/workflows/scripts/create-release-packages.ps1

ok to commit

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

* Add commands_subdir key to Mistral Vibe configuration

* Rename specify-agents.toml to specify-agents.md

* Update scripts/bash/update-agent-context.sh

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

* Update .github/workflows/scripts/create-release-packages.sh

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

* Update src/specify_cli/__init__.py

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

* Update src/specify_cli/__init__.py

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

* Fix duplicate 'commands_subdir' in vibe configuration

Removed duplicate 'commands_subdir' entries for 'vibe'.

* Add support for 'vibe' command in release script

add an mkdir and generate command

* Change commands_subdir from 'commands' to 'prompts'

* Update README.md

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

* Update update-agent-context.ps1

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

* Update create-release-packages.sh

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

* Update create-release-packages.ps1

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

* Update agent list in update-agent-context.sh

Kiro

---------

Co-authored-by: Lénaïc Huard <lenaic@lhuard.fr>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 08:05:13 -05:00
Ryo Hasegawa
3ef12cae3e fix: Remove duplicate options in specify.md (#1765) 2026-03-09 07:38:10 -05:00
Fabián Silva
8618d0a53e fix: use global branch numbering instead of per-short-name detection (#1757)
* fix: remove per-short-name number detection from specify prompt

The specify.md prompt instructed the AI to search for existing branches
filtered by the exact short-name, causing every new feature to start at
001 since no branches matched the new short-name. The underlying
create-new-feature.sh script already has correct global numbering logic
via check_existing_branches() that searches ALL branches and spec
directories.

The fix removes the AI's flawed number-detection steps and tells it to
NOT pass --number, letting the script auto-detect the next globally
available number.

Closes #1744
Closes #1468

🤖 Generated with [Claude Code](https://claude.com/code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify --json flag requirement per Copilot review

- Rephrased step 2 to mention both --short-name and --json flags
- Added explicit note to always include the JSON flag for reliable output parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:37:46 -05:00
Copilot
71e6b4da4a Add Community Walkthroughs section to README (#1766)
* Initial plan

* Add Community Walkthroughs section to README.md

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
2026-03-05 12:55:29 -06:00
Michal Bachorik
ad74334a85 feat(extensions): add Jira Integration to community catalog (#1764)
* feat(extensions): add Jira Integration to community catalog

Adds the spec-kit-jira extension to the community catalog.

## Extension Details
- **Name**: Jira Integration
- **Version**: 2.1.0
- **Repository**: https://github.com/mbachorik/spec-kit-jira

## Features
- Create Jira Epics, Stories, and Issues from spec-kit artifacts
- 3-level hierarchy (Epic → Stories → Tasks) or 2-level mode
- Configurable custom field support
- Status synchronization between local tasks and Jira

## Commands
- `/speckit.jira.specstoissues` - Create Jira hierarchy from spec and tasks
- `/speckit.jira.discover-fields` - Discover Jira custom fields
- `/speckit.jira.sync-status` - Sync task completion to Jira

---
This PR was prepared with the assistance of Claude (Anthropic).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review comments

- Set created_at to catalog submission date (2026-03-05)
- Add Jira Integration to Available Community Extensions table in README

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: iamaeroplane <michal.bachorik@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 07:21:38 -06:00
Pragya Chaurasia
8c3982d65b Add Azure DevOps Integration extension to community catalog (#1734)
* Add Azure DevOps work item synchronization with handoffs system

* Resolving the comments

* Added details of Azure DevOps extention

* t status
t revert --abort
Revert "Add Azure DevOps work item synchronization with handoffs system"

This reverts commit 39ac7e48d6.

* Update extensions/README.md

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

* Update extensions/catalog.community.json

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

---------

Co-authored-by: pragya247 <pragya@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 07:11:51 -06:00
layla
13dec1de05 Fix docs: update Antigravity link and add initialization example (#1748)
* Fix docs: update Antigravity link and add initialization example

* Update README.md

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

---------

Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 08:29:52 -06:00
Manfred Riem
d0a112c60f fix: wire after_tasks and after_implement hook events into command templates (#1702)
* fix: wire after_tasks and after_implement hook events into command templates (#1701)

The HookExecutor backend in extensions.py was fully implemented but
check_hooks_for_event() was never called by anything — the core command
templates had no instructions to check .specify/extensions.yml.

Add a final step to templates/commands/tasks.md (step 6, after_tasks) and
templates/commands/implement.md (step 10, after_implement) that instructs
the AI agent to:

- Read .specify/extensions.yml if it exists
- Filter hooks.{event} to enabled: true entries
- Evaluate any condition fields and skip non-matching hooks
- Output the RFC-specified hook message format, including
  EXECUTE_COMMAND: markers for mandatory (optional: false) hooks

Bumps version to 0.1.7.

* fix: clarify hook condition handling and add YAML error guidance in templates

- Replace ambiguous "evaluate any condition value" instruction with explicit
  guidance to skip hooks with non-empty conditions, deferring evaluation to
  HookExecutor
- Add instruction to skip hook checking silently if extensions.yml cannot
  be parsed or is invalid

* Fix/extension hooks not triggered (#1)

* feat(templates): implement before-hooks check as pre-execution phase

* test(hooks): create scenario for LLMs/Agents on hooks

---------

Co-authored-by: Dhilip <s.dhilipkumar@gmail.com>
2026-03-04 08:16:31 -06:00
Dhilip
c84756b7f3 make c ignores consistent with c++ (#1747) 2026-03-04 08:08:04 -06:00
Manfred Riem
524affca79 chore: bump version to 0.1.13 (#1746)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-03 16:08:40 -06:00
Medhat Galal
32c6e7f40c feat: add kiro-cli and AGENT_CONFIG consistency coverage (#1690)
* feat: add kiro-cli and AGENT_CONFIG consistency coverage

* fix: address PR feedback for kiro-cli migration

* test: assert init invocation result in --here mode test

* test: capture init result in here-mode command test

* chore: save local unapproved work in progress

* fix: resolve remaining PR1690 review threads

* Update src/specify_cli/__init__.py

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

* test: reduce brittleness in ai help alias assertion

* fix: resolve PR1690 ruff syntax regression

---------

Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-03 12:04:46 -06:00
Ismael
9cf33e81cc feat: add verify extension to community catalog (#1726)
* feat: add verify extension to community catalog

* Add verify to extension readme

---------

Co-authored-by: Ismael <ismael.jimenez-martinez@bmw.de>
2026-03-03 11:21:06 -06:00
Emi
254e9bbdf0 Add Retrospective Extension to community catalog README table (#1741)
* Add retrospective extension to community catalog.

Register the new retrospective extension release so users can discover and install it from the community catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add Retrospective Extension to community catalog

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-03 09:59:53 -06:00
Zaheer Uddin
6757c90dbd fix(scripts): add empty description validation and branch checkout error handling (#1559)
* fix(scripts): add empty description validation and branch checkout error handling

Adds two critical improvements to both PowerShell and Bash feature creation scripts:

1. Post-trim validation: Prevents creating features with whitespace-only descriptions
2. Branch checkout error handling: Provides clear error messages when branch
   creation fails (e.g., branch already exists) instead of silently continuing

Co-authored-by: Augment Agent <noreply@augmentcode.com>

* fix(scripts): use consistent stderr redirection for branch checkout

---------

Co-authored-by: Augment Agent <noreply@augmentcode.com>
2026-03-03 09:42:46 -06:00
Ismael
f6264d4ef4 fix: correct Copilot extension command registration (#1724)
* fix: correct Copilot extension command registration (#copilot)

- Use .agent.md extension for commands in .github/agents/
- Generate companion .prompt.md files in .github/prompts/
- Clean up .prompt.md files on extension removal
- Add tests for Copilot-specific registration behavior

Bumps version to 0.1.7.

* fix: test copilot prompt cleanup via ExtensionManager.remove() instead of manual unlink

---------

Co-authored-by: Ismael <ismael.jimenez-martinez@bmw.de>
2026-03-03 09:03:38 -06:00
Zaheer Uddin
dd8dbf6344 fix(implement): remove Makefile from C ignore patterns (#1558)
* fix(implement): remove Makefile from C ignore patterns

Makefile is typically source-controlled and should not be in .gitignore.
Replaced with proper autotools-generated files (autom4te.cache/, config.status).

Co-authored-by: Augment Agent <noreply@augmentcode.com>

* fix(implement): restore config.log in C ignore patterns

---------

Co-authored-by: Augment Agent <noreply@augmentcode.com>
2026-03-03 08:28:40 -06:00
Barry Gervin
bf8fb125ad Add sync extension to community catalog (#1728)
* Add sync extension to community catalog

- Extension ID: sync
- Version: 0.1.0
- Author: bgervin
- Description: Detect and resolve drift between specs and implementation

* fix: use main branch in URLs per Copilot review

* Reorder community extensions table alphabetically

Per Copilot review feedback and EXTENSION-PUBLISHING-GUIDE.md

---------

Co-authored-by: Barry Gervin <bgervin@hotmail.com>
2026-03-03 08:18:18 -06:00
Zaheer Uddin
2b92ab444d fix(checklist): clarify file handling behavior for append vs create (#1556)
* fix(checklist): clarify file handling behavior for append vs create

Resolves contradictory instructions in checklist.md lines 97-99 where the
template stated both 'append to existing file' and 'creates a NEW file'.

Changes:
- Clarified that if file doesn't exist, create new with CHK001
- If file exists, append new items continuing from last CHK ID
- Emphasized preservation of existing content (never delete/replace)

Co-authored-by: Augment Agent <noreply@augmentcode.com>

* fix(checklist): align report language with append behavior

---------

Co-authored-by: Augment Agent <noreply@augmentcode.com>
2026-03-03 07:52:02 -06:00
Zaheer Uddin
abe1b7085c fix(clarify): correct conflicting question limit from 10 to 5 (#1557)
Resolves inconsistency where line 92 stated 'Maximum of 10 total questions'
while all other references (lines 2, 91, 100, 134, 158, 178) consistently
specify a maximum of 5 questions.

Co-authored-by: Augment Agent <noreply@augmentcode.com>
2026-03-03 07:46:31 -06:00
Manfred Riem
bfaca2c449 chore: bump version to 0.1.12 (#1737)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-02 13:35:43 -06:00
Manfred Riem
78ed453e38 fix: use RELEASE_PAT so tag push triggers release workflow (#1736) 2026-03-02 13:31:18 -06:00
Manfred Riem
658ab2a38c fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
* fix: use release branch + PR instead of direct push to main

Bypass branch protection rules by pushing version bump to a
chore/release-vX.Y.Z branch, tagging that commit, then opening
an auto PR to merge back into main. The release workflow still
triggers immediately from the tag push.

* fix: remove --label automated from gh pr create (label does not exist)
2026-03-02 13:16:13 -06:00
Manfred Riem
2c41d3627e fix: Split release process to sync pyproject.toml version with git tags (#1732)
* fix: split release process to sync pyproject.toml version with git tags (#1721)

- Split release workflow into two: release-trigger.yml and release.yml
- release-trigger.yml: Updates pyproject.toml, generates changelog from commits, creates tag
- release.yml: Triggered by tag push, builds artifacts, creates GitHub release
- Ensures git tags point to commits with correct version in pyproject.toml
- Auto-generates changelog from commit messages since last tag
- Supports manual version input or auto-increment patch version
- Added simulate-release.sh for local testing without pushing
- Added comprehensive RELEASE-PROCESS.md documentation
- Updated pyproject.toml to v0.1.10 to sync with latest release

This fixes the version mismatch issue where tags pointed to commits with
outdated pyproject.toml versions, preventing confusion when installing from source.

* Update .github/workflows/RELEASE-PROCESS.md

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

* Update .github/workflows/scripts/simulate-release.sh

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

* Update .github/workflows/release.yml

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

* Update .github/workflows/release-trigger.yml

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

* fix: harden release-trigger against shell injection and fix stale docs

- Pass workflow_dispatch version input via env: instead of direct
  interpolation into shell script, preventing potential injection attacks
- Validate version input against strict semver regex before use
- Fix RELEASE-PROCESS.md Option 2 still referencing [Unreleased] section
  handling that no longer exists in the workflow

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 12:52:13 -06:00
Fabián Silva
b55d00beed fix: prepend YAML frontmatter to Cursor .mdc files (#1699)
* fix: prepend YAML frontmatter to Cursor .mdc files for auto-inclusion

Cursor IDE requires YAML frontmatter with `alwaysApply: true` in .mdc
rule files for them to be automatically loaded. Without this frontmatter,
users must manually configure glob patterns for the rules to take effect.

This fix adds frontmatter generation to both the bash and PowerShell
update-agent-context scripts, handling three scenarios:
- New .mdc file creation (frontmatter prepended after template processing)
- Existing .mdc file update without frontmatter (frontmatter added)
- Existing .mdc file with frontmatter (no duplication)

Closes #669

🤖 Generated with [Claude Code](https://claude.com/code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: address Copilot review suggestions

- Handle CRLF line endings in frontmatter detection (grep '^---' instead
  of '^---$')
- Fix double blank line after frontmatter in PowerShell New-AgentFile
- Remove unused tempfile import from tests
- Add encoding="utf-8" to all open() calls for cross-platform safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 08:20:00 -06:00
dependabot[bot]
525eae7f7e chore(deps): bump astral-sh/setup-uv from 6 to 7 (#1709)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6 to 7.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 08:09:43 -06:00
dependabot[bot]
ce7bed4823 chore(deps): bump actions/setup-python from 5 to 6 (#1710)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 08:07:19 -06:00
Manfred Riem
61b0637a6d chore: Update outdated GitHub Actions versions (#1706)
Co-authored-by: Padraic Slattery <pgoslatara@gmail.com>
2026-02-26 14:43:22 -06:00
Manfred Riem
56deda7be3 docs: Document dual-catalog system for extensions (#1689)
* docs: Document dual-catalog system for extensions

- Clarify distinction between catalog.json (curated) and catalog.community.json (reference)
- Update EXTENSION-DEVELOPMENT-GUIDE.md to explain community catalog submission
- Update EXTENSION-PUBLISHING-GUIDE.md with dual-catalog workflow
- Update EXTENSION-USER-GUIDE.md with catalog selection guidance
- Expand README.md with comprehensive catalog explanation
- Update RFC-EXTENSION-SYSTEM.md with dual-catalog design and current implementation
- Change GitHub references from statsperform to github
- Add SPECKIT_CATALOG_URL environment variable documentation

This clarifies how organizations can curate their own catalog while
browsing community-contributed extensions for discovery.

* Update extensions/README.md

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

* Update extensions/README.md

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

* Update extensions/README.md

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 14:38:56 -06:00
Santosh Bhavani
525cdc17ec Fix version command in documentation (#1685)
Co-authored-by: Santosh Bhavani <sb@mac-mini-i7.local>
2026-02-25 08:20:18 -06:00
dsrednicki
607760e72f Add Cleanup Extension to README (#1678) 2026-02-24 15:50:04 -06:00
Emi
c7ecdfb998 Add retrospective extension to community catalog. (#1681)
Register the new retrospective extension release so users can discover and install it from the community catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 13:56:03 -06:00
dsrednicki
f444ccba3a Add Cleanup Extension to catalog (#1617)
**Repository**: https://github.com/dsrednicki/spec-kit-cleanup
**Version**: 1.0.0
**License**: MIT
**Author**: @dsrednicki

Adds catalog entry for the Cleanup Extension - a post-implementation
quality gate that fixes small issues (scout rule), creates tasks for
medium issues, and generates analysis for large issues.
2026-02-23 13:52:40 -06:00
Manfred Riem
3040d33c31 Fix parameter ordering issues in CLI (#1669)
* chore: bump version to v0.0.6 [skip ci]

* Fix parameter ordering issues in CLI (#1641)

- Add validation to detect when option flags are consumed as values
- Provide clear error messages with helpful hints and examples
- Add 5 comprehensive tests to prevent regressions
- Update CODEOWNERS to @mnriem
- Bump version to 0.1.6 with changelog entry

Fixes: #1641

* Fix ruff linting errors: remove f-string prefix from strings without placeholders

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-23 13:17:08 -06:00
Leonardo Nascimento
6cc61025cb Update V-Model Extension Pack to v0.4.0 (#1665)
- Version: 0.2.0 → 0.4.0
- Commands: 5 → 9 (new: architecture-design, integration-test, module-design, unit-test)
- Download URL: updated to v0.4.0 tag
2026-02-23 13:14:02 -06:00
Emi
c1034f1d9d docs: Fix doc missing step (#1496)
* docs: Fix doc missing step

* docs: Update steps for generating technical plan and defining tasks
2026-02-23 11:40:41 -06:00
Leonardo Nascimento
cee4f26fac Update V-Model Extension Pack to v0.3.0 (#1661)
- Version: 0.2.0 → 0.3.0
- Commands: 5 → 7 (architecture-design, integration-test)
- Download URL updated to v0.3.0 tag
2026-02-23 09:02:24 -06:00
Manfred Riem
6f523ede22 Fix #1658: Add commands_subdir field to support non-standard agent directory structures (#1660)
- Added commands_subdir field to AGENT_CONFIG for all agents
- Updated install_ai_skills() to use commands_subdir instead of hardcoded 'commands'
- Fixed --ai-skills flag for copilot, opencode, windsurf, codex, kilocode, q, and agy
- Bumped version to 0.1.5
- Updated AGENTS.md documentation with new field

Affected agents now correctly locate their command templates:
- copilot: .github/agents/
- opencode: .opencode/command/ (singular)
- windsurf: .windsurf/workflows/
- codex: .codex/prompts/
- kilocode: .kilocode/workflows/
- q: .amazonq/prompts/
- agy: .agent/workflows/

All 51 tests pass.
2026-02-21 08:05:58 -06:00
Manfred Riem
68d1d3a0fc feat: add GitHub issue templates (#1655)
* feat: add issue templates for agent requests, bug reports, feature requests, extension submissions, and configuration

* Update .github/ISSUE_TEMPLATE/config.yml

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

* Update .github/ISSUE_TEMPLATE/agent_request.yml

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

* Update .github/ISSUE_TEMPLATE/config.yml

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

* Update .github/ISSUE_TEMPLATE/bug_report.yml

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

* Update .github/ISSUE_TEMPLATE/feature_request.yml

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 17:35:48 -06:00
Leonardo Nascimento
07077d0fc2 Update V-Model Extension Pack to v0.2.0 in community catalog (#1656)
- Version: 0.1.0 → 0.2.0
- Download URL updated to v0.2.0 tag
- Commands: 3 → 5 (added system-design, system-test)
2026-02-20 17:29:58 -06:00
Leonardo Nascimento
aeed11f735 Add V-Model Extension Pack to catalog (#1640)
* Add V-Model Extension Pack to catalog

Second community extension: V-Model paired dev-spec and test-spec
generation with regulatory-grade traceability.

- 3 commands: requirements, acceptance, trace
- Deterministic coverage validation (regex-based, not AI)
- Targets: IEC 62304, ISO 26262, DO-178C, FDA 21 CFR Part 820
- Repository: https://github.com/leocamello/spec-kit-v-model
- Release: v0.1.0

* Fix catalog entry: provides as number, add timestamps and statistics

Address review feedback:
- provides.commands: array → number (3), add hooks (1)
- Add created_at and updated_at timestamps
- Add statistics block (downloads: 0, stars: 0)

* Address review: use catalog.community.json and add extensions README

Per maintainer feedback:
- Revert catalog.json to its original empty state (empty by design)
- Rename catalog.example.json → catalog.community.json
- Replace example entries with real V-Model Extension Pack entry
- Add extensions/README.md with community extensions table
2026-02-20 15:11:51 -06:00
Manfred Riem
12405c01e1 refactor: remove OpenAPI/GraphQL bias from templates (#1652)
* chore: bump version to v0.0.5 [skip ci]

* refactor: update documentation for interface contracts and integration patterns

* Update pyproject.toml

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

* Update templates/commands/tasks.md

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

* Update templates/commands/tasks.md

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 14:46:56 -06:00
Manfred Riem
fc3b98ea09 fix: rename Qoder AGENT_CONFIG key from 'qoder' to 'qodercli' to match actual CLI executable (#1651)
* fix: rename Qoder CLI to QoderCLI across scripts and documentation

* Apply suggestion from @Copilot

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

* Apply suggestion from @Copilot

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

* Update .github/workflows/scripts/create-release-packages.ps1

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 13:30:16 -06:00
Manfred Riem
6150f1e317 Add generic agent support with customizable command directories (#1639)
- Add --ai generic option for unsupported AI agents (bring your own agent)
- Require --ai-commands-dir to specify where agent reads commands from
- Generate Markdown commands with $ARGUMENTS format (compatible with most agents)
- Rebuild CHANGELOG from GitHub releases (last 10 releases)
- Align version to 0.1.3

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-20 11:05:38 -06:00
Manfred Riem
6fca5d83b2 fix: pin click>=8.1 to prevent Python 3.14/Homebrew env isolation crash (#1648)
* fix: pin click>=8.1 to prevent Python 3.14/Homebrew env isolation failures

Fixes #1631. When uv installs specify-cli on macOS with Homebrew Python
3.14, the virtual environment can fail to fully isolate from the system
site-packages, causing Homebrew's click to be loaded instead of the one
uv installed. If that system click is older than 8.1, it lacks the `ctx`
keyword argument in `ParamType.get_metavar()`, which typer 0.24.0 requires,
resulting in:

  TypeError: ParamType.get_metavar() got an unexpected keyword argument 'ctx'

Adding an explicit `click>=8.1` dependency gives uv a hard constraint so
the correct version is always resolved and installed.

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

* Update CHANGELOG.md

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 10:22:16 -06:00
Manfred Riem
465acd9024 fix: include 'src/**' path in release workflow triggers (#1646)
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
2026-02-20 09:46:29 -06:00
dependabot[bot]
04fc3fd1ba chore(deps): bump github/codeql-action from 3 to 4 (#1635)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 16:31:23 -06:00
Manfred Riem
24d76b5d92 Add pytest and Python linting (ruff) to CI (#1637)
* feat: add GitHub Actions workflow for testing and linting Python code

* fix: resolve ruff lint errors in specify_cli

- Remove extraneous f-string prefixes (F541)
- Split multi-statement lines (E701, E702)
- Remove unused variable assignments (F841)
- Remove ruff format check from CI workflow (format-only PR to follow)

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

* fix: strip ANSI codes in ai-skills help text test

The Rich/Typer CLI injects ANSI escape codes into option names in
--help output, causing plain string matching to fail.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-19 16:21:58 -06:00
Manfred Riem
0f7d04b12b feat: add pull request template for better contribution guidelines (#1634) 2026-02-19 15:49:34 -06:00
Manfred Riem
9402ebd00a Feat/ai skills (#1632)
* implement ai-skills command line switch

* fix: address review comments, remove breaking change for existing projects, add tests

* fix: review comments

* fix: review comments

* fix: review comments

* fix: review comments

* fix: review comments, add test cases for all the agents

* fix: review comments

* fix: review comments

* chore: trigger CI

* chore: trigger CodeQL

* ci: add CodeQL workflow for code scanning

* ci: add actions language to CodeQL workflow, disable default setup

---------

Co-authored-by: dhilipkumars <s.dhilipkumar@gmail.com>
2026-02-19 13:24:41 -06:00
dependabot[bot]
d410d188fc chore(deps): bump actions/stale from 9 to 10 (#1623)
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9...v10)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '10'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 07:41:50 -06:00
Manfred Riem
686c91f94e feat: add dependabot configuration for pip and GitHub Actions updates (#1622) 2026-02-18 17:42:41 -06:00
Manfred Riem
22036732d8 Remove Maintainers section from README.md (#1618) 2026-02-18 16:11:16 -06:00
Laurent Thiebault
c78f8423f6 fix: typo in plan-template.md (#1446) 2026-02-17 07:22:43 -06:00
Hanzhi Yang
76cca34293 Feat: add a new agent: Google Anti Gravity (#1220)
* Add support for Antigravity (agy) agent

* fix a few things after gemini code update

* Fix missed merge conflicts

* As PR states it is IDE integration setting requires_cli to 'False'

---------

Co-authored-by: Manfred Riem <manfred.riem@microsoft.com>
2026-02-12 12:31:48 -06:00
Copilot
9a1e3037b0 Add stale workflow for 180-day inactive issues and PRs (#1594)
* Initial plan

* Add stale issues and PRs workflow

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* Add 7-day grace period before closing stale items

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* Update stale timing: mark at 150 days, close at 180 days

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
2026-02-11 16:03:17 -06:00
Michal Bachorik
f14a47ea7d Add modular extension system (#1551)
* Add modular extension system for Spec Kit

Implement a complete extension system that allows third-party developers
to extend Spec Kit functionality through plugins.

## Core Features
- Extension discovery and loading from local and global directories
- YAML-based extension manifest (extension.yml) with metadata and capabilities
- Command extensions: custom slash commands with markdown templates
- Hook system: pre/post hooks for generate, task, and sync operations
- Extension catalog for discovering and installing community extensions
- SPECKIT_CATALOG_URL environment variable for catalog URL override

## Installation Methods
- Catalog install: `specify extension add <name>`
- URL install: `specify extension add <name> --from <url>`
- Dev install: `specify extension add --dev <path>`

## Implementation
- ExtensionManager class for lifecycle management (load, enable, disable)
- Support for extension dependencies and version constraints
- Configuration layering (global → project → extension)
- Hook conditions for conditional execution

## Documentation
- RFC with design rationale and architecture decisions
- API reference for extension developers
- Development guide with examples
- User guide for installing and managing extensions
- Publishing guide for the extension catalog

## Included
- Extension template for bootstrapping new extensions
- Comprehensive test suite
- Example catalog.json structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update Jira extension to v2.1.0 in catalog

Adds 2-level mode support (Epic → Stories only).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Address PR review feedback

- Fix Zip Slip vulnerability in ZIP extraction with path validation
- Fix keep_config option to actually preserve config files on removal
- Add URL validation for SPECKIT_CATALOG_URL (HTTPS required, localhost exception)
- Add security warning when installing from custom URLs (--from flag)
- Empty catalog.json so organizations can ship their own catalogs
- Fix markdown linter errors (MD040: add language to code blocks)
- Remove redundant import and fix unused variables in tests
- Add comment explaining empty except clause for backwards compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add comprehensive organization catalog customization docs

- Explain why default catalog is empty (org control)
- Document how to create and host custom catalogs
- Add catalog JSON schema reference
- Include use cases: private extensions, curated catalogs, air-gapped environments
- Add examples for combining catalog with direct installation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix test assertions for extension system data structures

- Update test_config_backup_on_remove to use new subdirectory structure
  (.backup/test-ext/file.yml instead of .backup/test-ext-file.yml)
- Update test_full_install_and_remove_workflow to handle registered_commands
  being a dict keyed by agent name instead of a flat list

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Address Copilot review feedback

- Fix localhost URL check to use parsed.hostname instead of netloc.startswith()
  This correctly handles URLs with ports like localhost:8080
- Fix YAML indentation error in config-template.yml (line 57)
- Fix double space typo in example.md (line 172)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add catalog.example.json as reference for organizations

The main catalog.json is intentionally empty so organizations can ship
their own curated catalogs. This example file shows the expected schema
and structure for creating organization-specific catalogs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Address remaining Copilot security and logic review feedback

- Fix Zip Slip vulnerability by using relative_to() for safe path validation
- Add HTTPS validation for extension download URLs
- Backup both *-config.yml and *-config.local.yml files on remove
- Normalize boolean values to lowercase for hook condition comparisons
- Show non-default catalog warning only once per instance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Ignoring linter for extensions directory

---------

Co-authored-by: iamaeroplane <michal.bachorik@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Manfred Riem <manfred.riem@microsoft.com>
2026-02-10 14:27:20 -06:00
Manfred Riem
36d97235ad Fixes #1586 - .specify.specify path error (#1588) 2026-02-10 08:20:39 -06:00
jin-joung
4afbd87abb fix: preserve constitution.md during reinitialization (#1541) (#1553)
Moves constitution template from memory/ to templates/ to prevent
overwrites when spec-kit is reinitialized with a different AI agent.

Changes:
- Move memory/constitution.md to templates/constitution-template.md
- Update CLI to copy template to memory/ only on first initialization
- Update constitution command to reference correct paths with .specify/ prefix
- Preserve existing constitution.md when reinitializing project

The CLI now checks if .specify/memory/constitution.md exists:
- If it exists: preserve it (no overwrite)
- If it doesn't exist: copy from .specify/templates/constitution-template.md

This allows users to customize their constitution without losing changes
when adding support for additional AI agents or reinitializing.

Fixes #1541

Co-authored-by: jjoung1128 <jinwoong.joung@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 09:01:55 -06:00
amuzip
b562438df9 fix: resolve markdownlint errors across documentation (#1571)
- Add language specifiers to fenced code blocks (bash, text)
- Fix whitespace issues around code fences
- Disable MD036 (emphasis as heading) for intentional warning text
- Disable MD060 (table column style) for existing table formats

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 08:48:56 -06:00
101 changed files with 21801 additions and 582 deletions

View File

@@ -50,8 +50,6 @@
"kilocode.Kilo-Code",
// Roo Code
"RooVeterinaryInc.roo-cline",
// Amazon Developer Q
"AmazonWebServices.amazon-q-vscode",
// Claude Code
"anthropic.claude-code"
],

View File

@@ -8,15 +8,15 @@ run_command() {
local command_to_run="$*"
local output
local exit_code
# Capture all output (stdout and stderr)
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
exit_code=${exit_code:-0}
if [ $exit_code -ne 0 ]; then
echo -e "\033[0;31m[ERROR] Command failed (Exit Code $exit_code): $command_to_run\033[0m" >&2
echo -e "\033[0;31m$output\033[0m" >&2
exit $exit_code
fi
}
@@ -51,32 +51,38 @@ echo -e "\n🤖 Installing OpenCode CLI..."
run_command "npm install -g opencode-ai@latest"
echo "✅ Done"
echo -e "\n🤖 Installing Amazon Q CLI..."
# 👉🏾 https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-verify-download.html
echo -e "\n🤖 Installing Kiro CLI..."
# https://kiro.dev/docs/cli/
KIRO_INSTALLER_URL="https://kiro.dev/install.sh"
KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
KIRO_INSTALLER_PATH="$(mktemp)"
run_command "curl --proto '=https' --tlsv1.2 -sSf 'https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux.zip' -o 'q.zip'"
run_command "curl --proto '=https' --tlsv1.2 -sSf 'https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux.zip.sig' -o 'q.zip.sig'"
cat > amazonq-public-key.asc << 'EOF'
-----BEGIN PGP PUBLIC KEY BLOCK-----
cleanup_kiro_installer() {
rm -f "$KIRO_INSTALLER_PATH"
}
trap cleanup_kiro_installer EXIT
mDMEZig60RYJKwYBBAHaRw8BAQdAy/+G05U5/EOA72WlcD4WkYn5SInri8pc4Z6D
BKNNGOm0JEFtYXpvbiBRIENMSSBUZWFtIDxxLWNsaUBhbWF6b24uY29tPoiZBBMW
CgBBFiEEmvYEF+gnQskUPgPsUNx6jcJMVmcFAmYoOtECGwMFCQPCZwAFCwkIBwIC
IgIGFQoJCAsCBBYCAwECHgcCF4AACgkQUNx6jcJMVmef5QD/QWWEGG/cOnbDnp68
SJXuFkwiNwlH2rPw9ZRIQMnfAS0A/0V6ZsGB4kOylBfc7CNfzRFGtovdBBgHqA6P
zQ/PNscGuDgEZig60RIKKwYBBAGXVQEFAQEHQC4qleONMBCq3+wJwbZSr0vbuRba
D1xr4wUPn4Avn4AnAwEIB4h+BBgWCgAmFiEEmvYEF+gnQskUPgPsUNx6jcJMVmcF
AmYoOtECGwwFCQPCZwAACgkQUNx6jcJMVmchMgEA6l3RveCM0YHAGQaSFMkguoAo
vK6FgOkDawgP0NPIP2oA/jIAO4gsAntuQgMOsPunEdDeji2t+AhV02+DQIsXZpoB
=f8yY
-----END PGP PUBLIC KEY BLOCK-----
EOF
run_command "gpg --batch --import amazonq-public-key.asc"
run_command "gpg --verify q.zip.sig q.zip"
run_command "unzip -q q.zip"
run_command "chmod +x ./q/install.sh"
run_command "./q/install.sh --no-confirm"
run_command "rm -rf ./q q.zip q.zip.sig amazonq-public-key.asc"
run_command "curl -fsSL \"$KIRO_INSTALLER_URL\" -o \"$KIRO_INSTALLER_PATH\""
run_command "echo \"$KIRO_INSTALLER_SHA256 $KIRO_INSTALLER_PATH\" | sha256sum -c -"
run_command "bash \"$KIRO_INSTALLER_PATH\""
kiro_binary=""
if command -v kiro-cli >/dev/null 2>&1; then
kiro_binary="kiro-cli"
elif command -v kiro >/dev/null 2>&1; then
kiro_binary="kiro"
else
echo -e "\033[0;31m[ERROR] Kiro CLI installation did not create 'kiro-cli' or 'kiro' in PATH.\033[0m" >&2
exit 1
fi
run_command "$kiro_binary --help > /dev/null"
echo "✅ Done"
echo -e "\n🤖 Installing Kimi CLI..."
# https://code.kimi.com
run_command "pipx install kimi-cli"
echo "✅ Done"
echo -e "\n🤖 Installing CodeBuddy CLI..."

2
.github/CODEOWNERS vendored
View File

@@ -1,3 +1,3 @@
# Global code owner
* @localden
* @mnriem

141
.github/ISSUE_TEMPLATE/agent_request.yml vendored Normal file
View File

@@ -0,0 +1,141 @@
name: Agent Request
description: Request support for a new AI agent/assistant in Spec Kit
title: "[Agent]: Add support for "
labels: ["agent-request", "enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for requesting a new agent! Before submitting, please check if the agent is already supported.
**Currently supported agents**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Kiro CLI, Amp, SHAI, IBM Bob, Antigravity
- type: input
id: agent-name
attributes:
label: Agent Name
description: What is the name of the AI agent/assistant?
placeholder: "e.g., SuperCoder AI"
validations:
required: true
- type: input
id: website
attributes:
label: Official Website
description: Link to the agent's official website or documentation
placeholder: "https://..."
validations:
required: true
- type: dropdown
id: agent-type
attributes:
label: Agent Type
description: How is the agent accessed?
options:
- CLI tool (command-line interface)
- IDE extension/plugin
- Both CLI and IDE
- Other
validations:
required: true
- type: input
id: cli-command
attributes:
label: CLI Command (if applicable)
description: What command is used to invoke the agent from terminal?
placeholder: "e.g., supercode, ai-assistant"
- type: input
id: install-method
attributes:
label: Installation Method
description: How is the agent installed?
placeholder: "e.g., npm install -g supercode, pip install supercode, IDE marketplace"
validations:
required: true
- type: textarea
id: command-structure
attributes:
label: Command/Workflow Structure
description: How does the agent define custom commands or workflows?
placeholder: |
- Command file format (Markdown, YAML, TOML, etc.)
- Directory location (e.g., .supercode/commands/)
- Example command file structure
validations:
required: true
- type: textarea
id: argument-pattern
attributes:
label: Argument Passing Pattern
description: How does the agent handle arguments in commands?
placeholder: |
e.g., Uses {{args}}, $ARGUMENTS, %ARGS%, or other placeholder format
Example: "Run test suite with {{args}}"
- type: dropdown
id: popularity
attributes:
label: Popularity/Usage
description: How widely is this agent used?
options:
- Widely used (thousands+ of users)
- Growing adoption (hundreds of users)
- New/emerging (less than 100 users)
- Unknown
validations:
required: true
- type: textarea
id: documentation
attributes:
label: Documentation Links
description: Links to relevant documentation for custom commands/workflows
placeholder: |
- Command documentation: https://...
- API/CLI reference: https://...
- Examples: https://...
- type: textarea
id: use-case
attributes:
label: Use Case
description: Why do you want this agent supported in Spec Kit?
placeholder: Explain your workflow and how this agent fits into your development process
validations:
required: true
- type: textarea
id: example-command
attributes:
label: Example Command File
description: If possible, provide an example of a command file for this agent
render: markdown
placeholder: |
```toml
description = "Example command"
prompt = "Do something with {{args}}"
```
- type: checkboxes
id: contribution
attributes:
label: Contribution
description: Are you willing to help implement support for this agent?
options:
- label: I can help test the integration
- label: I can provide example command files
- label: I can help with documentation
- label: I can submit a pull request for the integration
- type: textarea
id: context
attributes:
label: Additional Context
description: Any other relevant information about this agent
placeholder: Screenshots, community links, comparison to existing agents, etc.

118
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
name: Bug Report
description: Report a bug or unexpected behavior in Specify CLI or Spec Kit
title: "[Bug]: "
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug! Please fill out the sections below to help us diagnose and fix the issue.
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is.
placeholder: What went wrong?
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Run command '...'
2. Execute script '...'
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: Describe the expected outcome
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened?
placeholder: Describe what happened instead
validations:
required: true
- type: input
id: version
attributes:
label: Specify CLI Version
description: "Run `specify version` or `pip show spec-kit`"
placeholder: "e.g., 1.3.0"
validations:
required: true
- type: dropdown
id: ai-agent
attributes:
label: AI Agent
description: Which AI agent are you using?
options:
- Claude Code
- Gemini CLI
- GitHub Copilot
- Cursor
- Qwen Code
- opencode
- Codex CLI
- Windsurf
- Kilo Code
- Auggie CLI
- Roo Code
- CodeBuddy
- Qoder CLI
- Kiro CLI
- Amp
- SHAI
- IBM Bob
- Antigravity
- Not applicable
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
description: Your operating system and version
placeholder: "e.g., macOS 14.2, Ubuntu 22.04, Windows 11"
validations:
required: true
- type: input
id: python
attributes:
label: Python Version
description: "Run `python --version` or `python3 --version`"
placeholder: "e.g., Python 3.11.5"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Error Logs
description: Please paste any relevant error messages or logs
render: shell
placeholder: Paste error output here
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context about the problem
placeholder: Screenshots, related issues, workarounds attempted, etc.

17
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: 💬 General Discussion
url: https://github.com/github/spec-kit/discussions
about: Ask questions, share ideas, or discuss Spec-Driven Development
- name: 📖 Documentation
url: https://github.com/github/spec-kit/blob/main/README.md
about: Read the Spec Kit documentation and guides
- name: 🛠️ Extension Development Guide
url: https://github.com/manfredseee/spec-kit/blob/main/extensions/EXTENSION-DEVELOPMENT-GUIDE.md
about: Learn how to develop and publish Spec Kit extensions
- name: 🤝 Contributing Guide
url: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md
about: Learn how to contribute to Spec Kit
- name: 🔒 Security Issues
url: https://github.com/github/spec-kit/blob/main/SECURITY.md
about: Report security vulnerabilities privately

View File

@@ -0,0 +1,280 @@
name: Extension Submission
description: Submit your extension to the Spec Kit catalog
title: "[Extension]: Add "
labels: ["extension-submission", "enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for contributing an extension! This template helps you submit your extension to the community catalog.
**Before submitting:**
- Review the [Extension Publishing Guide](https://github.com/github/spec-kit/blob/main/extensions/EXTENSION-PUBLISHING-GUIDE.md)
- Ensure your extension has a valid `extension.yml` manifest
- Create a GitHub release with a version tag (e.g., v1.0.0)
- Test installation: `specify extension add --from <your-release-url>`
- type: input
id: extension-id
attributes:
label: Extension ID
description: Unique extension identifier (lowercase with hyphens only)
placeholder: "e.g., jira-integration"
validations:
required: true
- type: input
id: extension-name
attributes:
label: Extension Name
description: Human-readable extension name
placeholder: "e.g., Jira Integration"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Semantic version number
placeholder: "e.g., 1.0.0"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Brief description of what your extension does (under 200 characters)
placeholder: Integrates Jira issue tracking with Spec Kit workflows for seamless task management
validations:
required: true
- type: input
id: author
attributes:
label: Author
description: Your name or organization
placeholder: "e.g., John Doe or Acme Corp"
validations:
required: true
- type: input
id: repository
attributes:
label: Repository URL
description: GitHub repository URL for your extension
placeholder: "https://github.com/your-org/spec-kit-your-extension"
validations:
required: true
- type: input
id: download-url
attributes:
label: Download URL
description: URL to the GitHub release archive (e.g., v1.0.0.zip)
placeholder: "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip"
validations:
required: true
- type: input
id: license
attributes:
label: License
description: Open source license type
placeholder: "e.g., MIT, Apache-2.0"
validations:
required: true
- type: input
id: homepage
attributes:
label: Homepage (optional)
description: Link to extension homepage or documentation site
placeholder: "https://..."
- type: input
id: documentation
attributes:
label: Documentation URL (optional)
description: Link to detailed documentation
placeholder: "https://github.com/your-org/spec-kit-your-extension/blob/main/docs/"
- type: input
id: changelog
attributes:
label: Changelog URL (optional)
description: Link to changelog file
placeholder: "https://github.com/your-org/spec-kit-your-extension/blob/main/CHANGELOG.md"
- type: input
id: speckit-version
attributes:
label: Required Spec Kit Version
description: Minimum Spec Kit version required
placeholder: "e.g., >=0.1.0"
validations:
required: true
- type: textarea
id: required-tools
attributes:
label: Required Tools (optional)
description: List any external tools or dependencies required
placeholder: |
- jira-cli (>=1.0.0) - required
- python (>=3.8) - optional
render: markdown
- type: input
id: commands-count
attributes:
label: Number of Commands
description: How many commands does your extension provide?
placeholder: "e.g., 3"
validations:
required: true
- type: input
id: hooks-count
attributes:
label: Number of Hooks (optional)
description: How many hooks does your extension provide?
placeholder: "e.g., 0"
- type: textarea
id: tags
attributes:
label: Tags
description: 2-5 relevant tags (lowercase, separated by commas)
placeholder: "issue-tracking, jira, atlassian, automation"
validations:
required: true
- type: textarea
id: features
attributes:
label: Key Features
description: List the main features and capabilities of your extension
placeholder: |
- Create Jira issues from specs
- Sync task status with Jira
- Link specs to existing issues
- Generate Jira reports
validations:
required: true
- type: checkboxes
id: testing
attributes:
label: Testing Checklist
description: Confirm that your extension has been tested
options:
- label: Extension installs successfully via download URL
required: true
- label: All commands execute without errors
required: true
- label: Documentation is complete and accurate
required: true
- label: No security vulnerabilities identified
required: true
- label: Tested on at least one real project
required: true
- type: checkboxes
id: requirements
attributes:
label: Submission Requirements
description: Verify your extension meets all requirements
options:
- label: Valid `extension.yml` manifest included
required: true
- label: README.md with installation and usage instructions
required: true
- label: LICENSE file included
required: true
- label: GitHub release created with version tag
required: true
- label: All command files exist and are properly formatted
required: true
- label: Extension ID follows naming conventions (lowercase-with-hyphens)
required: true
- type: textarea
id: testing-details
attributes:
label: Testing Details
description: Describe how you tested your extension
placeholder: |
**Tested on:**
- macOS 14.0 with Spec Kit v0.1.0
- Linux Ubuntu 22.04 with Spec Kit v0.1.0
**Test project:** [Link or description]
**Test scenarios:**
1. Installed extension
2. Configured settings
3. Ran all commands
4. Verified outputs
validations:
required: true
- type: textarea
id: example-usage
attributes:
label: Example Usage
description: Provide a simple example of using your extension
render: markdown
placeholder: |
```bash
# Install extension
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
# Use a command
/speckit.your-extension.command-name arg1 arg2
```
validations:
required: true
- type: textarea
id: catalog-entry
attributes:
label: Proposed Catalog Entry
description: Provide the JSON entry for catalog.json (helps reviewers)
render: json
placeholder: |
{
"your-extension": {
"name": "Your Extension",
"id": "your-extension",
"description": "Brief description",
"author": "Your Name",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/your-org/spec-kit-your-extension",
"homepage": "https://github.com/your-org/spec-kit-your-extension",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 3
},
"tags": ["category", "tool"],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-02-20T00:00:00Z",
"updated_at": "2026-02-20T00:00:00Z"
}
}
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Any other information that would help reviewers
placeholder: Screenshots, demo videos, links to related projects, etc.

View File

@@ -0,0 +1,104 @@
name: Feature Request
description: Suggest a new feature or enhancement for Specify CLI or Spec Kit
title: "[Feature]: "
labels: ["enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a feature! Please provide details below to help us understand and evaluate your request.
- type: textarea
id: problem
attributes:
label: Problem Statement
description: Is your feature request related to a problem? Please describe.
placeholder: "I'm frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like
placeholder: What would you like to happen?
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions or workarounds?
placeholder: What other approaches might work?
- type: dropdown
id: component
attributes:
label: Component
description: Which component does this feature relate to?
options:
- Specify CLI (initialization, commands)
- Spec templates (BDD, Testing Strategy, etc.)
- Agent integrations (command files, workflows)
- Scripts (Bash/PowerShell utilities)
- Documentation
- CI/CD workflows
- Other
validations:
required: true
- type: dropdown
id: ai-agent
attributes:
label: AI Agent (if applicable)
description: Does this feature relate to a specific AI agent?
options:
- All agents
- Claude Code
- Gemini CLI
- GitHub Copilot
- Cursor
- Qwen Code
- opencode
- Codex CLI
- Windsurf
- Kilo Code
- Auggie CLI
- Roo Code
- CodeBuddy
- Qoder CLI
- Kiro CLI
- Amp
- SHAI
- IBM Bob
- Antigravity
- Not applicable
- type: textarea
id: use-cases
attributes:
label: Use Cases
description: Describe specific use cases where this feature would be valuable
placeholder: |
1. When working on large projects...
2. During spec review...
3. When integrating with CI/CD...
- type: textarea
id: acceptance
attributes:
label: Acceptance Criteria
description: How would you know this feature is complete and working?
placeholder: |
- [ ] Feature does X
- [ ] Documentation is updated
- [ ] Works with all supported agents
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context, screenshots, or examples
placeholder: Links to similar features, mockups, related discussions, etc.

22
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,22 @@
## Description
<!-- What does this PR do? Why is it needed? -->
## Testing
<!-- How did you test your changes? -->
- [ ] Tested locally with `uv run specify --help`
- [ ] Ran existing tests with `uv sync && uv run pytest`
- [ ] Tested with a sample project (if applicable)
## AI Disclosure
<!-- Per our Contributing guidelines, AI assistance must be disclosed. -->
<!-- See: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md#ai-contributions-in-spec-kit -->
- [ ] I **did not** use AI assistance for this contribution
- [ ] I **did** use AI assistance (describe below)
<!-- If you used AI, briefly describe how (e.g., "Code generated by Copilot", "Consulted ChatGPT for approach"): -->

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

191
.github/workflows/RELEASE-PROCESS.md vendored Normal file
View File

@@ -0,0 +1,191 @@
# Release Process
This document describes the automated release process for Spec Kit.
## Overview
The release process is split into two workflows to ensure version consistency:
1. **Release Trigger Workflow** (`release-trigger.yml`) - Manages versioning and triggers release
2. **Release Workflow** (`release.yml`) - Builds and publishes artifacts
This separation ensures that git tags always point to commits with the correct version in `pyproject.toml`.
## Before Creating a Release
**Important**: Write clear, descriptive commit messages!
### How CHANGELOG.md Works
The CHANGELOG is **automatically generated** from your git commit messages:
1. **During Development**: Write clear, descriptive commit messages:
```bash
git commit -m "feat: Add new authentication feature"
git commit -m "fix: Resolve timeout issue in API client (#123)"
git commit -m "docs: Update installation instructions"
```
2. **When Releasing**: The release trigger workflow automatically:
- Finds all commits since the last release tag
- Formats them as changelog entries
- Inserts them into CHANGELOG.md
- Commits the updated changelog before creating the new tag
### Commit Message Best Practices
Good commit messages make good changelogs:
- **Be descriptive**: "Add user authentication" not "Update files"
- **Reference issues/PRs**: Include `(#123)` for automated linking
- **Use conventional commits** (optional): `feat:`, `fix:`, `docs:`, `chore:`
- **Keep it concise**: One line is ideal, details go in commit body
**Example commits that become good changelog entries:**
```
fix: prepend YAML frontmatter to Cursor .mdc files (#1699)
feat: add generic agent support with customizable command directories (#1639)
docs: document dual-catalog system for extensions (#1689)
```
## Creating a Release
### Option 1: Auto-Increment (Recommended for patches)
1. Go to **Actions** → **Release Trigger**
2. Click **Run workflow**
3. Leave the version field **empty**
4. Click **Run workflow**
The workflow will:
- Auto-increment the patch version (e.g., `0.1.10` → `0.1.11`)
- Update `pyproject.toml`
- Update `CHANGELOG.md` by adding a new section for the release based on commits since the last tag
- Commit changes to a `chore/release-vX.Y.Z` branch
- Create and push the git tag from that branch
- Open a PR to merge the version bump into `main`
- Trigger the release workflow automatically via the tag push
### Option 2: Manual Version (For major/minor bumps)
1. Go to **Actions** → **Release Trigger**
2. Click **Run workflow**
3. Enter the desired version (e.g., `0.2.0` or `v0.2.0`)
4. Click **Run workflow**
The workflow will:
- Use your specified version
- Update `pyproject.toml`
- Update `CHANGELOG.md` by adding a new section for the release based on commits since the last tag
- Commit changes to a `chore/release-vX.Y.Z` branch
- Create and push the git tag from that branch
- Open a PR to merge the version bump into `main`
- Trigger the release workflow automatically via the tag push
## What Happens Next
Once the release trigger workflow completes:
1. A `chore/release-vX.Y.Z` branch is pushed with the version bump commit
2. The git tag is pushed, pointing to that commit
3. The **Release Workflow** is automatically triggered by the tag push
4. Release artifacts are built for all supported agents
5. A GitHub Release is created with all assets
6. A PR is opened to merge the version bump branch into `main`
> **Note**: Merge the auto-opened PR after the release is published to keep `main` in sync.
## Workflow Details
### Release Trigger Workflow
**File**: `.github/workflows/release-trigger.yml`
**Trigger**: Manual (`workflow_dispatch`)
**Permissions Required**: `contents: write`
**Steps**:
1. Checkout repository
2. Determine version (manual or auto-increment)
3. Check if tag already exists (prevents duplicates)
4. Create `chore/release-vX.Y.Z` branch
5. Update `pyproject.toml`
6. Update `CHANGELOG.md` from git commits
7. Commit changes
8. Push branch and tag
9. Open PR to merge version bump into `main`
### Release Workflow
**File**: `.github/workflows/release.yml`
**Trigger**: Tag push (`v*`)
**Permissions Required**: `contents: write`
**Steps**:
1. Checkout repository at tag
2. Extract version from tag name
3. Check if release already exists
4. Build release package variants (all agents × shell/powershell)
5. Generate release notes from commits
6. Create GitHub Release with all assets
## Version Constraints
- Tags must follow format: `v{MAJOR}.{MINOR}.{PATCH}`
- Example valid versions: `v0.1.11`, `v0.2.0`, `v1.0.0`
- Auto-increment only bumps patch version
- Cannot create duplicate tags (workflow will fail)
## Benefits of This Approach
✅ **Version Consistency**: Git tags point to commits with matching `pyproject.toml` version
✅ **Single Source of Truth**: Version set once, used everywhere
✅ **Prevents Drift**: No more manual version synchronization needed
✅ **Clean Separation**: Versioning logic separate from artifact building
✅ **Flexibility**: Supports both auto-increment and manual versioning
## Troubleshooting
### No Commits Since Last Release
If you run the release trigger workflow when there are no new commits since the last tag:
- The workflow will still succeed
- The CHANGELOG will show "- Initial release" if it's the first release
- Or it will be empty if there are no commits
- Consider adding meaningful commits before releasing
**Best Practice**: Use descriptive commit messages - they become your changelog!
### Tag Already Exists
If you see "Error: Tag vX.Y.Z already exists!", you need to:
- Choose a different version number, or
- Delete the existing tag if it was created in error
### Release Workflow Didn't Trigger
Check that:
- The release trigger workflow completed successfully
- The tag was pushed (check repository tags)
- The release workflow is enabled in Actions settings
### Version Mismatch
If `pyproject.toml` doesn't match the latest tag:
- Run the release trigger workflow to sync versions
- Or manually update `pyproject.toml` and push changes before running the release trigger
## Legacy Behavior (Pre-v0.1.10)
Before this change, the release workflow:
- Created tags automatically on main branch pushes
- Updated `pyproject.toml` AFTER creating the tag
- Resulted in tags pointing to commits with outdated versions
This has been fixed in v0.1.10+.

32
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
strategy:
fail-fast: false
matrix:
language: [ 'actions', 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"

View File

@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0 # Fetch all history for git info

View File

@@ -12,9 +12,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@v19
with:
globs: '**/*.md'
globs: |
'**/*.md'
!extensions/**/*.md

161
.github/workflows/release-trigger.yml vendored Normal file
View File

@@ -0,0 +1,161 @@
name: Release Trigger
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.1.11). Leave empty to auto-increment patch version.'
required: false
type: string
jobs:
bump-version:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT }}
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Determine version
id: version
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
if [[ -n "$INPUT_VERSION" ]]; then
# Manual version specified - strip optional v prefix
VERSION="${INPUT_VERSION#v}"
# Validate strict semver format to prevent injection
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format '$VERSION'. Must be X.Y.Z (e.g. 1.2.3 or v1.2.3)"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
echo "Using manual version: $VERSION"
else
# Auto-increment patch version
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Latest tag: $LATEST_TAG"
# Extract version number and increment
VERSION=$(echo $LATEST_TAG | sed 's/v//')
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
MAJOR=${VERSION_PARTS[0]:-0}
MINOR=${VERSION_PARTS[1]:-0}
PATCH=${VERSION_PARTS[2]:-0}
# Increment patch version
PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "tag=v$NEW_VERSION" >> $GITHUB_OUTPUT
echo "Auto-incremented version: $NEW_VERSION"
fi
- name: Check if tag already exists
run: |
if git rev-parse "${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
echo "Error: Tag ${{ steps.version.outputs.tag }} already exists!"
exit 1
fi
- name: Create release branch
run: |
BRANCH="chore/release-${{ steps.version.outputs.tag }}"
git checkout -b "$BRANCH"
echo "branch=$BRANCH" >> $GITHUB_ENV
- name: Update pyproject.toml
run: |
sed -i "s/version = \".*\"/version = \"${{ steps.version.outputs.version }}\"/" pyproject.toml
echo "Updated pyproject.toml to version ${{ steps.version.outputs.version }}"
- name: Update CHANGELOG.md
run: |
if [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
# Get the previous tag to compare commits
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
echo "Generating changelog from commits..."
if [[ -n "$PREVIOUS_TAG" ]]; then
echo "Changes since $PREVIOUS_TAG"
COMMITS=$(git log --oneline "$PREVIOUS_TAG"..HEAD --no-merges --pretty=format:"- %s" 2>/dev/null || echo "- Initial release")
else
echo "No previous tag found - this is the first release"
COMMITS="- Initial release"
fi
# Create new changelog entry
{
head -n 8 CHANGELOG.md
echo ""
echo "## [${{ steps.version.outputs.version }}] - $DATE"
echo ""
echo "### Changed"
echo ""
echo "$COMMITS"
echo ""
tail -n +9 CHANGELOG.md
} > CHANGELOG.md.tmp
mv CHANGELOG.md.tmp CHANGELOG.md
echo "✅ Updated CHANGELOG.md with commits since $PREVIOUS_TAG"
else
echo "No CHANGELOG.md found"
fi
- name: Commit version bump
run: |
if [ -f "CHANGELOG.md" ]; then
git add pyproject.toml CHANGELOG.md
else
git add pyproject.toml
fi
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: bump version to ${{ steps.version.outputs.version }}"
echo "Changes committed"
fi
- name: Create and push tag
run: |
git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
git push origin "${{ env.branch }}"
git push origin "${{ steps.version.outputs.tag }}"
echo "Branch ${{ env.branch }} and tag ${{ steps.version.outputs.tag }} pushed"
- name: Open pull request
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
run: |
gh pr create \
--base main \
--head "${{ env.branch }}" \
--title "chore: bump version to ${{ steps.version.outputs.version }}" \
--body "Automated version bump to ${{ steps.version.outputs.version }}.
This PR was created by the Release Trigger workflow. The git tag \`${{ steps.version.outputs.tag }}\` has already been pushed and the release artifacts are being built.
Merge this PR to record the version bump and changelog update on \`main\`."
- name: Summary
run: |
echo "✅ Version bumped to ${{ steps.version.outputs.version }}"
echo "✅ Tag ${{ steps.version.outputs.tag }} created and pushed"
echo "✅ PR opened to merge version bump into main"
echo "🚀 Release workflow is building artifacts from the tag"

View File

@@ -2,59 +2,60 @@ name: Create Release
on:
push:
branches: [ main ]
paths:
- 'memory/**'
- 'scripts/**'
- 'templates/**'
- '.github/workflows/**'
workflow_dispatch:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Get latest tag
id: get_tag
- name: Extract version from tag
id: version
run: |
chmod +x .github/workflows/scripts/get-next-version.sh
.github/workflows/scripts/get-next-version.sh
VERSION=${GITHUB_REF#refs/tags/}
echo "tag=$VERSION" >> $GITHUB_OUTPUT
echo "Building release for $VERSION"
- name: Check if release already exists
id: check_release
run: |
chmod +x .github/workflows/scripts/check-release-exists.sh
.github/workflows/scripts/check-release-exists.sh ${{ steps.get_tag.outputs.new_version }}
.github/workflows/scripts/check-release-exists.sh ${{ steps.version.outputs.tag }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release package variants
if: steps.check_release.outputs.exists == 'false'
run: |
chmod +x .github/workflows/scripts/create-release-packages.sh
.github/workflows/scripts/create-release-packages.sh ${{ steps.get_tag.outputs.new_version }}
.github/workflows/scripts/create-release-packages.sh ${{ steps.version.outputs.tag }}
- name: Generate release notes
if: steps.check_release.outputs.exists == 'false'
id: release_notes
run: |
chmod +x .github/workflows/scripts/generate-release-notes.sh
.github/workflows/scripts/generate-release-notes.sh ${{ steps.get_tag.outputs.new_version }} ${{ steps.get_tag.outputs.latest_tag }}
# Get the previous tag for changelog generation
PREVIOUS_TAG=$(git describe --tags --abbrev=0 ${{ steps.version.outputs.tag }}^ 2>/dev/null || echo "")
# Default to v0.0.0 if no previous tag is found (e.g., first release)
if [ -z "$PREVIOUS_TAG" ]; then
PREVIOUS_TAG="v0.0.0"
fi
.github/workflows/scripts/generate-release-notes.sh ${{ steps.version.outputs.tag }} "$PREVIOUS_TAG"
- name: Create GitHub Release
if: steps.check_release.outputs.exists == 'false'
run: |
chmod +x .github/workflows/scripts/create-github-release.sh
.github/workflows/scripts/create-github-release.sh ${{ steps.get_tag.outputs.new_version }}
.github/workflows/scripts/create-github-release.sh ${{ steps.version.outputs.tag }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update version in pyproject.toml (for release artifacts only)
if: steps.check_release.outputs.exists == 'false'
run: |
chmod +x .github/workflows/scripts/update-version.sh
.github/workflows/scripts/update-version.sh ${{ steps.get_tag.outputs.new_version }}

18
.github/workflows/scripts/create-github-release.sh vendored Normal file → Executable file
View File

@@ -40,15 +40,25 @@ gh release create "$VERSION" \
.genreleases/spec-kit-template-roo-ps-"$VERSION".zip \
.genreleases/spec-kit-template-codebuddy-sh-"$VERSION".zip \
.genreleases/spec-kit-template-codebuddy-ps-"$VERSION".zip \
.genreleases/spec-kit-template-qoder-sh-"$VERSION".zip \
.genreleases/spec-kit-template-qoder-ps-"$VERSION".zip \
.genreleases/spec-kit-template-qodercli-sh-"$VERSION".zip \
.genreleases/spec-kit-template-qodercli-ps-"$VERSION".zip \
.genreleases/spec-kit-template-amp-sh-"$VERSION".zip \
.genreleases/spec-kit-template-amp-ps-"$VERSION".zip \
.genreleases/spec-kit-template-shai-sh-"$VERSION".zip \
.genreleases/spec-kit-template-shai-ps-"$VERSION".zip \
.genreleases/spec-kit-template-q-sh-"$VERSION".zip \
.genreleases/spec-kit-template-q-ps-"$VERSION".zip \
.genreleases/spec-kit-template-tabnine-sh-"$VERSION".zip \
.genreleases/spec-kit-template-tabnine-ps-"$VERSION".zip \
.genreleases/spec-kit-template-kiro-cli-sh-"$VERSION".zip \
.genreleases/spec-kit-template-kiro-cli-ps-"$VERSION".zip \
.genreleases/spec-kit-template-agy-sh-"$VERSION".zip \
.genreleases/spec-kit-template-agy-ps-"$VERSION".zip \
.genreleases/spec-kit-template-bob-sh-"$VERSION".zip \
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
.genreleases/spec-kit-template-vibe-sh-"$VERSION".zip \
.genreleases/spec-kit-template-vibe-ps-"$VERSION".zip \
.genreleases/spec-kit-template-kimi-sh-"$VERSION".zip \
.genreleases/spec-kit-template-kimi-ps-"$VERSION".zip \
.genreleases/spec-kit-template-generic-sh-"$VERSION".zip \
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
--title "Spec Kit Templates - $VERSION_NO_V" \
--notes-file release_notes.md

View File

@@ -8,13 +8,13 @@
.DESCRIPTION
create-release-packages.ps1 (workflow-local)
Build Spec Kit template release archives for each supported AI assistant and script type.
.PARAMETER Version
Version string with leading 'v' (e.g., v0.2.0)
.PARAMETER Agents
Comma or space separated subset of agents to build (default: all)
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, q, bob, qoder
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, kimi, generic
.PARAMETER Scripts
Comma or space separated subset of script types to build (default: both)
@@ -33,10 +33,10 @@
param(
[Parameter(Mandatory=$true, Position=0)]
[string]$Version,
[Parameter(Mandatory=$false)]
[string]$Agents = "",
[Parameter(Mandatory=$false)]
[string]$Scripts = ""
)
@@ -60,7 +60,7 @@ New-Item -ItemType Directory -Path $GenReleasesDir -Force | Out-Null
function Rewrite-Paths {
param([string]$Content)
$Content = $Content -replace '(/?)\bmemory/', '.specify/memory/'
$Content = $Content -replace '(/?)\bscripts/', '.specify/scripts/'
$Content = $Content -replace '(/?)\btemplates/', '.specify/templates/'
@@ -75,55 +75,55 @@ function Generate-Commands {
[string]$OutputDir,
[string]$ScriptVariant
)
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
foreach ($template in $templates) {
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
# Read file content and normalize line endings
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
# Extract description from YAML frontmatter
$description = ""
if ($fileContent -match '(?m)^description:\s*(.+)$') {
$description = $matches[1]
}
# Extract script command from YAML frontmatter
$scriptCommand = ""
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
$scriptCommand = $matches[1]
}
if ([string]::IsNullOrEmpty($scriptCommand)) {
Write-Warning "No script command found for $ScriptVariant in $($template.Name)"
$scriptCommand = "(Missing script command for $ScriptVariant)"
}
# Extract agent_script command from YAML frontmatter if present
$agentScriptCommand = ""
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
$agentScriptCommand = $matches[1].Trim()
}
# Replace {SCRIPT} placeholder with the script command
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
}
# Remove the scripts: and agent_scripts: sections from frontmatter
$lines = $body -split "`n"
$outputLines = @()
$inFrontmatter = $false
$skipScripts = $false
$dashCount = 0
foreach ($line in $lines) {
if ($line -match '^---$') {
$outputLines += $line
@@ -135,7 +135,7 @@ function Generate-Commands {
}
continue
}
if ($inFrontmatter) {
if ($line -match '^(scripts|agent_scripts):$') {
$skipScripts = $true
@@ -148,20 +148,20 @@ function Generate-Commands {
continue
}
}
$outputLines += $line
}
$body = $outputLines -join "`n"
# Apply other substitutions
$body = $body -replace '\{ARGS\}', $ArgFormat
$body = $body -replace '__AGENT__', $Agent
$body = Rewrite-Paths -Content $body
# Generate output file based on extension
$outputFile = Join-Path $OutputDir "speckit.$name.$Extension"
switch ($Extension) {
'toml' {
$body = $body -replace '\\', '\\'
@@ -183,15 +183,15 @@ function Generate-CopilotPrompts {
[string]$AgentsDir,
[string]$PromptsDir
)
New-Item -ItemType Directory -Path $PromptsDir -Force | Out-Null
$agentFiles = Get-ChildItem -Path "$AgentsDir/speckit.*.agent.md" -File -ErrorAction SilentlyContinue
foreach ($agentFile in $agentFiles) {
$basename = $agentFile.Name -replace '\.agent\.md$', ''
$promptFile = Join-Path $PromptsDir "$basename.prompt.md"
$content = @"
---
agent: $basename
@@ -201,31 +201,118 @@ agent: $basename
}
}
# Create Kimi Code skills in .kimi/skills/<name>/SKILL.md format.
# Kimi CLI discovers skills as directories containing a SKILL.md file,
# invoked with /skill:<name> (e.g. /skill:speckit.specify).
function New-KimiSkills {
param(
[string]$SkillsDir,
[string]$ScriptVariant
)
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
foreach ($template in $templates) {
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
$skillName = "speckit.$name"
$skillDir = Join-Path $SkillsDir $skillName
New-Item -ItemType Directory -Force -Path $skillDir | Out-Null
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
# Extract description
$description = "Spec Kit: $name workflow"
if ($fileContent -match '(?m)^description:\s*(.+)$') {
$description = $matches[1]
}
# Extract script command
$scriptCommand = "(Missing script command for $ScriptVariant)"
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
$scriptCommand = $matches[1]
}
# Extract agent_script command from frontmatter if present
$agentScriptCommand = ""
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
$agentScriptCommand = $matches[1].Trim()
}
# Replace {SCRIPT}, strip scripts sections, rewrite paths
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
}
$lines = $body -split "`n"
$outputLines = @()
$inFrontmatter = $false
$skipScripts = $false
$dashCount = 0
foreach ($line in $lines) {
if ($line -match '^---$') {
$outputLines += $line
$dashCount++
$inFrontmatter = ($dashCount -eq 1)
continue
}
if ($inFrontmatter) {
if ($line -match '^(scripts|agent_scripts):$') { $skipScripts = $true; continue }
if ($line -match '^[a-zA-Z].*:' -and $skipScripts) { $skipScripts = $false }
if ($skipScripts -and $line -match '^\s+') { continue }
}
$outputLines += $line
}
$body = $outputLines -join "`n"
$body = $body -replace '\{ARGS\}', '$ARGUMENTS'
$body = $body -replace '__AGENT__', 'kimi'
$body = Rewrite-Paths -Content $body
# Strip existing frontmatter, keep only body
$templateBody = ""
$fmCount = 0
$inBody = $false
foreach ($line in ($body -split "`n")) {
if ($line -match '^---$') {
$fmCount++
if ($fmCount -eq 2) { $inBody = $true }
continue
}
if ($inBody) { $templateBody += "$line`n" }
}
$skillContent = "---`nname: `"$skillName`"`ndescription: `"$description`"`n---`n`n$templateBody"
Set-Content -Path (Join-Path $skillDir "SKILL.md") -Value $skillContent -NoNewline
}
}
function Build-Variant {
param(
[string]$Agent,
[string]$Script
)
$baseDir = Join-Path $GenReleasesDir "sdd-${Agent}-package-${Script}"
Write-Host "Building $Agent ($Script) package..."
New-Item -ItemType Directory -Path $baseDir -Force | Out-Null
# Copy base structure but filter scripts by variant
$specDir = Join-Path $baseDir ".specify"
New-Item -ItemType Directory -Path $specDir -Force | Out-Null
# Copy memory directory
if (Test-Path "memory") {
Copy-Item -Path "memory" -Destination $specDir -Recurse -Force
Write-Host "Copied memory -> .specify"
}
# Only copy the relevant script variant directory
if (Test-Path "scripts") {
$scriptsDestDir = Join-Path $specDir "scripts"
New-Item -ItemType Directory -Path $scriptsDestDir -Force | Out-Null
switch ($Script) {
'sh' {
if (Test-Path "scripts/bash") {
@@ -240,18 +327,17 @@ function Build-Variant {
}
}
}
# Copy any script files that aren't in variant-specific directories
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
}
}
# Copy templates (excluding commands directory and vscode-settings.json)
if (Test-Path "templates") {
$templatesDestDir = Join-Path $specDir "templates"
New-Item -ItemType Directory -Path $templatesDestDir -Force | Out-Null
Get-ChildItem -Path "templates" -Recurse -File | Where-Object {
$_.FullName -notmatch 'templates[/\\]commands[/\\]' -and $_.Name -ne 'vscode-settings.json'
} | ForEach-Object {
@@ -263,7 +349,7 @@ function Build-Variant {
}
Write-Host "Copied templates -> .specify/templates"
}
# Generate agent-specific command files
switch ($Agent) {
'claude' {
@@ -280,12 +366,10 @@ function Build-Variant {
'copilot' {
$agentsDir = Join-Path $baseDir ".github/agents"
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
# Generate companion prompt files
$promptsDir = Join-Path $baseDir ".github/prompts"
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
# Create VS Code workspace settings
$vscodeDir = Join-Path $baseDir ".vscode"
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
if (Test-Path "templates/vscode-settings.json") {
@@ -298,7 +382,7 @@ function Build-Variant {
}
'qwen' {
$cmdDir = Join-Path $baseDir ".qwen/commands"
Generate-Commands -Agent 'qwen' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
Generate-Commands -Agent 'qwen' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
if (Test-Path "agent_templates/qwen/QWEN.md") {
Copy-Item -Path "agent_templates/qwen/QWEN.md" -Destination (Join-Path $baseDir "QWEN.md")
}
@@ -335,20 +419,50 @@ function Build-Variant {
$cmdDir = Join-Path $baseDir ".agents/commands"
Generate-Commands -Agent 'amp' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'q' {
$cmdDir = Join-Path $baseDir ".amazonq/prompts"
Generate-Commands -Agent 'q' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
'kiro-cli' {
$cmdDir = Join-Path $baseDir ".kiro/prompts"
Generate-Commands -Agent 'kiro-cli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'bob' {
$cmdDir = Join-Path $baseDir ".bob/commands"
Generate-Commands -Agent 'bob' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'qoder' {
'qodercli' {
$cmdDir = Join-Path $baseDir ".qoder/commands"
Generate-Commands -Agent 'qoder' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
Generate-Commands -Agent 'qodercli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'shai' {
$cmdDir = Join-Path $baseDir ".shai/commands"
Generate-Commands -Agent 'shai' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'tabnine' {
$cmdDir = Join-Path $baseDir ".tabnine/agent/commands"
Generate-Commands -Agent 'tabnine' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
$tabnineTemplate = Join-Path 'agent_templates' 'tabnine/TABNINE.md'
if (Test-Path $tabnineTemplate) { Copy-Item $tabnineTemplate (Join-Path $baseDir 'TABNINE.md') }
}
'agy' {
$cmdDir = Join-Path $baseDir ".agent/commands"
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'vibe' {
$cmdDir = Join-Path $baseDir ".vibe/prompts"
Generate-Commands -Agent 'vibe' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'kimi' {
$skillsDir = Join-Path $baseDir ".kimi/skills"
New-Item -ItemType Directory -Force -Path $skillsDir | Out-Null
New-KimiSkills -SkillsDir $skillsDir -ScriptVariant $Script
}
'generic' {
$cmdDir = Join-Path $baseDir ".speckit/commands"
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
default {
throw "Unsupported agent '$Agent'."
}
}
# Create zip archive
$zipFile = Join-Path $GenReleasesDir "spec-kit-template-${Agent}-${Script}-${Version}.zip"
Compress-Archive -Path "$baseDir/*" -DestinationPath $zipFile -Force
@@ -356,17 +470,16 @@ function Build-Variant {
}
# Define all agents and scripts
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'q', 'bob', 'qoder')
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'kimi', 'generic')
$AllScripts = @('sh', 'ps')
function Normalize-List {
param([string]$Input)
if ([string]::IsNullOrEmpty($Input)) {
return @()
}
# Split by comma or space and remove duplicates while preserving order
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
return $items
}
@@ -377,7 +490,7 @@ function Validate-Subset {
[string[]]$Allowed,
[string[]]$Items
)
$ok = $true
foreach ($item in $Items) {
if ($item -notin $Allowed) {
@@ -421,4 +534,4 @@ foreach ($agent in $AgentList) {
Write-Host "`nArchives in ${GenReleasesDir}:"
Get-ChildItem -Path $GenReleasesDir -Filter "spec-kit-template-*-${Version}.zip" | ForEach-Object {
Write-Host " $($_.Name)"
}
}

View File

@@ -6,7 +6,7 @@ set -euo pipefail
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
# Version argument should include leading 'v'.
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex amp shai bob (default: all)
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi generic (default: all)
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
# Examples:
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
@@ -34,7 +34,8 @@ rewrite_paths() {
sed -E \
-e 's@(/?)memory/@.specify/memory/@g' \
-e 's@(/?)scripts/@.specify/scripts/@g' \
-e 's@(/?)templates/@.specify/templates/@g'
-e 's@(/?)templates/@.specify/templates/@g' \
-e 's@\.specify\.specify/@.specify/@g'
}
generate_commands() {
@@ -44,19 +45,19 @@ generate_commands() {
[[ -f "$template" ]] || continue
local name description script_command agent_script_command body
name=$(basename "$template" .md)
# Normalize line endings
file_content=$(tr -d '\r' < "$template")
# Extract description and script command from YAML frontmatter
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
if [[ -z $script_command ]]; then
echo "Warning: no script command found for $script_variant in $template" >&2
script_command="(Missing script command for $script_variant)"
fi
# Extract agent_script command from YAML frontmatter if present
agent_script_command=$(printf '%s\n' "$file_content" | awk '
/^agent_scripts:$/ { in_agent_scripts=1; next }
@@ -67,15 +68,15 @@ generate_commands() {
}
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
')
# Replace {SCRIPT} placeholder with the script command
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
if [[ -n $agent_script_command ]]; then
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
fi
# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
body=$(printf '%s\n' "$body" | awk '
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
@@ -85,10 +86,10 @@ generate_commands() {
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
{ print }
')
# Apply other substitutions
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
case $ext in
toml)
body=$(printf '%s\n' "$body" | sed 's/\\/\\\\/g')
@@ -104,15 +105,14 @@ generate_commands() {
generate_copilot_prompts() {
local agents_dir=$1 prompts_dir=$2
mkdir -p "$prompts_dir"
# Generate a .prompt.md file for each .agent.md file
for agent_file in "$agents_dir"/speckit.*.agent.md; do
[[ -f "$agent_file" ]] || continue
local basename=$(basename "$agent_file" .agent.md)
local prompt_file="$prompts_dir/${basename}.prompt.md"
# Create prompt file with agent frontmatter
cat > "$prompt_file" <<EOF
---
agent: ${basename}
@@ -121,41 +121,104 @@ EOF
done
}
# Create Kimi Code skills in .kimi/skills/<name>/SKILL.md format.
# Kimi CLI discovers skills as directories containing a SKILL.md file,
# invoked with /skill:<name> (e.g. /skill:speckit.specify).
create_kimi_skills() {
local skills_dir="$1"
local script_variant="$2"
for template in templates/commands/*.md; do
[[ -f "$template" ]] || continue
local name
name=$(basename "$template" .md)
local skill_name="speckit.${name}"
local skill_dir="${skills_dir}/${skill_name}"
mkdir -p "$skill_dir"
local file_content
file_content=$(tr -d '\r' < "$template")
# Extract description from frontmatter
local description
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
[[ -z "$description" ]] && description="Spec Kit: ${name} workflow"
# Extract script command
local script_command
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
[[ -z "$script_command" ]] && script_command="(Missing script command for $script_variant)"
# Extract agent_script command from frontmatter if present
local agent_script_command
agent_script_command=$(printf '%s\n' "$file_content" | awk '
/^agent_scripts:$/ { in_agent_scripts=1; next }
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
print
exit
}
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
')
# Build body: replace placeholders, strip scripts sections, rewrite paths
local body
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
if [[ -n $agent_script_command ]]; then
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
fi
body=$(printf '%s\n' "$body" | awk '
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
in_frontmatter && /^agent_scripts:$/ { skip_scripts=1; next }
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
{ print }
')
body=$(printf '%s\n' "$body" | sed 's/{ARGS}/\$ARGUMENTS/g' | sed 's/__AGENT__/kimi/g' | rewrite_paths)
# Strip existing frontmatter and prepend Kimi frontmatter
local template_body
template_body=$(printf '%s\n' "$body" | awk '/^---/{p++; if(p==2){found=1; next}} found')
{
printf -- '---\n'
printf 'name: "%s"\n' "$skill_name"
printf 'description: "%s"\n' "$description"
printf -- '---\n\n'
printf '%s\n' "$template_body"
} > "$skill_dir/SKILL.md"
done
}
build_variant() {
local agent=$1 script=$2
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
echo "Building $agent ($script) package..."
mkdir -p "$base_dir"
# Copy base structure but filter scripts by variant
SPEC_DIR="$base_dir/.specify"
mkdir -p "$SPEC_DIR"
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
# Only copy the relevant script variant directory
if [[ -d scripts ]]; then
mkdir -p "$SPEC_DIR/scripts"
case $script in
sh)
[[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; }
# Copy any script files that aren't in variant-specific directories
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
;;
ps)
[[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; }
# Copy any script files that aren't in variant-specific directories
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
;;
esac
fi
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
# NOTE: We substitute {ARGS} internally. Outward tokens differ intentionally:
# * Markdown/prompt (claude, copilot, cursor-agent, opencode): $ARGUMENTS
# * TOML (gemini, qwen): {{args}}
# This keeps formats readable without extra abstraction.
case $agent in
claude)
@@ -168,9 +231,7 @@ build_variant() {
copilot)
mkdir -p "$base_dir/.github/agents"
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$script"
# Generate companion prompt files
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts"
# Create VS Code workspace settings
mkdir -p "$base_dir/.vscode"
[[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json"
;;
@@ -179,7 +240,7 @@ build_variant() {
generate_commands cursor-agent md "\$ARGUMENTS" "$base_dir/.cursor/commands" "$script" ;;
qwen)
mkdir -p "$base_dir/.qwen/commands"
generate_commands qwen toml "{{args}}" "$base_dir/.qwen/commands" "$script"
generate_commands qwen md "\$ARGUMENTS" "$base_dir/.qwen/commands" "$script"
[[ -f agent_templates/qwen/QWEN.md ]] && cp agent_templates/qwen/QWEN.md "$base_dir/QWEN.md" ;;
opencode)
mkdir -p "$base_dir/.opencode/command"
@@ -202,32 +263,47 @@ build_variant() {
codebuddy)
mkdir -p "$base_dir/.codebuddy/commands"
generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;;
qoder)
qodercli)
mkdir -p "$base_dir/.qoder/commands"
generate_commands qoder md "\$ARGUMENTS" "$base_dir/.qoder/commands" "$script" ;;
generate_commands qodercli md "\$ARGUMENTS" "$base_dir/.qoder/commands" "$script" ;;
amp)
mkdir -p "$base_dir/.agents/commands"
generate_commands amp md "\$ARGUMENTS" "$base_dir/.agents/commands" "$script" ;;
shai)
mkdir -p "$base_dir/.shai/commands"
generate_commands shai md "\$ARGUMENTS" "$base_dir/.shai/commands" "$script" ;;
q)
mkdir -p "$base_dir/.amazonq/prompts"
generate_commands q md "\$ARGUMENTS" "$base_dir/.amazonq/prompts" "$script" ;;
tabnine)
mkdir -p "$base_dir/.tabnine/agent/commands"
generate_commands tabnine toml "{{args}}" "$base_dir/.tabnine/agent/commands" "$script"
[[ -f agent_templates/tabnine/TABNINE.md ]] && cp agent_templates/tabnine/TABNINE.md "$base_dir/TABNINE.md" ;;
kiro-cli)
mkdir -p "$base_dir/.kiro/prompts"
generate_commands kiro-cli md "\$ARGUMENTS" "$base_dir/.kiro/prompts" "$script" ;;
agy)
mkdir -p "$base_dir/.agent/commands"
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/commands" "$script" ;;
bob)
mkdir -p "$base_dir/.bob/commands"
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
vibe)
mkdir -p "$base_dir/.vibe/prompts"
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
kimi)
mkdir -p "$base_dir/.kimi/skills"
create_kimi_skills "$base_dir/.kimi/skills" "$script" ;;
generic)
mkdir -p "$base_dir/.speckit/commands"
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
esac
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
}
# Determine agent list
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai q bob qoder)
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi generic)
ALL_SCRIPTS=(sh ps)
norm_list() {
# convert comma+space separated -> line separated unique while preserving order of first occurrence
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?"\n":"") $i);out=1}}}END{printf("\n")}'
}
@@ -270,4 +346,3 @@ done
echo "Archives in $GENRELEASES_DIR:"
ls -1 "$GENRELEASES_DIR"/spec-kit-template-*-"${NEW_VERSION}".zip

161
.github/workflows/scripts/simulate-release.sh vendored Executable file
View File

@@ -0,0 +1,161 @@
#!/usr/bin/env bash
set -euo pipefail
# simulate-release.sh
# Simulate the release process locally without pushing to GitHub
# Usage: simulate-release.sh [version]
# If version is omitted, auto-increments patch version
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${BLUE}🧪 Simulating Release Process Locally${NC}"
echo "======================================"
echo ""
# Step 1: Determine version
if [[ -n "${1:-}" ]]; then
VERSION="${1#v}"
TAG="v$VERSION"
echo -e "${GREEN}📝 Using manual version: $VERSION${NC}"
else
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo -e "${BLUE}Latest tag: $LATEST_TAG${NC}"
VERSION=$(echo $LATEST_TAG | sed 's/v//')
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
MAJOR=${VERSION_PARTS[0]:-0}
MINOR=${VERSION_PARTS[1]:-0}
PATCH=${VERSION_PARTS[2]:-0}
PATCH=$((PATCH + 1))
VERSION="$MAJOR.$MINOR.$PATCH"
TAG="v$VERSION"
echo -e "${GREEN}📝 Auto-incremented to: $VERSION${NC}"
fi
echo ""
# Step 2: Check if tag exists
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo -e "${RED}❌ Error: Tag $TAG already exists!${NC}"
echo " Please use a different version or delete the tag first."
exit 1
fi
echo -e "${GREEN}✓ Tag $TAG is available${NC}"
# Step 3: Backup current state
echo ""
echo -e "${YELLOW}💾 Creating backup of current state...${NC}"
BACKUP_DIR=$(mktemp -d)
cp pyproject.toml "$BACKUP_DIR/pyproject.toml.bak"
cp CHANGELOG.md "$BACKUP_DIR/CHANGELOG.md.bak"
echo -e "${GREEN}✓ Backup created at: $BACKUP_DIR${NC}"
# Step 4: Update pyproject.toml
echo ""
echo -e "${YELLOW}📝 Updating pyproject.toml...${NC}"
sed -i.tmp "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml
rm -f pyproject.toml.tmp
echo -e "${GREEN}✓ Updated pyproject.toml to version $VERSION${NC}"
# Step 5: Update CHANGELOG.md
echo ""
echo -e "${YELLOW}📝 Updating CHANGELOG.md...${NC}"
DATE=$(date +%Y-%m-%d)
# Get the previous tag to compare commits
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [[ -n "$PREVIOUS_TAG" ]]; then
echo " Generating changelog from commits since $PREVIOUS_TAG"
# Get commits since last tag, format as bullet points
COMMITS=$(git log --oneline "$PREVIOUS_TAG"..HEAD --no-merges --pretty=format:"- %s" 2>/dev/null || echo "- Initial release")
else
echo " No previous tag found - this is the first release"
COMMITS="- Initial release"
fi
# Create temp file with new entry
{
head -n 8 CHANGELOG.md
echo ""
echo "## [$VERSION] - $DATE"
echo ""
echo "### Changed"
echo ""
echo "$COMMITS"
echo ""
tail -n +9 CHANGELOG.md
} > CHANGELOG.md.tmp
mv CHANGELOG.md.tmp CHANGELOG.md
echo -e "${GREEN}✓ Updated CHANGELOG.md with commits since $PREVIOUS_TAG${NC}"
# Step 6: Show what would be committed
echo ""
echo -e "${YELLOW}📋 Changes that would be committed:${NC}"
git diff pyproject.toml CHANGELOG.md
# Step 7: Create temporary tag (no push)
echo ""
echo -e "${YELLOW}🏷️ Creating temporary local tag...${NC}"
git tag -a "$TAG" -m "Simulated release $TAG" 2>/dev/null || true
echo -e "${GREEN}✓ Tag $TAG created locally${NC}"
# Step 8: Simulate release artifact creation
echo ""
echo -e "${YELLOW}📦 Simulating release package creation...${NC}"
echo " (High-level simulation only; packaging script is not executed)"
echo ""
# Check if script exists and is executable
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -x "$SCRIPT_DIR/create-release-packages.sh" ]]; then
echo -e "${BLUE}In a real release, the following command would be run to create packages:${NC}"
echo " $SCRIPT_DIR/create-release-packages.sh \"$TAG\""
echo ""
echo "This simulation does not enumerate individual package files to avoid"
echo "drifting from the actual behavior of create-release-packages.sh."
else
echo -e "${RED}⚠️ create-release-packages.sh not found or not executable${NC}"
fi
# Step 9: Simulate release notes generation
echo ""
echo -e "${YELLOW}📄 Simulating release notes generation...${NC}"
echo ""
PREVIOUS_TAG=$(git describe --tags --abbrev=0 $TAG^ 2>/dev/null || echo "")
if [[ -n "$PREVIOUS_TAG" ]]; then
echo -e "${BLUE}Changes since $PREVIOUS_TAG:${NC}"
git log --oneline "$PREVIOUS_TAG".."$TAG" | head -n 10
echo ""
else
echo -e "${BLUE}No previous tag found - this would be the first release${NC}"
fi
# Step 10: Summary
echo ""
echo -e "${GREEN}🎉 Simulation Complete!${NC}"
echo "======================================"
echo ""
echo -e "${BLUE}Summary:${NC}"
echo " Version: $VERSION"
echo " Tag: $TAG"
echo " Backup: $BACKUP_DIR"
echo ""
echo -e "${YELLOW}⚠️ SIMULATION ONLY - NO CHANGES PUSHED${NC}"
echo ""
echo -e "${BLUE}Next steps:${NC}"
echo " 1. Review the changes above"
echo " 2. To keep changes: git add pyproject.toml CHANGELOG.md && git commit"
echo " 3. To discard changes: git checkout pyproject.toml CHANGELOG.md && git tag -d $TAG"
echo " 4. To restore from backup: cp $BACKUP_DIR/* ."
echo ""
echo -e "${BLUE}To run the actual release:${NC}"
echo " Go to: https://github.com/github/spec-kit/actions/workflows/release-trigger.yml"
echo " Click 'Run workflow' and enter version: $VERSION"
echo ""

42
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
workflow_dispatch: # Allow manual triggering
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
# Days of inactivity before an issue or PR becomes stale
days-before-stale: 150
# Days of inactivity before a stale issue or PR is closed (after being marked stale)
days-before-close: 30
# Stale issue settings
stale-issue-message: 'This issue has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
close-issue-message: 'This issue has been automatically closed due to inactivity (180 days total). If you believe this issue is still relevant, please reopen it or create a new issue.'
stale-issue-label: 'stale'
# Stale PR settings
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
close-pr-message: 'This pull request has been automatically closed due to inactivity (180 days total). If you believe this PR is still relevant, please reopen it or create a new PR.'
stale-pr-label: 'stale'
# Exempt issues and PRs with these labels from being marked as stale
exempt-issue-labels: 'pinned,security'
exempt-pr-labels: 'pinned,security'
# Only issues or PRs with all of these labels are checked
# Leave empty to check all issues and PRs
any-of-labels: ''
# Operations per run (helps avoid rate limits)
operations-per-run: 100

50
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Test & Lint Python
permissions:
contents: read
on:
push:
branches: ["main"]
pull_request:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Run ruff check
run: uvx ruff check src/
pytest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --extra test
- name: Run tests
run: uv run pytest

6
.gitignore vendored
View File

@@ -44,3 +44,9 @@ env/
.genreleases/
*.zip
sdd-*/
docs/dev
# Extension system
.specify/extensions/.cache/
.specify/extensions/.backup/
.specify/extensions/*/local-config.yml

View File

@@ -19,7 +19,9 @@
},
"MD050": {
"style": "asterisk"
}
},
"MD036": false,
"MD060": false
},
"ignores": [
".genreleases/"

View File

@@ -10,10 +10,6 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their
---
## General practices
- Any changes to `__init__.py` for the Specify CLI require a version rev in `pyproject.toml` and addition of entries to `CHANGELOG.md`.
## Adding New Agent Support
This section explains how to add support for new AI agents/assistants to the Specify CLI. Use this guide as a reference when integrating new AI tools into the Spec-Driven Development workflow.
@@ -35,7 +31,7 @@ Specify supports multiple AI agents by generating agent-specific command files a
| **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini CLI |
| **GitHub Copilot** | `.github/agents/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code |
| **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI |
| **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI |
| **Qwen Code** | `.qwen/commands/` | Markdown | `qwen` | Alibaba's Qwen Code CLI |
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
| **Codex CLI** | `.codex/commands/` | Markdown | `codex` | Codex CLI |
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
@@ -43,11 +39,14 @@ Specify supports multiple AI agents by generating agent-specific command files a
| **Auggie CLI** | `.augment/rules/` | Markdown | `auggie` | Auggie CLI |
| **Roo Code** | `.roo/rules/` | Markdown | N/A (IDE-based) | Roo Code IDE |
| **CodeBuddy CLI** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy CLI |
| **Qoder CLI** | `.qoder/commands/` | Markdown | `qoder` | Qoder CLI |
| **Amazon Q Developer CLI** | `.amazonq/prompts/` | Markdown | `q` | Amazon Q Developer CLI |
| **Qoder CLI** | `.qoder/commands/` | Markdown | `qodercli` | Qoder CLI |
| **Kiro CLI** | `.kiro/prompts/` | Markdown | `kiro-cli` | Kiro CLI |
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
| **Tabnine CLI** | `.tabnine/agent/commands/` | TOML | `tabnine` | Tabnine CLI |
| **Kimi Code** | `.kimi/skills/` | Markdown | `kimi` | Kimi Code CLI (Moonshot AI) |
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
### Step-by-Step Integration Guide
@@ -65,6 +64,7 @@ AGENT_CONFIG = {
"new-agent-cli": { # Use the ACTUAL CLI tool name (what users type in terminal)
"name": "New Agent Display Name",
"folder": ".newagent/", # Directory for agent files
"commands_subdir": "commands", # Subdirectory name for command files (default: "commands")
"install_url": "https://example.com/install", # URL for installation docs (or None if IDE-based)
"requires_cli": True, # True if CLI tool required, False for IDE-based agents
},
@@ -82,6 +82,10 @@ This eliminates the need for special-case mappings throughout the codebase.
- `name`: Human-readable display name shown to users
- `folder`: Directory where agent-specific files are stored (relative to project root)
- `commands_subdir`: Subdirectory name within the agent folder where command/prompt files are stored (default: `"commands"`)
- Most agents use `"commands"` (e.g., `.claude/commands/`)
- Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode), `"prompts"` (codex, kiro-cli), `"command"` (opencode - singular)
- This field enables `--ai-skills` to locate command templates correctly for skill generation
- `install_url`: Installation documentation URL (set to `None` for IDE-based agents)
- `requires_cli`: Whether the agent requires a CLI tool check during initialization
@@ -90,7 +94,7 @@ This eliminates the need for special-case mappings throughout the codebase.
Update the `--ai` parameter help text in the `init()` command to include the new agent:
```python
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, new-agent-cli, or q"),
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, new-agent-cli, or kiro-cli"),
```
Also update any function docstrings, examples, and error messages that list available agents.
@@ -111,7 +115,7 @@ Modify `.github/workflows/scripts/create-release-packages.sh`:
##### Add to ALL_AGENTS array
```bash
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf q)
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf kiro-cli)
```
##### Add case statement for directory structure
@@ -311,11 +315,13 @@ Require a command-line tool to be installed:
- **Cursor**: `cursor-agent` CLI
- **Qwen Code**: `qwen` CLI
- **opencode**: `opencode` CLI
- **Amazon Q Developer CLI**: `q` CLI
- **Kiro CLI**: `kiro-cli` CLI
- **CodeBuddy CLI**: `codebuddy` CLI
- **Qoder CLI**: `qoder` CLI
- **Qoder CLI**: `qodercli` CLI
- **Amp**: `amp` CLI
- **SHAI**: `shai` CLI
- **Tabnine CLI**: `tabnine` CLI
- **Kimi Code**: `kimi` CLI
### IDE-Based Agents
@@ -329,7 +335,7 @@ Work within integrated development environments:
### Markdown Format
Used by: Claude, Cursor, opencode, Windsurf, Amazon Q Developer, Amp, SHAI, IBM Bob
Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code, Qwen
**Standard format:**
@@ -354,7 +360,7 @@ Command content with {SCRIPT} and $ARGUMENTS placeholders.
### TOML Format
Used by: Gemini, Qwen
Used by: Gemini, Tabnine
```toml
description = "Command description"

View File

@@ -2,182 +2,303 @@
<!-- markdownlint-disable MD024 -->
All notable changes to the Specify CLI and templates are documented here.
Recent changes to the Specify CLI and templates are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.22] - 2025-11-07
- Support for VS Code/Copilot agents, and moving away from prompts to proper agents with hand-offs.
- Move to use `AGENTS.md` for Copilot workloads, since it's already supported out-of-the-box.
- Adds support for the version command. ([#486](https://github.com/github/spec-kit/issues/486))
- Fixes potential bug with the `create-new-feature.ps1` script that ignores existing feature branches when determining next feature number ([#975](https://github.com/github/spec-kit/issues/975))
- Add graceful fallback and logging for GitHub API rate-limiting during template fetch ([#970](https://github.com/github/spec-kit/issues/970))
## [0.0.21] - 2025-10-21
- Fixes [#975](https://github.com/github/spec-kit/issues/975) (thank you [@fgalarraga](https://github.com/fgalarraga)).
- Adds support for Amp CLI.
- Adds support for VS Code hand-offs and moves prompts to be full-fledged chat modes.
- Adds support for `version` command (addresses [#811](https://github.com/github/spec-kit/issues/811) and [#486](https://github.com/github/spec-kit/issues/486), thank you [@mcasalaina](https://github.com/mcasalaina) and [@dentity007](https://github.com/dentity007)).
- Adds support for rendering the rate limit errors from the CLI when encountered ([#970](https://github.com/github/spec-kit/issues/970), thank you [@psmman](https://github.com/psmman)).
## [0.0.20] - 2025-10-14
### Added
- **Intelligent Branch Naming**: `create-new-feature` scripts now support `--short-name` parameter for custom branch names
- When `--short-name` provided: Uses the custom name directly (cleaned and formatted)
- When omitted: Automatically generates meaningful names using stop word filtering and length-based filtering
- Filters out common stop words (I, want, to, the, for, etc.)
- Removes words shorter than 3 characters (unless they're uppercase acronyms)
- Takes 3-4 most meaningful words from the description
- **Enforces GitHub's 244-byte branch name limit** with automatic truncation and warnings
- Examples:
- "I want to create user authentication" → `001-create-user-authentication`
- "Implement OAuth2 integration for API" → `001-implement-oauth2-integration-api`
- "Fix payment processing bug" → `001-fix-payment-processing`
- Very long descriptions are automatically truncated at word boundaries to stay within limits
- Designed for AI agents to provide semantic short names while maintaining standalone usability
## [0.3.0] - 2026-03-13
### Changed
- Enhanced help documentation for `create-new-feature.sh` and `create-new-feature.ps1` scripts with examples
- Branch names now validated against GitHub's 244-byte limit with automatic truncation if needed
- No changes have been documented for this release yet.
## [0.0.19] - 2025-10-10
<!-- Entries for 0.2.x and earlier releases are documented in their respective sections below. -->
- make c ignores consistent with c++ (#1747)
- chore: bump version to 0.1.13 (#1746)
- feat: add kiro-cli and AGENT_CONFIG consistency coverage (#1690)
- feat: add verify extension to community catalog (#1726)
- Add Retrospective Extension to community catalog README table (#1741)
- fix(scripts): add empty description validation and branch checkout error handling (#1559)
- fix: correct Copilot extension command registration (#1724)
- fix(implement): remove Makefile from C ignore patterns (#1558)
- Add sync extension to community catalog (#1728)
- fix(checklist): clarify file handling behavior for append vs create (#1556)
- fix(clarify): correct conflicting question limit from 10 to 5 (#1557)
- chore: bump version to 0.1.12 (#1737)
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
## [Unreleased]
### Added
- Support for CodeBuddy (thank you to [@lispking](https://github.com/lispking) for the contribution).
- You can now see Git-sourced errors in the Specify CLI.
- feat(presets): Pluggable preset system with preset catalog and template resolver
- Preset manifest (`preset.yml`) with validation for artifact, command, and script types
- `PresetManifest`, `PresetRegistry`, `PresetManager`, `PresetCatalog`, `PresetResolver` classes in `src/specify_cli/presets.py`
- CLI commands: `specify preset search`, `specify preset add`, `specify preset list`, `specify preset remove`, `specify preset resolve`, `specify preset info`
- CLI commands: `specify preset catalog list`, `specify preset catalog add`, `specify preset catalog remove` for multi-catalog management
- `PresetCatalogEntry` dataclass and multi-catalog support mirroring the extension catalog system
- `--preset` option for `specify init` to install presets during initialization
- Priority-based preset resolution: presets with lower priority number win (`--priority` flag)
- `resolve_template()` / `Resolve-Template` helpers in bash and PowerShell common scripts
- Template resolution priority stack: overrides → presets → extensions → core
- Preset catalog files (`presets/catalog.json`, `presets/catalog.community.json`)
- Preset scaffold directory (`presets/scaffold/`)
- Scripts updated to use template resolution instead of hardcoded paths
- feat(presets): Preset command overrides now propagate to agent skills when `--ai-skills` was used during init
- feat: `specify init` persists CLI options to `.specify/init-options.json` for downstream operations
- feat(extensions): support `.extensionignore` to exclude files/folders during `specify extension add` (#1781)
## [0.2.1] - 2026-03-11
### Changed
- Fixed the path to the constitution in `plan.md` (thank you to [@lyzno1](https://github.com/lyzno1) for spotting).
- Fixed backslash escapes in generated TOML files for Gemini (thank you to [@hsin19](https://github.com/hsin19) for the contribution).
- Implementation command now ensures that the correct ignore files are added (thank you to [@sigent-amazon](https://github.com/sigent-amazon) for the contribution).
- Added February 2026 newsletter (#1812)
- feat: add Kimi Code CLI agent support (#1790)
- docs: fix broken links in quickstart guide (#1759) (#1797)
- docs: add catalog cli help documentation (#1793) (#1794)
- fix: use quiet checkout to avoid exception on git checkout (#1792)
- feat(extensions): support .extensionignore to exclude files during install (#1781)
- feat: add Codex support for extension command registration (#1767)
- chore: bump version to 0.2.0 (#1786)
- fix: sync agent list comments with actual supported agents (#1785)
- feat(extensions): support multiple active catalogs simultaneously (#1720)
- Pavel/add tabnine cli support (#1503)
- Add Understanding extension to community catalog (#1778)
- Add ralph extension to community catalog (#1780)
- Update README with project initialization instructions (#1772)
- feat: add review extension to community catalog (#1775)
- Add fleet extension to community catalog (#1771)
- Integration of Mistral vibe support into speckit (#1725)
- fix: Remove duplicate options in specify.md (#1765)
- fix: use global branch numbering instead of per-short-name detection (#1757)
- Add Community Walkthroughs section to README (#1766)
- feat(extensions): add Jira Integration to community catalog (#1764)
- Add Azure DevOps Integration extension to community catalog (#1734)
- Fix docs: update Antigravity link and add initialization example (#1748)
- fix: wire after_tasks and after_implement hook events into command templates (#1702)
- make c ignores consistent with c++ (#1747)
- chore: bump version to 0.1.13 (#1746)
- feat: add kiro-cli and AGENT_CONFIG consistency coverage (#1690)
- feat: add verify extension to community catalog (#1726)
- Add Retrospective Extension to community catalog README table (#1741)
- fix(scripts): add empty description validation and branch checkout error handling (#1559)
- fix: correct Copilot extension command registration (#1724)
- fix(implement): remove Makefile from C ignore patterns (#1558)
- Add sync extension to community catalog (#1728)
- fix(checklist): clarify file handling behavior for append vs create (#1556)
- fix(clarify): correct conflicting question limit from 10 to 5 (#1557)
- chore: bump version to 0.1.12 (#1737)
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
## [0.0.18] - 2025-10-06
### Added
- Support for using `.` as a shorthand for current directory in `specify init .` command, equivalent to `--here` flag but more intuitive for users.
- Use the `/speckit.` command prefix to easily discover Spec Kit-related commands.
- Refactor the prompts and templates to simplify their capabilities and how they are tracked. No more polluting things with tests when they are not needed.
- Ensure that tasks are created per user story (simplifies testing and validation).
- Add support for Visual Studio Code prompt shortcuts and automatic script execution.
## [0.2.0] - 2026-03-09
### Changed
- All command files now prefixed with `speckit.` (e.g., `speckit.specify.md`, `speckit.plan.md`) for better discoverability and differentiation in IDE/CLI command palettes and file explorers
- feat: add Kimi Code CLI agent support
- fix: sync agent list comments with actual supported agents (#1785)
- feat(extensions): support multiple active catalogs simultaneously (#1720)
- Pavel/add tabnine cli support (#1503)
- Add Understanding extension to community catalog (#1778)
- Add ralph extension to community catalog (#1780)
- Update README with project initialization instructions (#1772)
- feat: add review extension to community catalog (#1775)
- Add fleet extension to community catalog (#1771)
- Integration of Mistral vibe support into speckit (#1725)
- fix: Remove duplicate options in specify.md (#1765)
- fix: use global branch numbering instead of per-short-name detection (#1757)
- Add Community Walkthroughs section to README (#1766)
- feat(extensions): add Jira Integration to community catalog (#1764)
- Add Azure DevOps Integration extension to community catalog (#1734)
- Fix docs: update Antigravity link and add initialization example (#1748)
- fix: wire after_tasks and after_implement hook events into command templates (#1702)
- make c ignores consistent with c++ (#1747)
- chore: bump version to 0.1.13 (#1746)
- feat: add kiro-cli and AGENT_CONFIG consistency coverage (#1690)
- feat: add verify extension to community catalog (#1726)
- Add Retrospective Extension to community catalog README table (#1741)
- fix(scripts): add empty description validation and branch checkout error handling (#1559)
- fix: correct Copilot extension command registration (#1724)
- fix(implement): remove Makefile from C ignore patterns (#1558)
- Add sync extension to community catalog (#1728)
- fix(checklist): clarify file handling behavior for append vs create (#1556)
- fix(clarify): correct conflicting question limit from 10 to 5 (#1557)
- chore: bump version to 0.1.12 (#1737)
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
## [0.0.17] - 2025-09-22
## [0.1.14] - 2026-03-09
### Added
- New `/clarify` command template to surface up to 5 targeted clarification questions for an existing spec and persist answers into a Clarifications section in the spec.
- New `/analyze` command template providing a non-destructive cross-artifact discrepancy and alignment report (spec, clarifications, plan, tasks, constitution) inserted after `/tasks` and before `/implement`.
- Note: Constitution rules are explicitly treated as non-negotiable; any conflict is a CRITICAL finding requiring artifact remediation, not weakening of principles.
- feat: add Tabnine CLI agent support
- **Multi-Catalog Support (#1707)**: Extension catalog system now supports multiple active catalogs simultaneously via a catalog stack
- New `specify extension catalog list` command lists all active catalogs with name, URL, priority, and `install_allowed` status
- New `specify extension catalog add` and `specify extension catalog remove` commands for project-scoped catalog management
- Default built-in stack includes `catalog.json` (default, installable) and `catalog.community.json` (community, discovery only) — community extensions are now surfaced in search results out of the box
- `specify extension search` aggregates results across all active catalogs, annotating each result with source catalog
- `specify extension add` enforces `install_allowed` policy — extensions from discovery-only catalogs cannot be installed directly
- Project-level `.specify/extension-catalogs.yml` and user-level `~/.specify/extension-catalogs.yml` config files supported, with project-level taking precedence
- `SPECKIT_CATALOG_URL` environment variable still works for backward compatibility (replaces full stack with single catalog)
- All catalog URLs require HTTPS (HTTP allowed for localhost development)
- New `CatalogEntry` dataclass in `extensions.py` for catalog stack representation
- Per-URL hash-based caching for non-default catalogs; legacy cache preserved for default catalog
- Higher-priority catalogs win on merge conflicts (same extension id in multiple catalogs)
- 13 new tests covering catalog stack resolution, merge conflicts, URL validation, and `install_allowed` enforcement
- Updated RFC, Extension User Guide, and Extension API Reference documentation
## [0.0.16] - 2025-09-22
### Added
- `--force` flag for `init` command to bypass confirmation when using `--here` in a non-empty directory and proceed with merging/overwriting files.
## [0.0.15] - 2025-09-21
### Added
- Support for Roo Code.
## [0.0.14] - 2025-09-21
## [0.1.13] - 2026-03-03
### Changed
- Error messages are now shown consistently.
- feat: add kiro-cli and AGENT_CONFIG consistency coverage (#1690)
- feat: add verify extension to community catalog (#1726)
- Add Retrospective Extension to community catalog README table (#1741)
- fix(scripts): add empty description validation and branch checkout error handling (#1559)
- fix: correct Copilot extension command registration (#1724)
- fix(implement): remove Makefile from C ignore patterns (#1558)
- Add sync extension to community catalog (#1728)
- fix(checklist): clarify file handling behavior for append vs create (#1556)
- fix(clarify): correct conflicting question limit from 10 to 5 (#1557)
- chore: bump version to 0.1.12 (#1737)
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
## [0.0.13] - 2025-09-21
### Added
- Support for Kilo Code. Thank you [@shahrukhkhan489](https://github.com/shahrukhkhan489) with [#394](https://github.com/github/spec-kit/pull/394).
- Support for Auggie CLI. Thank you [@hungthai1401](https://github.com/hungthai1401) with [#137](https://github.com/github/spec-kit/pull/137).
- Agent folder security notice displayed after project provisioning completion, warning users that some agents may store credentials or auth tokens in their agent folders and recommending adding relevant folders to `.gitignore` to prevent accidental credential leakage.
### Changed
- Warning displayed to ensure that folks are aware that they might need to add their agent folder to `.gitignore`.
- Cleaned up the `check` command output.
## [0.0.12] - 2025-09-21
### Changed
- Added additional context for OpenAI Codex users - they need to set an additional environment variable, as described in [#417](https://github.com/github/spec-kit/issues/417).
## [0.0.11] - 2025-09-20
### Added
- Codex CLI support (thank you [@honjo-hiroaki-gtt](https://github.com/honjo-hiroaki-gtt) for the contribution in [#14](https://github.com/github/spec-kit/pull/14))
- Codex-aware context update tooling (Bash and PowerShell) so feature plans refresh `AGENTS.md` alongside existing assistants without manual edits.
## [0.0.10] - 2025-09-20
## [0.1.13] - 2026-03-03
### Fixed
- Addressed [#378](https://github.com/github/spec-kit/issues/378) where a GitHub token may be attached to the request when it was empty.
## [0.0.9] - 2025-09-19
- **Copilot Extension Commands Not Visible**: Fixed extension commands not appearing in GitHub Copilot when installed via `specify extension add --dev`
- Changed Copilot file extension from `.md` to `.agent.md` in `CommandRegistrar.AGENT_CONFIGS` so Copilot recognizes agent files
- Added generation of companion `.prompt.md` files in `.github/prompts/` during extension command registration, matching the release packaging behavior
- Added cleanup of `.prompt.md` companion files when removing extensions via `specify extension remove`
- Fixed a syntax regression in `src/specify_cli/__init__.py` in `_build_ai_assistant_help()` that broke `ruff` and `pytest` collection in CI.
## [0.1.12] - 2026-03-02
### Changed
- Improved agent selector UI with cyan highlighting for agent keys and gray parentheses for full names
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
## [0.0.8] - 2025-09-19
### Added
- Windsurf IDE support as additional AI assistant option (thank you [@raedkit](https://github.com/raedkit) for the work in [#151](https://github.com/github/spec-kit/pull/151))
- GitHub token support for API requests to handle corporate environments and rate limiting (contributed by [@zryfish](https://github.com/@zryfish) in [#243](https://github.com/github/spec-kit/pull/243))
### Changed
- Updated README with Windsurf examples and GitHub token usage
- Enhanced release workflow to include Windsurf templates
## [0.0.7] - 2025-09-18
### Changed
- Updated command instructions in the CLI.
- Cleaned up the code to not render agent-specific information when it's generic.
## [0.0.6] - 2025-09-17
### Added
- opencode support as additional AI assistant option
## [0.0.5] - 2025-09-17
### Added
- Qwen Code support as additional AI assistant option
## [0.0.4] - 2025-09-14
### Added
- SOCKS proxy support for corporate environments via `httpx[socks]` dependency
## [0.1.10] - 2026-03-02
### Fixed
N/A
- **Version Sync Issue (#1721)**: Fixed version mismatch between `pyproject.toml` and git release tags
- Split release process into two workflows: `release-trigger.yml` for version management and `release.yml` for artifact building
- Version bump now happens BEFORE tag creation, ensuring tags point to commits with correct version
- Supports both manual version specification and auto-increment (patch version)
- Git tags now accurately reflect the version in `pyproject.toml` at that commit
- Prevents confusion when installing from source
## [0.1.9] - 2026-02-28
### Changed
N/A
- Updated dependency: bumped astral-sh/setup-uv from 6 to 7
## [0.1.8] - 2026-02-28
### Changed
- Updated dependency: bumped actions/setup-python from 5 to 6
## [0.1.7] - 2026-02-27
### Changed
- Updated outdated GitHub Actions versions
- Documented dual-catalog system for extensions
### Fixed
- Fixed version command in documentation
### Added
- Added Cleanup Extension to README
- Added retrospective extension to community catalog
## [0.1.6] - 2026-02-23
### Fixed
- **Parameter Ordering Issues (#1641)**: Fixed CLI parameter parsing issue where option flags were incorrectly consumed as values for preceding options
- Added validation to detect when `--ai` or `--ai-commands-dir` incorrectly consume following flags like `--here` or `--ai-skills`
- Now provides clear error messages: "Invalid value for --ai: '--here'"
- Includes helpful hints suggesting proper usage and listing available agents
- Commands like `specify init --ai-skills --ai --here` now fail with actionable feedback instead of confusing "Must specify project name" errors
- Added comprehensive test suite (5 new tests) to prevent regressions
## [0.1.5] - 2026-02-21
### Fixed
- **AI Skills Installation Bug (#1658)**: Fixed `--ai-skills` flag not generating skill files for GitHub Copilot and other agents with non-standard command directory structures
- Added `commands_subdir` field to `AGENT_CONFIG` to explicitly specify the subdirectory name for each agent
- Affected agents now work correctly: copilot (`.github/agents/`), opencode (`.opencode/command/`), windsurf (`.windsurf/workflows/`), codex (`.codex/prompts/`), kilocode (`.kilocode/workflows/`), q (`.amazonq/prompts/`), and agy (`.agent/workflows/`)
- The `install_ai_skills()` function now uses the correct path for all agents instead of assuming `commands/` for everyone
## [0.1.4] - 2026-02-20
### Fixed
- **Qoder CLI detection**: Renamed `AGENT_CONFIG` key from `"qoder"` to `"qodercli"` to match the actual executable name, fixing `specify check` and `specify init --ai` detection failures
## [0.1.3] - 2026-02-20
### Added
- **Generic Agent Support**: Added `--ai generic` option for unsupported AI agents ("bring your own agent")
- Requires `--ai-commands-dir <path>` to specify where the agent reads commands from
- Generates Markdown commands with `$ARGUMENTS` format (compatible with most agents)
- Example: `specify init my-project --ai generic --ai-commands-dir .myagent/commands/`
- Enables users to start with Spec Kit immediately while their agent awaits formal support
## [0.0.102] - 2026-02-20
- fix: include 'src/**' path in release workflow triggers (#1646)
## [0.0.101] - 2026-02-19
- chore(deps): bump github/codeql-action from 3 to 4 (#1635)
## [0.0.100] - 2026-02-19
- Add pytest and Python linting (ruff) to CI (#1637)
- feat: add pull request template for better contribution guidelines (#1634)
## [0.0.99] - 2026-02-19
- Feat/ai skills (#1632)
## [0.0.98] - 2026-02-19
- chore(deps): bump actions/stale from 9 to 10 (#1623)
- feat: add dependabot configuration for pip and GitHub Actions updates (#1622)
## [0.0.97] - 2026-02-18
- Remove Maintainers section from README.md (#1618)
## [0.0.96] - 2026-02-17
- fix: typo in plan-template.md (#1446)
## [0.0.95] - 2026-02-12
- Feat: add a new agent: Google Anti Gravity (#1220)
## [0.0.94] - 2026-02-11
- Add stale workflow for 180-day inactive issues and PRs (#1594)

View File

@@ -22,6 +22,7 @@
- [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development)
- [⚡ Get Started](#-get-started)
- [📽️ Video Overview](#-video-overview)
- [🚶 Community Walkthroughs](#-community-walkthroughs)
- [🤖 Supported AI Agents](#-supported-ai-agents)
- [🔧 Specify CLI Reference](#-specify-cli-reference)
- [📚 Core Philosophy](#-core-philosophy)
@@ -31,7 +32,6 @@
- [📖 Learn More](#-learn-more)
- [📋 Detailed Process](#-detailed-process)
- [🔍 Troubleshooting](#-troubleshooting)
- [👥 Maintainers](#-maintainers)
- [💬 Support](#-support)
- [🙏 Acknowledgements](#-acknowledgements)
- [📄 License](#-license)
@@ -80,7 +80,13 @@ uv tool install specify-cli --force --from git+https://github.com/github/spec-ki
Run directly without installing:
```bash
# Create new project
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
# Or initialize in existing project
uvx --from git+https://github.com/github/spec-kit.git specify init . --ai claude
# or
uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai claude
```
**Benefits of persistent installation:**
@@ -140,12 +146,24 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
[![Spec Kit video header](/media/spec-kit-video-header.jpg)](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
## 🚶 Community Walkthroughs
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
- **[Greenfield Spring Boot + React platform](https://github.com/mnriem/spec-kit-spring-react-demo)** — Builds an LLM performance analytics platform (REST API, graphs, iteration tracking) from scratch using Spring Boot, embedded React, PostgreSQL, and Docker Compose, with a clarify step and a cross-artifact consistency analysis pass included.
- **[Brownfield ASP.NET CMS extension](https://github.com/mnriem/spec-kit-aspnet-brownfield-demo)** — Extends an existing open-source .NET CMS (CarrotCakeCMS-Core, ~307,000 lines of C#, Razor, SQL, JavaScript, and config files) with two new features — cross-platform Docker Compose infrastructure and a token-authenticated headless REST API — demonstrating how spec-kit fits into existing codebases without prior specs or a constitution.
- **[Brownfield Java runtime extension](https://github.com/mnriem/spec-kit-java-brownfield-demo)** — Extends an existing open-source Jakarta EE runtime (Piranha, ~420,000 lines of Java, XML, JSP, HTML, and config files across 180 Maven modules) with a password-protected Server Admin Console, demonstrating spec-kit on a large multi-module Java project with no prior specs or constitution.
## 🤖 Supported AI Agents
| Agent | Support | Notes |
| ------------------------------------------------------------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [Qoder CLI](https://qoder.com/cli) | ✅ | |
| [Amazon Q Developer CLI](https://aws.amazon.com/developer/learning/q-developer-cli/) | ⚠️ | Amazon Q Developer CLI [does not support](https://github.com/aws/amazon-q-developer-cli/issues/3064) custom arguments for slash commands. |
| [Kiro CLI](https://kiro.dev/docs/cli/) | ✅ | Use `--ai kiro-cli` (alias: `--ai kiro`) |
| [Amp](https://ampcode.com/) | ✅ | |
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | |
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | |
@@ -161,7 +179,12 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
| [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ | |
| [Roo Code](https://roocode.com/) | ✅ | |
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | ✅ | |
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ | |
| [Kimi Code](https://code.kimi.com/) | ✅ | |
| [Windsurf](https://windsurf.com/) | ✅ | |
| [Antigravity (agy)](https://antigravity.google/) | ✅ | Requires `--ai-skills` |
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
## 🔧 Specify CLI Reference
@@ -172,14 +195,15 @@ The `specify` command supports the following options:
| Command | Description |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `init` | Initialize a new Specify project from the latest template |
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `shai`, `qoder`) |
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`, `kimi`) |
### `specify init` Arguments & Options
| Argument/Option | Type | Description |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `q`, `bob`, or `qoder` |
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, `kimi`, or `generic` (requires `--ai-commands-dir`) |
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
| `--no-git` | Flag | Skip git repository initialization |
@@ -188,6 +212,7 @@ The `specify` command supports the following options:
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
| `--ai-skills` | Flag | Install Prompt.MD templates as agent skills in agent-specific `skills/` directory (requires `--ai`) |
### Examples
@@ -202,20 +227,32 @@ specify init my-project --ai claude
specify init my-project --ai cursor-agent
# Initialize with Qoder support
specify init my-project --ai qoder
specify init my-project --ai qodercli
# Initialize with Windsurf support
specify init my-project --ai windsurf
# Initialize with Kiro CLI support
specify init my-project --ai kiro-cli
# Initialize with Amp support
specify init my-project --ai amp
# Initialize with SHAI support
specify init my-project --ai shai
# Initialize with Mistral Vibe support
specify init my-project --ai vibe
# Initialize with IBM Bob support
specify init my-project --ai bob
# Initialize with Antigravity support
specify init my-project --ai agy --ai-skills
# Initialize with an unsupported agent (generic / bring your own agent)
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
# Initialize with PowerShell scripts (Windows/cross-platform)
specify init my-project --ai copilot --script ps
@@ -238,6 +275,12 @@ specify init my-project --ai claude --debug
# Use GitHub token for API requests (helpful for corporate environments)
specify init my-project --ai claude --github-token ghp_your_token_here
# Install agent skills with the project
specify init my-project --ai claude --ai-skills
# Initialize in current directory with agent skills
specify init --here --ai gemini --ai-skills
# Check system requirements
specify check
```
@@ -381,7 +424,7 @@ specify init . --force --ai claude
specify init --here --force --ai claude
```
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, or Amazon Q Developer CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, or Mistral Vibe installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
```bash
specify init <project_name> --ai claude --ignore-agent-tools
@@ -636,11 +679,6 @@ echo "Cleaning up..."
rm gcm-linux_amd64.2.6.1.deb
```
## 👥 Maintainers
- Den Delimarsky ([@localden](https://github.com/localden))
- John Lam ([@jflam](https://github.com/jflam))
## 💬 Support
For support, please open a [GitHub issue](https://github.com/github/spec-kit/issues/new). We welcome bug reports, feature requests, and questions about using Spec-Driven Development.

View File

@@ -81,6 +81,9 @@ Then, use the `/speckit.implement` slash command to execute the plan.
/speckit.implement
```
> [!TIP]
> **Phased Implementation**: For complex projects, implement in phases to avoid overwhelming the agent's context. Start with core functionality, validate it works, then add features incrementally.
## Detailed Example: Building Taskify
Here's a complete example of building a team productivity platform:
@@ -135,7 +138,15 @@ Be specific about your tech stack and technical requirements:
/speckit.plan We are going to generate this using .NET Aspire, using Postgres as the database. The frontend should use Blazor server with drag-and-drop task boards, real-time updates. There should be a REST API created with a projects API, tasks API, and a notifications API.
```
### Step 6: Validate and Implement
### Step 6: Define Tasks
Generate an actionable task list using the `/speckit.tasks` command:
```bash
/speckit.tasks
```
### Step 7: Validate and Implement
Have your AI agent audit the implementation plan using `/speckit.analyze`:
@@ -149,6 +160,9 @@ Finally, implement the solution:
/speckit.implement
```
> [!TIP]
> **Phased Implementation**: For large projects like Taskify, consider implementing in phases (e.g., Phase 1: Basic project/task structure, Phase 2: Kanban functionality, Phase 3: Comments and assignments). This prevents context saturation and allows for validation at each stage.
## Key Principles
- **Be explicit** about what you're building and why
@@ -159,6 +173,6 @@ Finally, implement the solution:
## Next Steps
- Read the [complete methodology](../spec-driven.md) for in-depth guidance
- Check out [more examples](../templates) in the repository
- Read the [complete methodology](https://github.com/github/spec-kit/blob/main/spec-driven.md) for in-depth guidance
- Check out [more examples](https://github.com/github/spec-kit/tree/main/templates) in the repository
- Explore the [source code on GitHub](https://github.com/github/spec-kit)

View File

@@ -0,0 +1,816 @@
# Extension API Reference
Technical reference for Spec Kit extension system APIs and manifest schema.
## Table of Contents
1. [Extension Manifest](#extension-manifest)
2. [Python API](#python-api)
3. [Command File Format](#command-file-format)
4. [Configuration Schema](#configuration-schema)
5. [Hook System](#hook-system)
6. [CLI Commands](#cli-commands)
---
## Extension Manifest
### Schema Version 1.0
File: `extension.yml`
```yaml
schema_version: "1.0" # Required
extension:
id: string # Required, pattern: ^[a-z0-9-]+$
name: string # Required, human-readable name
version: string # Required, semantic version (X.Y.Z)
description: string # Required, brief description (<200 chars)
author: string # Required
repository: string # Required, valid URL
license: string # Required (e.g., "MIT", "Apache-2.0")
homepage: string # Optional, valid URL
requires:
speckit_version: string # Required, version specifier (>=X.Y.Z)
tools: # Optional, array of tool requirements
- name: string # Tool name
version: string # Optional, version specifier
required: boolean # Optional, default: false
provides:
commands: # Required, at least one command
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
file: string # Required, relative path to command file
description: string # Required
aliases: [string] # Optional, array of alternate names
config: # Optional, array of config files
- name: string # Config file name
template: string # Template file path
description: string
required: boolean # Default: false
hooks: # Optional, event hooks
event_name: # e.g., "after_tasks", "after_implement"
command: string # Command to execute
optional: boolean # Default: true
prompt: string # Prompt text for optional hooks
description: string # Hook description
condition: string # Optional, condition expression
tags: # Optional, array of tags (2-10 recommended)
- string
defaults: # Optional, default configuration values
key: value # Any YAML structure
```
### Field Specifications
#### `extension.id`
- **Type**: string
- **Pattern**: `^[a-z0-9-]+$`
- **Description**: Unique extension identifier
- **Examples**: `jira`, `linear`, `azure-devops`
- **Invalid**: `Jira`, `my_extension`, `extension.id`
#### `extension.version`
- **Type**: string
- **Format**: Semantic versioning (X.Y.Z)
- **Description**: Extension version
- **Examples**: `1.0.0`, `0.9.5`, `2.1.3`
- **Invalid**: `v1.0`, `1.0`, `1.0.0-beta`
#### `requires.speckit_version`
- **Type**: string
- **Format**: Version specifier
- **Description**: Required spec-kit version range
- **Examples**:
- `>=0.1.0` - Any version 0.1.0 or higher
- `>=0.1.0,<2.0.0` - Version 0.1.x or 1.x
- `==0.1.0` - Exactly 0.1.0
- **Invalid**: `0.1.0`, `>= 0.1.0` (space), `latest`
#### `provides.commands[].name`
- **Type**: string
- **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$`
- **Description**: Namespaced command name
- **Format**: `speckit.{extension-id}.{command-name}`
- **Examples**: `speckit.jira.specstoissues`, `speckit.linear.sync`
- **Invalid**: `jira.specstoissues`, `speckit.command`, `speckit.jira.CreateIssues`
#### `hooks`
- **Type**: object
- **Keys**: Event names (e.g., `after_tasks`, `after_implement`, `before_commit`)
- **Description**: Hooks that execute at lifecycle events
- **Events**: Defined by core spec-kit commands
---
## Python API
### ExtensionManifest
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionManifest
manifest = ExtensionManifest(Path("extension.yml"))
```
**Properties**:
```python
manifest.id # str: Extension ID
manifest.name # str: Extension name
manifest.version # str: Version
manifest.description # str: Description
manifest.requires_speckit_version # str: Required spec-kit version
manifest.commands # List[Dict]: Command definitions
manifest.hooks # Dict: Hook definitions
```
**Methods**:
```python
manifest.get_hash() # str: SHA256 hash of manifest file
```
**Exceptions**:
```python
ValidationError # Invalid manifest structure
CompatibilityError # Incompatible with current spec-kit version
```
### ExtensionRegistry
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionRegistry
registry = ExtensionRegistry(extensions_dir)
```
**Methods**:
```python
# Add extension to registry
registry.add(extension_id: str, metadata: dict)
# Remove extension from registry
registry.remove(extension_id: str)
# Get extension metadata
metadata = registry.get(extension_id: str) # Optional[dict]
# List all extensions
extensions = registry.list() # Dict[str, dict]
# Check if installed
is_installed = registry.is_installed(extension_id: str) # bool
```
**Registry Format**:
```json
{
"schema_version": "1.0",
"extensions": {
"jira": {
"version": "1.0.0",
"source": "catalog",
"manifest_hash": "sha256...",
"enabled": true,
"registered_commands": ["speckit.jira.specstoissues", ...],
"installed_at": "2026-01-28T..."
}
}
}
```
### ExtensionManager
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionManager
manager = ExtensionManager(project_root)
```
**Methods**:
```python
# Install from directory
manifest = manager.install_from_directory(
source_dir: Path,
speckit_version: str,
register_commands: bool = True
) # Returns: ExtensionManifest
# Install from ZIP
manifest = manager.install_from_zip(
zip_path: Path,
speckit_version: str
) # Returns: ExtensionManifest
# Remove extension
success = manager.remove(
extension_id: str,
keep_config: bool = False
) # Returns: bool
# List installed extensions
extensions = manager.list_installed() # List[Dict]
# Get extension manifest
manifest = manager.get_extension(extension_id: str) # Optional[ExtensionManifest]
# Check compatibility
manager.check_compatibility(
manifest: ExtensionManifest,
speckit_version: str
) # Raises: CompatibilityError if incompatible
```
### CatalogEntry
**Module**: `specify_cli.extensions`
Represents a single catalog in the active catalog stack.
```python
from specify_cli.extensions import CatalogEntry
entry = CatalogEntry(
url="https://example.com/catalog.json",
name="default",
priority=1,
install_allowed=True,
description="Built-in catalog of installable extensions",
)
```
**Fields**:
| Field | Type | Description |
|-------|------|-------------|
| `url` | `str` | Catalog URL (must use HTTPS, or HTTP for localhost) |
| `name` | `str` | Human-readable catalog name |
| `priority` | `int` | Sort order (lower = higher priority, wins on conflicts) |
| `install_allowed` | `bool` | Whether extensions from this catalog can be installed |
| `description` | `str` | Optional human-readable description of the catalog (default: empty) |
### ExtensionCatalog
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionCatalog
catalog = ExtensionCatalog(project_root)
```
**Class attributes**:
```python
ExtensionCatalog.DEFAULT_CATALOG_URL # default catalog URL
ExtensionCatalog.COMMUNITY_CATALOG_URL # community catalog URL
```
**Methods**:
```python
# Get the ordered list of active catalogs
entries = catalog.get_active_catalogs() # List[CatalogEntry]
# Fetch catalog (primary catalog, backward compat)
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
# Search extensions across all active catalogs
# Each result includes _catalog_name and _install_allowed
results = catalog.search(
query: Optional[str] = None,
tag: Optional[str] = None,
author: Optional[str] = None,
verified_only: bool = False
) # Returns: List[Dict] — each dict includes _catalog_name, _install_allowed
# Get extension info (searches all active catalogs)
# Returns None if not found; includes _catalog_name and _install_allowed
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
# Check cache validity (primary catalog)
is_valid = catalog.is_cache_valid() # bool
# Clear all catalog caches
catalog.clear_cache()
```
**Result annotation fields**:
Each extension dict returned by `search()` and `get_extension_info()` includes:
| Field | Type | Description |
|-------|------|-------------|
| `_catalog_name` | `str` | Name of the source catalog |
| `_install_allowed` | `bool` | Whether installation is allowed from this catalog |
**Catalog config file** (`.specify/extension-catalogs.yml`):
```yaml
catalogs:
- name: "default"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
priority: 1
install_allowed: true
description: "Built-in catalog of installable extensions"
- name: "community"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
priority: 2
install_allowed: false
description: "Community-contributed extensions (discovery only)"
```
### HookExecutor
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import HookExecutor
hook_executor = HookExecutor(project_root)
```
**Methods**:
```python
# Get project config
config = hook_executor.get_project_config() # Dict
# Save project config
hook_executor.save_project_config(config: Dict)
# Register hooks
hook_executor.register_hooks(manifest: ExtensionManifest)
# Unregister hooks
hook_executor.unregister_hooks(extension_id: str)
# Get hooks for event
hooks = hook_executor.get_hooks_for_event(event_name: str) # List[Dict]
# Check if hook should execute
should_run = hook_executor.should_execute_hook(hook: Dict) # bool
# Format hook message
message = hook_executor.format_hook_message(
event_name: str,
hooks: List[Dict]
) # str
```
### CommandRegistrar
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import CommandRegistrar
registrar = CommandRegistrar()
```
**Methods**:
```python
# Register commands for Claude Code
registered = registrar.register_commands_for_claude(
manifest: ExtensionManifest,
extension_dir: Path,
project_root: Path
) # Returns: List[str] (command names)
# Parse frontmatter
frontmatter, body = registrar.parse_frontmatter(content: str)
# Render frontmatter
yaml_text = registrar.render_frontmatter(frontmatter: Dict) # str
```
---
## Command File Format
### Universal Command Format
**File**: `commands/{command-name}.md`
```markdown
---
description: "Command description"
tools:
- 'mcp-server/tool_name'
- 'other-mcp-server/other_tool'
---
# Command Title
Command documentation in Markdown.
## Prerequisites
1. Requirement 1
2. Requirement 2
## User Input
$ARGUMENTS
## Steps
### Step 1: Description
Instruction text...
\`\`\`bash
# Shell commands
\`\`\`
### Step 2: Another Step
More instructions...
## Configuration Reference
Information about configuration options.
## Notes
Additional notes and tips.
```
### Frontmatter Fields
```yaml
description: string # Required, brief command description
tools: [string] # Optional, MCP tools required
```
### Special Variables
- `$ARGUMENTS` - Placeholder for user-provided arguments
- Extension context automatically injected:
```markdown
<!-- Extension: {extension-id} -->
<!-- Config: .specify/extensions/{extension-id}/ -->
```
---
## Configuration Schema
### Extension Config File
**File**: `.specify/extensions/{extension-id}/{extension-id}-config.yml`
Extensions define their own config schema. Common patterns:
```yaml
# Connection settings
connection:
url: string
api_key: string
# Project settings
project:
key: string
workspace: string
# Feature flags
features:
enabled: boolean
auto_sync: boolean
# Defaults
defaults:
labels: [string]
assignee: string
# Custom fields
field_mappings:
internal_name: "external_field_id"
```
### Config Layers
1. **Extension Defaults** (from `extension.yml` `defaults` section)
2. **Project Config** (`{extension-id}-config.yml`)
3. **Local Override** (`{extension-id}-config.local.yml`, gitignored)
4. **Environment Variables** (`SPECKIT_{EXTENSION}_*`)
### Environment Variable Pattern
Format: `SPECKIT_{EXTENSION}_{KEY}`
Examples:
- `SPECKIT_JIRA_PROJECT_KEY`
- `SPECKIT_LINEAR_API_KEY`
- `SPECKIT_GITHUB_TOKEN`
---
## Hook System
### Hook Definition
**In extension.yml**:
```yaml
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy"
condition: null
```
### Hook Events
Standard events (defined by core):
- `after_tasks` - After task generation
- `after_implement` - After implementation
- `before_commit` - Before git commit
- `after_commit` - After git commit
### Hook Configuration
**In `.specify/extensions.yml`**:
```yaml
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
description: "..."
condition: null
```
### Hook Message Format
```markdown
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
Or for mandatory hooks:
```markdown
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
---
## CLI Commands
### extension list
**Usage**: `specify extension list [OPTIONS]`
**Options**:
- `--available` - Show available extensions from catalog
- `--all` - Show both installed and available
**Output**: List of installed extensions with metadata
### extension catalog list
**Usage**: `specify extension catalog list`
Lists all active catalogs in the current catalog stack, showing name, description, URL, priority, and `install_allowed` status.
### extension catalog add
**Usage**: `specify extension catalog add URL [OPTIONS]`
**Options**:
- `--name NAME` - Catalog name (required)
- `--priority INT` - Priority (lower = higher priority, default: 10)
- `--install-allowed / --no-install-allowed` - Allow installs from this catalog (default: false)
- `--description TEXT` - Optional description of the catalog
**Arguments**:
- `URL` - Catalog URL (must use HTTPS)
Adds a catalog entry to `.specify/extension-catalogs.yml`.
### extension catalog remove
**Usage**: `specify extension catalog remove NAME`
**Arguments**:
- `NAME` - Catalog name to remove
Removes a catalog entry from `.specify/extension-catalogs.yml`.
### extension add
**Usage**: `specify extension add EXTENSION [OPTIONS]`
**Options**:
- `--from URL` - Install from custom URL
- `--dev PATH` - Install from local directory
**Arguments**:
- `EXTENSION` - Extension name or URL
**Note**: Extensions from catalogs with `install_allowed: false` cannot be installed via this command.
### extension remove
**Usage**: `specify extension remove EXTENSION [OPTIONS]`
**Options**:
- `--keep-config` - Preserve config files
- `--force` - Skip confirmation
**Arguments**:
- `EXTENSION` - Extension ID
### extension search
**Usage**: `specify extension search [QUERY] [OPTIONS]`
Searches all active catalogs simultaneously. Results include source catalog name and install_allowed status.
**Options**:
- `--tag TAG` - Filter by tag
- `--author AUTHOR` - Filter by author
- `--verified` - Show only verified extensions
**Arguments**:
- `QUERY` - Optional search query
### extension info
**Usage**: `specify extension info EXTENSION`
Shows source catalog and install_allowed status.
**Arguments**:
- `EXTENSION` - Extension ID
### extension update
**Usage**: `specify extension update [EXTENSION]`
**Arguments**:
- `EXTENSION` - Optional, extension ID (default: all)
### extension enable
**Usage**: `specify extension enable EXTENSION`
**Arguments**:
- `EXTENSION` - Extension ID
### extension disable
**Usage**: `specify extension disable EXTENSION`
**Arguments**:
- `EXTENSION` - Extension ID
---
## Exceptions
### ValidationError
Raised when extension manifest validation fails.
```python
from specify_cli.extensions import ValidationError
try:
manifest = ExtensionManifest(path)
except ValidationError as e:
print(f"Invalid manifest: {e}")
```
### CompatibilityError
Raised when extension is incompatible with current spec-kit version.
```python
from specify_cli.extensions import CompatibilityError
try:
manager.check_compatibility(manifest, "0.1.0")
except CompatibilityError as e:
print(f"Incompatible: {e}")
```
### ExtensionError
Base exception for all extension-related errors.
```python
from specify_cli.extensions import ExtensionError
try:
manager.install_from_directory(path, "0.1.0")
except ExtensionError as e:
print(f"Extension error: {e}")
```
---
## Version Functions
### version_satisfies
Check if a version satisfies a specifier.
```python
from specify_cli.extensions import version_satisfies
# True if 1.2.3 satisfies >=1.0.0,<2.0.0
satisfied = version_satisfies("1.2.3", ">=1.0.0,<2.0.0") # bool
```
---
## File System Layout
```text
.specify/
├── extensions/
│ ├── .registry # Extension registry (JSON)
│ ├── .cache/ # Catalog cache
│ │ ├── catalog.json
│ │ └── catalog-metadata.json
│ ├── .backup/ # Config backups
│ │ └── {ext}-{config}.yml
│ ├── {extension-id}/ # Extension directory
│ │ ├── extension.yml # Manifest
│ │ ├── {ext}-config.yml # User config
│ │ ├── {ext}-config.local.yml # Local overrides (gitignored)
│ │ ├── {ext}-config.template.yml # Template
│ │ ├── commands/ # Command files
│ │ │ └── *.md
│ │ ├── scripts/ # Helper scripts
│ │ │ └── *.sh
│ │ ├── docs/ # Documentation
│ │ └── README.md
│ └── extensions.yml # Project extension config
└── scripts/ # (existing spec-kit)
.claude/
└── commands/
└── speckit.{ext}.{cmd}.md # Registered commands
```
---
*Last Updated: 2026-01-28*
*API Version: 1.0*
*Spec Kit Version: 0.1.0*

View File

@@ -0,0 +1,712 @@
# Extension Development Guide
A guide for creating Spec Kit extensions.
---
## Quick Start
### 1. Create Extension Directory
```bash
mkdir my-extension
cd my-extension
```
### 2. Create `extension.yml` Manifest
```yaml
schema_version: "1.0"
extension:
id: "my-ext" # Lowercase, alphanumeric + hyphens only
name: "My Extension"
version: "1.0.0" # Semantic versioning
description: "My custom extension"
author: "Your Name"
repository: "https://github.com/you/spec-kit-my-ext"
license: "MIT"
requires:
speckit_version: ">=0.1.0" # Minimum spec-kit version
tools: # Optional: External tools required
- name: "my-tool"
required: true
version: ">=1.0.0"
commands: # Optional: Core commands needed
- "speckit.tasks"
provides:
commands:
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
file: "commands/hello.md"
description: "Say hello"
aliases: ["speckit.hello"] # Optional aliases
config: # Optional: Config files
- name: "my-ext-config.yml"
template: "my-ext-config.template.yml"
description: "Extension configuration"
required: false
hooks: # Optional: Integration hooks
after_tasks:
command: "speckit.my-ext.hello"
optional: true
prompt: "Run hello command?"
tags: # Optional: For catalog search
- "example"
- "utility"
```
### 3. Create Commands Directory
```bash
mkdir commands
```
### 4. Create Command File
**File**: `commands/hello.md`
```markdown
---
description: "Say hello command"
tools: # Optional: AI tools this command uses
- 'some-tool/function'
scripts: # Optional: Helper scripts
sh: ../../scripts/bash/helper.sh
ps: ../../scripts/powershell/helper.ps1
---
# Hello Command
This command says hello!
## User Input
$ARGUMENTS
## Steps
1. Greet the user
2. Show extension is working
```bash
echo "Hello from my extension!"
echo "Arguments: $ARGUMENTS"
```
## Extension Configuration
Load extension config from `.specify/extensions/my-ext/my-ext-config.yml`.
### 5. Test Locally
```bash
cd /path/to/spec-kit-project
specify extension add --dev /path/to/my-extension
```
### 6. Verify Installation
```bash
specify extension list
# Should show:
# ✓ My Extension (v1.0.0)
# My custom extension
# Commands: 1 | Hooks: 1 | Status: Enabled
```
### 7. Test Command
If using Claude:
```bash
claude
> /speckit.my-ext.hello world
```
The command will be available in `.claude/commands/speckit.my-ext.hello.md`.
---
## Manifest Schema Reference
### Required Fields
#### `schema_version`
Extension manifest schema version. Currently: `"1.0"`
#### `extension`
Extension metadata block.
**Required sub-fields**:
- `id`: Extension identifier (lowercase, alphanumeric, hyphens)
- `name`: Human-readable name
- `version`: Semantic version (e.g., "1.0.0")
- `description`: Short description
**Optional sub-fields**:
- `author`: Extension author
- `repository`: Source code URL
- `license`: SPDX license identifier
- `homepage`: Extension homepage URL
#### `requires`
Compatibility requirements.
**Required sub-fields**:
- `speckit_version`: Semantic version specifier (e.g., ">=0.1.0,<2.0.0")
**Optional sub-fields**:
- `tools`: External tools required (array of tool objects)
- `commands`: Core spec-kit commands needed (array of command names)
- `scripts`: Core scripts required (array of script names)
#### `provides`
What the extension provides.
**Required sub-fields**:
- `commands`: Array of command objects (must have at least one)
**Command object**:
- `name`: Command name (must match `speckit.{ext-id}.{command}`)
- `file`: Path to command file (relative to extension root)
- `description`: Command description (optional)
- `aliases`: Alternative command names (optional, array)
### Optional Fields
#### `hooks`
Integration hooks for automatic execution.
Available hook points:
- `after_tasks`: After `/speckit.tasks` completes
- `after_implement`: After `/speckit.implement` completes (future)
Hook object:
- `command`: Command to execute (must be in `provides.commands`)
- `optional`: If true, prompt user before executing
- `prompt`: Prompt text for optional hooks
- `description`: Hook description
- `condition`: Execution condition (future)
#### `tags`
Array of tags for catalog discovery.
#### `defaults`
Default extension configuration values.
#### `config_schema`
JSON Schema for validating extension configuration.
---
## Command File Format
### Frontmatter (YAML)
```yaml
---
description: "Command description" # Required
tools: # Optional
- 'tool-name/function'
scripts: # Optional
sh: ../../scripts/bash/helper.sh
ps: ../../scripts/powershell/helper.ps1
---
```
### Body (Markdown)
Use standard Markdown with special placeholders:
- `$ARGUMENTS`: User-provided arguments
- `{SCRIPT}`: Replaced with script path during registration
**Example**:
````markdown
## Steps
1. Parse arguments
2. Execute logic
```bash
args="$ARGUMENTS"
echo "Running with args: $args"
```
````
### Script Path Rewriting
Extension commands use relative paths that get rewritten during registration:
**In extension**:
```yaml
scripts:
sh: ../../scripts/bash/helper.sh
```
**After registration**:
```yaml
scripts:
sh: .specify/scripts/bash/helper.sh
```
This allows scripts to reference core spec-kit scripts.
---
## Configuration Files
### Config Template
**File**: `my-ext-config.template.yml`
```yaml
# My Extension Configuration
# Copy this to my-ext-config.yml and customize
# Example configuration
api:
endpoint: "https://api.example.com"
timeout: 30
features:
feature_a: true
feature_b: false
credentials:
# DO NOT commit credentials!
# Use environment variables instead
api_key: "${MY_EXT_API_KEY}"
```
### Config Loading
In your command, load config with layered precedence:
1. Extension defaults (`extension.yml` → `defaults`)
2. Project config (`.specify/extensions/my-ext/my-ext-config.yml`)
3. Local overrides (`.specify/extensions/my-ext/my-ext-config.local.yml` - gitignored)
4. Environment variables (`SPECKIT_MY_EXT_*`)
**Example loading script**:
```bash
#!/usr/bin/env bash
EXT_DIR=".specify/extensions/my-ext"
# Load and merge config
config=$(yq eval '.' "$EXT_DIR/my-ext-config.yml" -o=json)
# Apply env overrides
if [ -n "${SPECKIT_MY_EXT_API_KEY:-}" ]; then
config=$(echo "$config" | jq ".api.api_key = \"$SPECKIT_MY_EXT_API_KEY\"")
fi
echo "$config"
```
---
## Excluding Files with `.extensionignore`
Extension authors can create a `.extensionignore` file in the extension root to exclude files and folders from being copied when a user installs the extension with `specify extension add`. This is useful for keeping development-only files (tests, CI configs, docs source, etc.) out of the installed copy.
### Format
The file uses `.gitignore`-compatible patterns (one per line), powered by the [`pathspec`](https://pypi.org/project/pathspec/) library:
- Blank lines are ignored
- Lines starting with `#` are comments
- `*` matches anything **except** `/` (does not cross directory boundaries)
- `**` matches zero or more directories (e.g., `docs/**/*.draft.md`)
- `?` matches any single character except `/`
- A trailing `/` restricts a pattern to directories only
- Patterns containing `/` (other than a trailing slash) are anchored to the extension root
- Patterns without `/` match at any depth in the tree
- `!` negates a previously excluded pattern (re-includes a file)
- Backslashes in patterns are normalised to forward slashes for cross-platform compatibility
- The `.extensionignore` file itself is always excluded automatically
### Example
```gitignore
# .extensionignore
# Development files
tests/
.github/
.gitignore
# Build artifacts
__pycache__/
*.pyc
dist/
# Documentation source (keep only the built README)
docs/
CONTRIBUTING.md
```
### Pattern Matching
| Pattern | Matches | Does NOT match |
|---------|---------|----------------|
| `*.pyc` | Any `.pyc` file in any directory | — |
| `tests/` | The `tests` directory (and all its contents) | A file named `tests` |
| `docs/*.draft.md` | `docs/api.draft.md` (directly inside `docs/`) | `docs/sub/api.draft.md` (nested) |
| `.env` | The `.env` file at any level | — |
| `!README.md` | Re-includes `README.md` even if matched by an earlier pattern | — |
| `docs/**/*.draft.md` | `docs/api.draft.md`, `docs/sub/api.draft.md` | — |
### Unsupported Features
The following `.gitignore` features are **not applicable** in this context:
- **Multiple `.extensionignore` files**: Only a single file at the extension root is supported (`.gitignore` supports files in subdirectories)
- **`$GIT_DIR/info/exclude` and `core.excludesFile`**: These are Git-specific and have no equivalent here
- **Negation inside excluded directories**: Because file copying uses `shutil.copytree`, excluding a directory prevents recursion into it entirely. A negation pattern cannot re-include a file inside a directory that was itself excluded. For example, the combination `tests/` followed by `!tests/important.py` will **not** preserve `tests/important.py` — the `tests/` directory is skipped at the root level and its contents are never evaluated. To work around this, exclude the directory's contents individually instead of the directory itself (e.g., `tests/*.pyc` and `tests/.cache/` rather than `tests/`).
---
## Validation Rules
### Extension ID
- **Pattern**: `^[a-z0-9-]+$`
- **Valid**: `my-ext`, `tool-123`, `awesome-plugin`
- **Invalid**: `MyExt` (uppercase), `my_ext` (underscore), `my ext` (space)
### Extension Version
- **Format**: Semantic versioning (MAJOR.MINOR.PATCH)
- **Valid**: `1.0.0`, `0.1.0`, `2.5.3`
- **Invalid**: `1.0`, `v1.0.0`, `1.0.0-beta`
### Command Name
- **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$`
- **Valid**: `speckit.my-ext.hello`, `speckit.tool.cmd`
- **Invalid**: `my-ext.hello` (missing prefix), `speckit.hello` (no extension namespace)
### Command File Path
- **Must be** relative to extension root
- **Valid**: `commands/hello.md`, `commands/subdir/cmd.md`
- **Invalid**: `/absolute/path.md`, `../outside.md`
---
## Testing Extensions
### Manual Testing
1. **Create test extension**
2. **Install locally**:
```bash
specify extension add --dev /path/to/extension
```
3. **Verify installation**:
```bash
specify extension list
```
4. **Test commands** with your AI agent
5. **Check command registration**:
```bash
ls .claude/commands/speckit.my-ext.*
```
6. **Remove extension**:
```bash
specify extension remove my-ext
```
### Automated Testing
Create tests for your extension:
```python
# tests/test_my_extension.py
import pytest
from pathlib import Path
from specify_cli.extensions import ExtensionManifest
def test_manifest_valid():
"""Test extension manifest is valid."""
manifest = ExtensionManifest(Path("extension.yml"))
assert manifest.id == "my-ext"
assert len(manifest.commands) >= 1
def test_command_files_exist():
"""Test all command files exist."""
manifest = ExtensionManifest(Path("extension.yml"))
for cmd in manifest.commands:
cmd_file = Path(cmd["file"])
assert cmd_file.exists(), f"Command file not found: {cmd_file}"
```
---
## Distribution
### Option 1: GitHub Repository
1. **Create repository**: `spec-kit-my-ext`
2. **Add files**:
```text
spec-kit-my-ext/
├── extension.yml
├── commands/
├── scripts/
├── docs/
├── README.md
├── LICENSE
└── CHANGELOG.md
```
3. **Create release**: Tag with version (e.g., `v1.0.0`)
4. **Install from repo**:
```bash
git clone https://github.com/you/spec-kit-my-ext
specify extension add --dev spec-kit-my-ext/
```
### Option 2: ZIP Archive (Future)
Create ZIP archive and host on GitHub Releases:
```bash
zip -r spec-kit-my-ext-1.0.0.zip extension.yml commands/ scripts/ docs/
```
Users install with:
```bash
specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
```
### Option 3: Community Reference Catalog
Submit to the community catalog for public discovery:
1. **Fork** spec-kit repository
2. **Add entry** to `extensions/catalog.community.json`
3. **Update** `extensions/README.md` with your extension
4. **Create PR** following the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md)
5. **After merge**, your extension becomes available:
- Users can browse `catalog.community.json` to discover your extension
- Users copy the entry to their own `catalog.json`
- Users install with: `specify extension add my-ext` (from their catalog)
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed submission instructions.
---
## Best Practices
### Naming Conventions
- **Extension ID**: Use descriptive, hyphenated names (`jira-integration`, not `ji`)
- **Commands**: Use verb-noun pattern (`create-issue`, `sync-status`)
- **Config files**: Match extension ID (`jira-config.yml`)
### Documentation
- **README.md**: Overview, installation, usage
- **CHANGELOG.md**: Version history
- **docs/**: Detailed guides
- **Command descriptions**: Clear, concise
### Versioning
- **Follow SemVer**: `MAJOR.MINOR.PATCH`
- **MAJOR**: Breaking changes
- **MINOR**: New features
- **PATCH**: Bug fixes
### Security
- **Never commit secrets**: Use environment variables
- **Validate input**: Sanitize user arguments
- **Document permissions**: What files/APIs are accessed
### Compatibility
- **Specify version range**: Don't require exact version
- **Test with multiple versions**: Ensure compatibility
- **Graceful degradation**: Handle missing features
---
## Example Extensions
### Minimal Extension
Smallest possible extension:
```yaml
# extension.yml
schema_version: "1.0"
extension:
id: "minimal"
name: "Minimal Extension"
version: "1.0.0"
description: "Minimal example"
requires:
speckit_version: ">=0.1.0"
provides:
commands:
- name: "speckit.minimal.hello"
file: "commands/hello.md"
```
````markdown
<!-- commands/hello.md -->
---
description: "Hello command"
---
# Hello World
```bash
echo "Hello, $ARGUMENTS!"
```
````
### Extension with Config
Extension using configuration:
```yaml
# extension.yml
# ... metadata ...
provides:
config:
- name: "tool-config.yml"
template: "tool-config.template.yml"
required: true
```
```yaml
# tool-config.template.yml
api_endpoint: "https://api.example.com"
timeout: 30
```
````markdown
<!-- commands/use-config.md -->
# Use Config
Load config:
```bash
config_file=".specify/extensions/tool/tool-config.yml"
endpoint=$(yq eval '.api_endpoint' "$config_file")
echo "Using endpoint: $endpoint"
```
````
### Extension with Hooks
Extension that runs automatically:
```yaml
# extension.yml
hooks:
after_tasks:
command: "speckit.auto.analyze"
optional: false # Always run
description: "Analyze tasks after generation"
```
---
## Troubleshooting
### Extension won't install
**Error**: `Invalid extension ID`
- **Fix**: Use lowercase, alphanumeric + hyphens only
**Error**: `Extension requires spec-kit >=0.2.0`
- **Fix**: Update spec-kit with `uv tool install specify-cli --force`
**Error**: `Command file not found`
- **Fix**: Ensure command files exist at paths specified in manifest
### Commands not registered
**Symptom**: Commands don't appear in AI agent
**Check**:
1. `.claude/commands/` directory exists
2. Extension installed successfully
3. Commands registered in registry:
```bash
cat .specify/extensions/.registry
```
**Fix**: Reinstall extension to trigger registration
### Config not loading
**Check**:
1. Config file exists: `.specify/extensions/{ext-id}/{ext-id}-config.yml`
2. YAML syntax is valid: `yq eval '.' config.yml`
3. Environment variables set correctly
---
## Getting Help
- **Issues**: Report bugs at GitHub repository
- **Discussions**: Ask questions in GitHub Discussions
- **Examples**: See `spec-kit-jira` for full-featured example (Phase B)
---
## Next Steps
1. **Create your extension** following this guide
2. **Test locally** with `--dev` flag
3. **Share with community** (GitHub, catalog)
4. **Iterate** based on feedback
Happy extending! 🚀

View File

@@ -0,0 +1,548 @@
# Extension Publishing Guide
This guide explains how to publish your extension to the Spec Kit extension catalog, making it discoverable by `specify extension search`.
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Prepare Your Extension](#prepare-your-extension)
3. [Submit to Catalog](#submit-to-catalog)
4. [Verification Process](#verification-process)
5. [Release Workflow](#release-workflow)
6. [Best Practices](#best-practices)
---
## Prerequisites
Before publishing an extension, ensure you have:
1. **Valid Extension**: A working extension with a valid `extension.yml` manifest
2. **Git Repository**: Extension hosted on GitHub (or other public git hosting)
3. **Documentation**: README.md with installation and usage instructions
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
5. **Versioning**: Semantic versioning (e.g., 1.0.0)
6. **Testing**: Extension tested on real projects
---
## Prepare Your Extension
### 1. Extension Structure
Ensure your extension follows the standard structure:
```text
your-extension/
├── extension.yml # Required: Extension manifest
├── README.md # Required: Documentation
├── LICENSE # Required: License file
├── CHANGELOG.md # Recommended: Version history
├── .gitignore # Recommended: Git ignore rules
├── commands/ # Extension commands
│ ├── command1.md
│ └── command2.md
├── config-template.yml # Config template (if needed)
└── docs/ # Additional documentation
├── usage.md
└── examples/
```
### 2. extension.yml Validation
Verify your manifest is valid:
```yaml
schema_version: "1.0"
extension:
id: "your-extension" # Unique lowercase-hyphenated ID
name: "Your Extension Name" # Human-readable name
version: "1.0.0" # Semantic version
description: "Brief description (one sentence)"
author: "Your Name or Organization"
repository: "https://github.com/your-org/spec-kit-your-extension"
license: "MIT"
homepage: "https://github.com/your-org/spec-kit-your-extension"
requires:
speckit_version: ">=0.1.0" # Required spec-kit version
provides:
commands: # List all commands
- name: "speckit.your-extension.command"
file: "commands/command.md"
description: "Command description"
tags: # 2-5 relevant tags
- "category"
- "tool-name"
```
**Validation Checklist**:
-`id` is lowercase with hyphens only (no underscores, spaces, or special characters)
-`version` follows semantic versioning (X.Y.Z)
-`description` is concise (under 100 characters)
-`repository` URL is valid and public
- ✅ All command files exist in the extension directory
- ✅ Tags are lowercase and descriptive
### 3. Create GitHub Release
Create a GitHub release for your extension version:
```bash
# Tag the release
git tag v1.0.0
git push origin v1.0.0
# Create release on GitHub
# Go to: https://github.com/your-org/spec-kit-your-extension/releases/new
# - Tag: v1.0.0
# - Title: v1.0.0 - Release Name
# - Description: Changelog/release notes
```
The release archive URL will be:
```text
https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
```
### 4. Test Installation
Test that users can install from your release:
```bash
# Test dev installation
specify extension add --dev /path/to/your-extension
# Test from GitHub archive
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
```
---
## Submit to Catalog
### Understanding the Catalogs
Spec Kit uses a dual-catalog system. For details about how catalogs work, see the main [Extensions README](README.md#extension-catalogs).
**For extension publishing**: All community extensions should be added to `catalog.community.json`. Users browse this catalog and copy extensions they trust into their own `catalog.json`.
### 1. Fork the spec-kit Repository
```bash
# Fork on GitHub
# https://github.com/github/spec-kit/fork
# Clone your fork
git clone https://github.com/YOUR-USERNAME/spec-kit.git
cd spec-kit
```
### 2. Add Extension to Community Catalog
Edit `extensions/catalog.community.json` and add your extension:
```json
{
"schema_version": "1.0",
"updated_at": "2026-01-28T15:54:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"your-extension": {
"name": "Your Extension Name",
"id": "your-extension",
"description": "Brief description of your extension",
"author": "Your Name",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/your-org/spec-kit-your-extension",
"homepage": "https://github.com/your-org/spec-kit-your-extension",
"documentation": "https://github.com/your-org/spec-kit-your-extension/blob/main/docs/",
"changelog": "https://github.com/your-org/spec-kit-your-extension/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "required-mcp-tool",
"version": ">=1.0.0",
"required": true
}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"category",
"tool-name",
"feature"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-01-28T00:00:00Z",
"updated_at": "2026-01-28T00:00:00Z"
}
}
}
```
**Important**:
- Set `verified: false` (maintainers will verify)
- Set `downloads: 0` and `stars: 0` (auto-updated later)
- Use current timestamp for `created_at` and `updated_at`
- Update the top-level `updated_at` to current time
### 3. Update Extensions README
Add your extension to the Available Extensions table in `extensions/README.md`:
```markdown
| Your Extension Name | Brief description of what it does | [repo-name](https://github.com/your-org/spec-kit-your-extension) |
```
Insert your extension in alphabetical order in the table.
### 4. Submit Pull Request
```bash
# Create a branch
git checkout -b add-your-extension
# Commit your changes
git add extensions/catalog.community.json extensions/README.md
git commit -m "Add your-extension to community catalog
- Extension ID: your-extension
- Version: 1.0.0
- Author: Your Name
- Description: Brief description
"
# Push to your fork
git push origin add-your-extension
# Create Pull Request on GitHub
# https://github.com/github/spec-kit/compare
```
**Pull Request Template**:
```markdown
## Extension Submission
**Extension Name**: Your Extension Name
**Extension ID**: your-extension
**Version**: 1.0.0
**Author**: Your Name
**Repository**: https://github.com/your-org/spec-kit-your-extension
### Description
Brief description of what your extension does.
### Checklist
- [x] Valid extension.yml manifest
- [x] README.md with installation and usage docs
- [x] LICENSE file included
- [x] GitHub release created (v1.0.0)
- [x] Extension tested on real project
- [x] All commands working
- [x] No security vulnerabilities
- [x] Added to extensions/catalog.community.json
- [x] Added to extensions/README.md Available Extensions table
### Testing
Tested on:
- macOS 13.0+ with spec-kit 0.1.0
- Project: [Your test project]
### Additional Notes
Any additional context or notes for reviewers.
```
---
## Verification Process
### What Happens After Submission
1. **Automated Checks** (if available):
- Manifest validation
- Download URL accessibility
- Repository existence
- License file presence
2. **Manual Review**:
- Code quality review
- Security audit
- Functionality testing
- Documentation review
3. **Verification**:
- If approved, `verified: true` is set
- Extension appears in `specify extension search --verified`
### Verification Criteria
To be verified, your extension must:
**Functionality**:
- Works as described in documentation
- All commands execute without errors
- No breaking changes to user workflows
**Security**:
- No known vulnerabilities
- No malicious code
- Safe handling of user data
- Proper validation of inputs
**Code Quality**:
- Clean, readable code
- Follows extension best practices
- Proper error handling
- Helpful error messages
**Documentation**:
- Clear installation instructions
- Usage examples
- Troubleshooting section
- Accurate description
**Maintenance**:
- Active repository
- Responsive to issues
- Regular updates
- Semantic versioning followed
### Typical Review Timeline
- **Automated checks**: Immediate (if implemented)
- **Manual review**: 3-7 business days
- **Verification**: After successful review
---
## Release Workflow
### Publishing New Versions
When releasing a new version:
1. **Update version** in `extension.yml`:
```yaml
extension:
version: "1.1.0" # Updated version
```
2. **Update CHANGELOG.md**:
```markdown
## [1.1.0] - 2026-02-15
### Added
- New feature X
### Fixed
- Bug fix Y
```
3. **Create GitHub release**:
```bash
git tag v1.1.0
git push origin v1.1.0
# Create release on GitHub
```
4. **Update catalog**:
```bash
# Fork spec-kit repo (or update existing fork)
cd spec-kit
# Update extensions/catalog.json
jq '.extensions["your-extension"].version = "1.1.0"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.extensions["your-extension"].download_url = "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.1.0.zip"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.extensions["your-extension"].updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
# Submit PR
git checkout -b update-your-extension-v1.1.0
git add extensions/catalog.json
git commit -m "Update your-extension to v1.1.0"
git push origin update-your-extension-v1.1.0
```
5. **Submit update PR** with changelog in description
---
## Best Practices
### Extension Design
1. **Single Responsibility**: Each extension should focus on one tool/integration
2. **Clear Naming**: Use descriptive, unambiguous names
3. **Minimal Dependencies**: Avoid unnecessary dependencies
4. **Backward Compatibility**: Follow semantic versioning strictly
### Documentation
1. **README.md Structure**:
- Overview and features
- Installation instructions
- Configuration guide
- Usage examples
- Troubleshooting
- Contributing guidelines
2. **Command Documentation**:
- Clear description
- Prerequisites listed
- Step-by-step instructions
- Error handling guidance
- Examples
3. **Configuration**:
- Provide template file
- Document all options
- Include examples
- Explain defaults
### Security
1. **Input Validation**: Validate all user inputs
2. **No Hardcoded Secrets**: Never include credentials
3. **Safe Dependencies**: Only use trusted dependencies
4. **Audit Regularly**: Check for vulnerabilities
### Maintenance
1. **Respond to Issues**: Address issues within 1-2 weeks
2. **Regular Updates**: Keep dependencies updated
3. **Changelog**: Maintain detailed changelog
4. **Deprecation**: Give advance notice for breaking changes
### Community
1. **License**: Use permissive open-source license (MIT, Apache 2.0)
2. **Contributing**: Welcome contributions
3. **Code of Conduct**: Be respectful and inclusive
4. **Support**: Provide ways to get help (issues, discussions, email)
---
## FAQ
### Q: Can I publish private/proprietary extensions?
A: The main catalog is for public extensions only. For private extensions:
- Host your own catalog.json file
- Users add your catalog: `specify extension add-catalog https://your-domain.com/catalog.json`
- Not yet implemented - coming in Phase 4
### Q: How long does verification take?
A: Typically 3-7 business days for initial review. Updates to verified extensions are usually faster.
### Q: What if my extension is rejected?
A: You'll receive feedback on what needs to be fixed. Make the changes and resubmit.
### Q: Can I update my extension anytime?
A: Yes, submit a PR to update the catalog with your new version. Verified status may be re-evaluated for major changes.
### Q: Do I need to be verified to be in the catalog?
A: No, unverified extensions are still searchable. Verification just adds trust and visibility.
### Q: Can extensions have paid features?
A: Extensions should be free and open-source. Commercial support/services are allowed, but core functionality must be free.
---
## Support
- **Catalog Issues**: <https://github.com/statsperform/spec-kit/issues>
- **Extension Template**: <https://github.com/statsperform/spec-kit-extension-template> (coming soon)
- **Development Guide**: See EXTENSION-DEVELOPMENT-GUIDE.md
- **Community**: Discussions and Q&A
---
## Appendix: Catalog Schema
### Complete Catalog Entry Schema
```json
{
"name": "string (required)",
"id": "string (required, unique)",
"description": "string (required, <200 chars)",
"author": "string (required)",
"version": "string (required, semver)",
"download_url": "string (required, valid URL)",
"repository": "string (required, valid URL)",
"homepage": "string (optional, valid URL)",
"documentation": "string (optional, valid URL)",
"changelog": "string (optional, valid URL)",
"license": "string (required)",
"requires": {
"speckit_version": "string (required, version specifier)",
"tools": [
{
"name": "string (required)",
"version": "string (optional, version specifier)",
"required": "boolean (default: false)"
}
]
},
"provides": {
"commands": "integer (optional)",
"hooks": "integer (optional)"
},
"tags": ["array of strings (2-10 tags)"],
"verified": "boolean (default: false)",
"downloads": "integer (auto-updated)",
"stars": "integer (auto-updated)",
"created_at": "string (ISO 8601 datetime)",
"updated_at": "string (ISO 8601 datetime)"
}
```
### Valid Tags
Recommended tag categories:
- **Integration**: jira, linear, github, gitlab, azure-devops
- **Category**: issue-tracking, vcs, ci-cd, documentation, testing
- **Platform**: atlassian, microsoft, google
- **Feature**: automation, reporting, deployment, monitoring
Use 2-5 tags that best describe your extension.
---
*Last Updated: 2026-01-28*
*Catalog Format Version: 1.0*

View File

@@ -0,0 +1,990 @@
# Extension User Guide
Complete guide for using Spec Kit extensions to enhance your workflow.
## Table of Contents
1. [Introduction](#introduction)
2. [Getting Started](#getting-started)
3. [Finding Extensions](#finding-extensions)
4. [Installing Extensions](#installing-extensions)
5. [Using Extensions](#using-extensions)
6. [Managing Extensions](#managing-extensions)
7. [Configuration](#configuration)
8. [Troubleshooting](#troubleshooting)
9. [Best Practices](#best-practices)
---
## Introduction
### What are Extensions?
Extensions are modular packages that add new commands and functionality to Spec Kit without bloating the core framework. They allow you to:
- **Integrate** with external tools (Jira, Linear, GitHub, etc.)
- **Automate** repetitive tasks with hooks
- **Customize** workflows for your team
- **Share** solutions across projects
### Why Use Extensions?
- **Clean Core**: Keeps spec-kit lightweight and focused
- **Optional Features**: Only install what you need
- **Community Driven**: Anyone can create and share extensions
- **Version Controlled**: Extensions are versioned independently
---
## Getting Started
### Prerequisites
- Spec Kit version 0.1.0 or higher
- A spec-kit project (directory with `.specify/` folder)
### Check Your Version
```bash
specify version
# Should show 0.1.0 or higher
```
### First Extension
Let's install the Jira extension as an example:
```bash
# 1. Search for the extension
specify extension search jira
# 2. Get detailed information
specify extension info jira
# 3. Install it
specify extension add jira
# 4. Configure it
vim .specify/extensions/jira/jira-config.yml
# 5. Use it
# (Commands are now available in Claude Code)
/speckit.jira.specstoissues
```
---
## Finding Extensions
`specify extension search` searches **all active catalogs** simultaneously, including the community catalog by default. Results are annotated with their source catalog and install status.
### Browse All Extensions
```bash
specify extension search
```
Shows all extensions across all active catalogs (default and community by default).
### Search by Keyword
```bash
# Search for "jira"
specify extension search jira
# Search for "issue tracking"
specify extension search issue
```
### Filter by Tag
```bash
# Find all issue-tracking extensions
specify extension search --tag issue-tracking
# Find all Atlassian tools
specify extension search --tag atlassian
```
### Filter by Author
```bash
# Extensions by Stats Perform
specify extension search --author "Stats Perform"
```
### Show Verified Only
```bash
# Only show verified extensions
specify extension search --verified
```
### Get Extension Details
```bash
# Detailed information
specify extension info jira
```
Shows:
- Description
- Requirements
- Commands provided
- Hooks available
- Links (documentation, repository, changelog)
- Installation status
---
## Installing Extensions
### Install from Catalog
```bash
# By name (from catalog)
specify extension add jira
```
This will:
1. Download the extension from GitHub
2. Validate the manifest
3. Check compatibility with your spec-kit version
4. Install to `.specify/extensions/jira/`
5. Register commands with your AI agent
6. Create config template
### Install from URL
```bash
# From GitHub release
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
```
### Install from Local Directory (Development)
```bash
# For testing or development
specify extension add --dev /path/to/extension
```
### Installation Output
```text
✓ Extension installed successfully!
Jira Integration (v1.0.0)
Create Jira Epics, Stories, and Issues from spec-kit artifacts
Provided commands:
• speckit.jira.specstoissues - Create Jira hierarchy from spec and tasks
• speckit.jira.discover-fields - Discover Jira custom fields for configuration
• speckit.jira.sync-status - Sync task completion status to Jira
⚠ Configuration may be required
Check: .specify/extensions/jira/
```
---
## Using Extensions
### Using Extension Commands
Extensions add commands that appear in your AI agent (Claude Code):
```text
# In Claude Code
> /speckit.jira.specstoissues
# Or use short alias (if provided)
> /speckit.specstoissues
```
### Extension Configuration
Most extensions require configuration:
```bash
# 1. Find the config file
ls .specify/extensions/jira/
# 2. Copy template to config
cp .specify/extensions/jira/jira-config.template.yml \
.specify/extensions/jira/jira-config.yml
# 3. Edit configuration
vim .specify/extensions/jira/jira-config.yml
# 4. Use the extension
# (Commands will now work with your config)
```
### Extension Hooks
Some extensions provide hooks that execute after core commands:
**Example**: Jira extension hooks into `/speckit.tasks`
```text
# Run core command
> /speckit.tasks
# Output includes:
## Extension Hooks
**Optional Hook**: jira
Command: `/speckit.jira.specstoissues`
Description: Automatically create Jira hierarchy after task generation
Prompt: Create Jira issues from tasks?
To execute: `/speckit.jira.specstoissues`
```
You can then choose to run the hook or skip it.
---
## Managing Extensions
### List Installed Extensions
```bash
specify extension list
```
Output:
```text
Installed Extensions:
✓ Jira Integration (v1.0.0)
Create Jira Epics, Stories, and Issues from spec-kit artifacts
Commands: 3 | Hooks: 1 | Status: Enabled
```
### Update Extensions
```bash
# Check for updates (all extensions)
specify extension update
# Update specific extension
specify extension update jira
```
Output:
```text
🔄 Checking for updates...
Updates available:
• jira: 1.0.0 → 1.1.0
Update these extensions? [y/N]:
```
### Disable Extension Temporarily
```bash
# Disable without removing
specify extension disable jira
✓ Extension 'jira' disabled
Commands will no longer be available. Hooks will not execute.
To re-enable: specify extension enable jira
```
### Re-enable Extension
```bash
specify extension enable jira
✓ Extension 'jira' enabled
```
### Remove Extension
```bash
# Remove extension (with confirmation)
specify extension remove jira
# Keep configuration when removing
specify extension remove jira --keep-config
# Force removal (no confirmation)
specify extension remove jira --force
```
---
## Configuration
### Configuration Files
Extensions can have multiple configuration files:
```text
.specify/extensions/jira/
├── jira-config.yml # Main config (version controlled)
├── jira-config.local.yml # Local overrides (gitignored)
└── jira-config.template.yml # Template (reference)
```
### Configuration Layers
Configuration is merged in this order (highest priority last):
1. **Extension defaults** (from `extension.yml`)
2. **Project config** (`jira-config.yml`)
3. **Local overrides** (`jira-config.local.yml`)
4. **Environment variables** (`SPECKIT_JIRA_*`)
### Example: Jira Configuration
**Project config** (`.specify/extensions/jira/jira-config.yml`):
```yaml
project:
key: "MSATS"
defaults:
epic:
labels: ["spec-driven"]
```
**Local override** (`.specify/extensions/jira/jira-config.local.yml`):
```yaml
project:
key: "MYTEST" # Override for local development
```
**Environment variable**:
```bash
export SPECKIT_JIRA_PROJECT_KEY="DEVTEST"
```
Final resolved config uses `DEVTEST` from environment variable.
### Project-Wide Extension Settings
File: `.specify/extensions.yml`
```yaml
# Extensions installed in this project
installed:
- jira
- linear
# Global settings
settings:
auto_execute_hooks: true
# Hook configuration
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
```
### Core Environment Variables
In addition to extension-specific environment variables (`SPECKIT_{EXT_ID}_*`), spec-kit supports core environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| `SPECKIT_CATALOG_URL` | Override the full catalog stack with a single URL (backward compat) | Built-in default stack |
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None |
#### Example: Using a custom catalog for testing
```bash
# Point to a local or alternative catalog (replaces the full stack)
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
# Or use a staging catalog
export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
```
---
## Extension Catalogs
Spec Kit uses a **catalog stack** — an ordered list of catalogs searched simultaneously. By default, two catalogs are active:
| Priority | Catalog | Install Allowed | Purpose |
|----------|---------|-----------------|---------|
| 1 | `catalog.json` (default) | ✅ Yes | Curated extensions available for installation |
| 2 | `catalog.community.json` (community) | ❌ No (discovery only) | Browse community extensions |
### Listing Active Catalogs
```bash
specify extension catalog list
```
### Managing Catalogs via CLI
You can view the main catalog management commands using `--help`:
```text
specify extension catalog --help
Usage: specify extension catalog [OPTIONS] COMMAND [ARGS]...
Manage extension catalogs
╭─ Options ────────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────────╮
│ list List all active extension catalogs. │
│ add Add a catalog to .specify/extension-catalogs.yml. │
│ remove Remove a catalog from .specify/extension-catalogs.yml. │
╰──────────────────────────────────────────────────────────────────────────────────╯
```
### Adding a Catalog (Project-scoped)
```bash
# Add an internal catalog that allows installs
specify extension catalog add \
--name "internal" \
--priority 2 \
--install-allowed \
https://internal.company.com/spec-kit/catalog.json
# Add a discovery-only catalog
specify extension catalog add \
--name "partner" \
--priority 5 \
https://partner.example.com/spec-kit/catalog.json
```
This creates or updates `.specify/extension-catalogs.yml`.
### Removing a Catalog
```bash
specify extension catalog remove internal
```
### Manual Config File
You can also edit `.specify/extension-catalogs.yml` directly:
```yaml
catalogs:
- name: "default"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
priority: 1
install_allowed: true
description: "Built-in catalog of installable extensions"
- name: "internal"
url: "https://internal.company.com/spec-kit/catalog.json"
priority: 2
install_allowed: true
description: "Internal company extensions"
- name: "community"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
priority: 3
install_allowed: false
description: "Community-contributed extensions (discovery only)"
```
A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. Project-level config takes full precedence when it contains one or more catalog entries. An empty `catalogs: []` list falls back to built-in defaults.
## Organization Catalog Customization
### Why Customize Your Catalog
Organizations customize their catalogs to:
- **Control available extensions** - Curate which extensions your team can install
- **Host private extensions** - Internal tools that shouldn't be public
- **Customize for compliance** - Meet security/audit requirements
- **Support air-gapped environments** - Work without internet access
### Setting Up a Custom Catalog
#### 1. Create Your Catalog File
Create a `catalog.json` file with your extensions:
```json
{
"schema_version": "1.0",
"updated_at": "2026-02-03T00:00:00Z",
"catalog_url": "https://your-org.com/spec-kit/catalog.json",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"description": "Create Jira issues from spec-kit artifacts",
"author": "Your Organization",
"version": "2.1.0",
"download_url": "https://github.com/your-org/spec-kit-jira/archive/refs/tags/v2.1.0.zip",
"repository": "https://github.com/your-org/spec-kit-jira",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{"name": "atlassian-mcp-server", "required": true}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": ["jira", "atlassian", "issue-tracking"],
"verified": true
},
"internal-tool": {
"name": "Internal Tool Integration",
"id": "internal-tool",
"description": "Connect to internal company systems",
"author": "Your Organization",
"version": "1.0.0",
"download_url": "https://internal.your-org.com/extensions/internal-tool-1.0.0.zip",
"repository": "https://github.internal.your-org.com/spec-kit-internal",
"license": "Proprietary",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2
},
"tags": ["internal", "proprietary"],
"verified": true
}
}
}
```
#### 2. Host the Catalog
Options for hosting your catalog:
| Method | URL Example | Use Case |
| ------ | ----------- | -------- |
| GitHub Pages | `https://your-org.github.io/spec-kit-catalog/catalog.json` | Public or org-visible |
| Internal web server | `https://internal.company.com/spec-kit/catalog.json` | Corporate network |
| S3/Cloud storage | `https://s3.amazonaws.com/your-bucket/catalog.json` | Cloud-hosted teams |
| Local file server | `http://localhost:8000/catalog.json` | Development/testing |
**Security requirement**: URLs must use HTTPS (except `localhost` for testing).
#### 3. Configure Your Environment
##### Option A: Catalog stack config file (recommended)
Add to `.specify/extension-catalogs.yml` in your project:
```yaml
catalogs:
- name: "my-org"
url: "https://your-org.com/spec-kit/catalog.json"
priority: 1
install_allowed: true
```
Or use the CLI:
```bash
specify extension catalog add \
--name "my-org" \
--install-allowed \
https://your-org.com/spec-kit/catalog.json
```
##### Option B: Environment variable (recommended for CI/CD, single-catalog)
```bash
# In ~/.bashrc, ~/.zshrc, or CI pipeline
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
```
#### 4. Verify Configuration
```bash
# List active catalogs
specify extension catalog list
# Search should now show your catalog's extensions
specify extension search
# Install from your catalog
specify extension add jira
```
### Catalog JSON Schema
Required fields for each extension entry:
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `name` | string | Yes | Human-readable name |
| `id` | string | Yes | Unique identifier (lowercase, hyphens) |
| `version` | string | Yes | Semantic version (X.Y.Z) |
| `download_url` | string | Yes | URL to ZIP archive |
| `repository` | string | Yes | Source code URL |
| `description` | string | No | Brief description |
| `author` | string | No | Author/organization |
| `license` | string | No | SPDX license identifier |
| `requires.speckit_version` | string | No | Version constraint |
| `requires.tools` | array | No | Required external tools |
| `provides.commands` | number | No | Number of commands |
| `provides.hooks` | number | No | Number of hooks |
| `tags` | array | No | Search tags |
| `verified` | boolean | No | Verification status |
### Use Cases
#### Private/Internal Extensions
Host proprietary extensions that integrate with internal systems:
```json
{
"internal-auth": {
"name": "Internal SSO Integration",
"download_url": "https://artifactory.company.com/spec-kit/internal-auth-1.0.0.zip",
"verified": true
}
}
```
#### Curated Team Catalog
Limit which extensions your team can install:
```json
{
"extensions": {
"jira": { "..." },
"github": { "..." }
}
}
```
Only `jira` and `github` will appear in `specify extension search`.
#### Air-Gapped Environments
For networks without internet access:
1. Download extension ZIPs to internal file server
2. Create catalog pointing to internal URLs
3. Host catalog on internal web server
```json
{
"jira": {
"download_url": "https://files.internal/spec-kit/jira-2.1.0.zip"
}
}
```
#### Development/Testing
Test new extensions before publishing:
```bash
# Start local server
python -m http.server 8000 --directory ./my-catalog/
# Point spec-kit to local catalog
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
# Test installation
specify extension add my-new-extension
```
### Combining with Direct Installation
You can still install extensions not in your catalog using `--from`:
```bash
# From catalog
specify extension add jira
# Direct URL (bypasses catalog)
specify extension add --from https://github.com/someone/spec-kit-ext/archive/v1.0.0.zip
# Local development
specify extension add --dev /path/to/extension
```
**Note**: Direct URL installation shows a security warning since the extension isn't from your configured catalog.
---
## Troubleshooting
### Extension Not Found
**Error**: `Extension 'jira' not found in catalog
**Solutions**:
1. Check spelling: `specify extension search jira`
2. Refresh catalog: `specify extension search --help`
3. Check internet connection
4. Extension may not be published yet
### Configuration Not Found
**Error**: `Jira configuration not found`
**Solutions**:
1. Check if extension is installed: `specify extension list`
2. Create config from template:
```bash
cp .specify/extensions/jira/jira-config.template.yml \
.specify/extensions/jira/jira-config.yml
```
3. Reinstall extension: `specify extension remove jira && specify extension add jira`
### Command Not Available
**Issue**: Extension command not appearing in AI agent
**Solutions**:
1. Check extension is enabled: `specify extension list`
2. Restart AI agent (Claude Code)
3. Check command file exists:
```bash
ls .claude/commands/speckit.jira.*.md
```
4. Reinstall extension
### Incompatible Version
**Error**: `Extension requires spec-kit >=0.2.0, but you have 0.1.0`
**Solutions**:
1. Upgrade spec-kit:
```bash
uv tool upgrade specify-cli
```
2. Install older version of extension:
```bash
specify extension add --from https://github.com/org/ext/archive/v1.0.0.zip
```
### MCP Tool Not Available
**Error**: `Tool 'jira-mcp-server/epic_create' not found`
**Solutions**:
1. Check MCP server is installed
2. Check AI agent MCP configuration
3. Restart AI agent
4. Check extension requirements: `specify extension info jira`
### Permission Denied
**Error**: `Permission denied` when accessing Jira
**Solutions**:
1. Check Jira credentials in MCP server config
2. Verify project permissions in Jira
3. Test MCP server connection independently
---
## Best Practices
### 1. Version Control
**Do commit**:
- `.specify/extensions.yml` (project extension config)
- `.specify/extensions/*/jira-config.yml` (project config)
**Don't commit**:
- `.specify/extensions/.cache/` (catalog cache)
- `.specify/extensions/.backup/` (config backups)
- `.specify/extensions/*/*.local.yml` (local overrides)
- `.specify/extensions/.registry` (installation state)
Add to `.gitignore`:
```gitignore
.specify/extensions/.cache/
.specify/extensions/.backup/
.specify/extensions/*/*.local.yml
.specify/extensions/.registry
```
### 2. Team Workflows
**For teams**:
1. Agree on which extensions to use
2. Commit extension configuration
3. Document extension usage in README
4. Keep extensions updated together
**Example README section**:
```markdown
## Extensions
This project uses:
- **jira** (v1.0.0) - Jira integration
- Config: `.specify/extensions/jira/jira-config.yml`
- Requires: jira-mcp-server
To install: `specify extension add jira`
```
### 3. Local Development
Use local config for development:
```yaml
# .specify/extensions/jira/jira-config.local.yml
project:
key: "DEVTEST" # Your test project
defaults:
task:
custom_fields:
customfield_10002: 1 # Lower story points for testing
```
### 4. Environment-Specific Config
Use environment variables for CI/CD:
```bash
# .github/workflows/deploy.yml
env:
SPECKIT_JIRA_PROJECT_KEY: ${{ secrets.JIRA_PROJECT }}
- name: Create Jira Issues
run: specify extension add jira && ...
```
### 5. Extension Updates
**Check for updates regularly**:
```bash
# Weekly or before major releases
specify extension update
```
**Pin versions for stability**:
```yaml
# .specify/extensions.yml
installed:
- id: jira
version: "1.0.0" # Pin to specific version
```
### 6. Minimal Extensions
Only install extensions you actively use:
- Reduces complexity
- Faster command loading
- Less configuration
### 7. Documentation
Document extension usage in your project:
```markdown
# PROJECT.md
## Working with Jira
After creating tasks, sync to Jira:
1. Run `/speckit.tasks` to generate tasks
2. Run `/speckit.jira.specstoissues` to create Jira issues
3. Run `/speckit.jira.sync-status` to update status
```
---
## FAQ
### Q: Can I use multiple extensions at once?
**A**: Yes! Extensions are designed to work together. Install as many as you need.
### Q: Do extensions slow down spec-kit?
**A**: No. Extensions are loaded on-demand and only when their commands are used.
### Q: Can I create private extensions?
**A**: Yes. Install with `--dev` or `--from` and keep private. Public catalog submission is optional.
### Q: How do I know if an extension is safe?
**A**: Look for the ✓ Verified badge. Verified extensions are reviewed by maintainers. Always review extension code before installing.
### Q: Can extensions modify spec-kit core?
**A**: No. Extensions can only add commands and hooks. They cannot modify core functionality.
### Q: What happens if two extensions have the same command name?
**A**: Extensions use namespaced commands (`speckit.{extension}.{command}`), so conflicts are very rare. The extension system will warn you if conflicts occur.
### Q: Can I contribute to existing extensions?
**A**: Yes! Most extensions are open source. Check the repository link in `specify extension info {extension}`.
### Q: How do I report extension bugs?
**A**: Go to the extension's repository (shown in `specify extension info`) and create an issue.
### Q: Can extensions work offline?
**A**: Once installed, extensions work offline. However, some extensions may require internet for their functionality (e.g., Jira requires Jira API access).
### Q: How do I backup my extension configuration?
**A**: Extension configs are in `.specify/extensions/{extension}/`. Back up this directory or commit configs to git.
---
## Support
- **Extension Issues**: Report to extension repository (see `specify extension info`)
- **Spec Kit Issues**: <https://github.com/statsperform/spec-kit/issues>
- **Extension Catalog**: <https://github.com/statsperform/spec-kit/tree/main/extensions>
- **Documentation**: See EXTENSION-DEVELOPMENT-GUIDE.md and EXTENSION-PUBLISHING-GUIDE.md
---
*Last Updated: 2026-01-28*
*Spec Kit Version: 0.1.0*

130
extensions/README.md Normal file
View File

@@ -0,0 +1,130 @@
# Spec Kit Extensions
Extension system for [Spec Kit](https://github.com/github/spec-kit) - add new functionality without bloating the core framework.
## Extension Catalogs
Spec Kit provides two catalog files with different purposes:
### Your Catalog (`catalog.json`)
- **Purpose**: Default upstream catalog of extensions used by the Spec Kit CLI
- **Default State**: Empty by design in the upstream project - you or your organization populate a fork/copy with extensions you trust
- **Location (upstream)**: `extensions/catalog.json` in the GitHub-hosted spec-kit repo
- **CLI Default**: The `specify extension` commands use the upstream catalog URL by default, unless overridden
- **Org Catalog**: Point `SPECKIT_CATALOG_URL` at your organization's fork or hosted catalog JSON to use it instead of the upstream default
- **Customization**: Copy entries from the community catalog into your org catalog, or add your own extensions directly
**Example override:**
```bash
# Override the default upstream catalog with your organization's catalog
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
specify extension search # Now uses your organization's catalog instead of the upstream default
```
### Community Reference Catalog (`catalog.community.json`)
- **Purpose**: Browse available community-contributed extensions
- **Status**: Active - contains extensions submitted by the community
- **Location**: `extensions/catalog.community.json`
- **Usage**: Reference catalog for discovering available extensions
- **Submission**: Open to community contributions via Pull Request
**How It Works:**
## Making Extensions Available
You control which extensions your team can discover and install:
### Option 1: Curated Catalog (Recommended for Organizations)
Populate your `catalog.json` with approved extensions:
1. **Discover** extensions from various sources:
- Browse `catalog.community.json` for community extensions
- Find private/internal extensions in your organization's repos
- Discover extensions from trusted third parties
2. **Review** extensions and choose which ones you want to make available
3. **Add** those extension entries to your own `catalog.json`
4. **Team members** can now discover and install them:
- `specify extension search` shows your curated catalog
- `specify extension add <name>` installs from your catalog
**Benefits**: Full control over available extensions, team consistency, organizational approval workflow
**Example**: Copy an entry from `catalog.community.json` to your `catalog.json`, then your team can discover and install it by name.
### Option 2: Direct URLs (For Ad-hoc Use)
Skip catalog curation - team members install directly using URLs:
```bash
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
```
**Benefits**: Quick for one-off testing or private extensions
**Tradeoff**: Extensions installed this way won't appear in `specify extension search` for other team members unless you also add them to your `catalog.json`.
## Available Community Extensions
The following community-contributed extensions are available in [`catalog.community.json`](catalog.community.json):
| Extension | Purpose | URL |
|-----------|---------|-----|
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
| 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 | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
| Ralph Loop | Autonomous implementation loop using AI agent CLI | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
| Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
| Understanding | Automated requirements quality analysis — 31 deterministic metrics against IEEE/ISO standards with experimental energy-based ambiguity detection | [understanding](https://github.com/Testimonial/understanding) |
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | [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 | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
## Adding Your Extension
### Submission Process
To add your extension to the community catalog:
1. **Prepare your extension** following the [Extension Development Guide](EXTENSION-DEVELOPMENT-GUIDE.md)
2. **Create a GitHub release** for your extension
3. **Submit a Pull Request** that:
- Adds your extension to `extensions/catalog.community.json`
- Updates this README with your extension in the Available Extensions table
4. **Wait for review** - maintainers will review and merge if criteria are met
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed step-by-step instructions.
### Submission Checklist
Before submitting, ensure:
- ✅ Valid `extension.yml` manifest
- ✅ Complete README with installation and usage instructions
- ✅ LICENSE file included
- ✅ GitHub release created with semantic version (e.g., v1.0.0)
- ✅ Extension tested on a real project
- ✅ All commands working as documented
## Installing Extensions
Once extensions are available (either in your catalog or via direct URL), install them:
```bash
# From your curated catalog (by name)
specify extension search # See what's in your catalog
specify extension add <extension-name> # Install by name
# Direct from URL (bypasses catalog)
specify extension add --from https://github.com/<org>/<repo>/archive/refs/tags/<version>.zip
# List installed extensions
specify extension list
```
For more information, see the [Extension User Guide](EXTENSION-USER-GUIDE.md).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,397 @@
{
"schema_version": "1.0",
"updated_at": "2026-03-13T12:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"azure-devops": {
"name": "Azure DevOps Integration",
"id": "azure-devops",
"description": "Sync user stories and tasks to Azure DevOps work items using OAuth authentication.",
"author": "pragya247",
"version": "1.0.0",
"download_url": "https://github.com/pragya247/spec-kit-azure-devops/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/pragya247/spec-kit-azure-devops",
"homepage": "https://github.com/pragya247/spec-kit-azure-devops",
"documentation": "https://github.com/pragya247/spec-kit-azure-devops/blob/main/README.md",
"changelog": "https://github.com/pragya247/spec-kit-azure-devops/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "az",
"version": ">=2.0.0",
"required": true
}
]
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"azure",
"devops",
"project-management",
"work-items",
"issue-tracking"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-03T00:00:00Z",
"updated_at": "2026-03-03T00:00:00Z"
},
"cleanup": {
"name": "Cleanup Extension",
"id": "cleanup",
"description": "Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues.",
"author": "dsrednicki",
"version": "1.0.0",
"download_url": "https://github.com/dsrednicki/spec-kit-cleanup/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/dsrednicki/spec-kit-cleanup",
"homepage": "https://github.com/dsrednicki/spec-kit-cleanup",
"documentation": "https://github.com/dsrednicki/spec-kit-cleanup/blob/main/README.md",
"changelog": "https://github.com/dsrednicki/spec-kit-cleanup/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"quality",
"tech-debt",
"review",
"cleanup",
"scout-rule"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-02-22T00:00:00Z",
"updated_at": "2026-02-22T00:00:00Z"
},
"doctor": {
"name": "Project Health Check",
"id": "doctor",
"description": "Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git.",
"author": "KhawarHabibKhan",
"version": "1.0.0",
"download_url": "https://github.com/KhawarHabibKhan/spec-kit-doctor/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/KhawarHabibKhan/spec-kit-doctor",
"homepage": "https://github.com/KhawarHabibKhan/spec-kit-doctor",
"documentation": "https://github.com/KhawarHabibKhan/spec-kit-doctor/blob/main/README.md",
"changelog": "https://github.com/KhawarHabibKhan/spec-kit-doctor/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 0
},
"tags": [
"diagnostics",
"health-check",
"validation",
"project-structure"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-13T00:00:00Z",
"updated_at": "2026-03-13T00:00:00Z"
},
"fleet": {
"name": "Fleet Orchestrator",
"id": "fleet",
"description": "Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases.",
"author": "sharathsatish",
"version": "1.0.0",
"download_url": "https://github.com/sharathsatish/spec-kit-fleet/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/sharathsatish/spec-kit-fleet",
"homepage": "https://github.com/sharathsatish/spec-kit-fleet",
"documentation": "https://github.com/sharathsatish/spec-kit-fleet/blob/main/README.md",
"changelog": "https://github.com/sharathsatish/spec-kit-fleet/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2,
"hooks": 1
},
"tags": ["orchestration", "workflow", "human-in-the-loop", "parallel"],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-06T00:00:00Z",
"updated_at": "2026-03-06T00:00:00Z"
},
"jira": {
"name": "Jira Integration",
"id": "jira",
"description": "Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support.",
"author": "mbachorik",
"version": "2.1.0",
"download_url": "https://github.com/mbachorik/spec-kit-jira/archive/refs/tags/v2.1.0.zip",
"repository": "https://github.com/mbachorik/spec-kit-jira",
"homepage": "https://github.com/mbachorik/spec-kit-jira",
"documentation": "https://github.com/mbachorik/spec-kit-jira/blob/main/README.md",
"changelog": "https://github.com/mbachorik/spec-kit-jira/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"issue-tracking",
"jira",
"atlassian",
"project-management"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-05T00:00:00Z",
"updated_at": "2026-03-05T00:00:00Z"
},
"ralph": {
"name": "Ralph Loop",
"id": "ralph",
"description": "Autonomous implementation loop using AI agent CLI.",
"author": "Rubiss",
"version": "1.0.0",
"download_url": "https://github.com/Rubiss/spec-kit-ralph/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/Rubiss/spec-kit-ralph",
"homepage": "https://github.com/Rubiss/spec-kit-ralph",
"documentation": "https://github.com/Rubiss/spec-kit-ralph/blob/main/README.md",
"changelog": "https://github.com/Rubiss/spec-kit-ralph/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "copilot",
"required": true
},
{
"name": "git",
"required": true
}
]
},
"provides": {
"commands": 2,
"hooks": 1
},
"tags": ["implementation", "automation", "loop", "copilot"],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-09T00:00:00Z",
"updated_at": "2026-03-09T00:00:00Z"
},
"retrospective": {
"name": "Retrospective Extension",
"id": "retrospective",
"description": "Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates.",
"author": "emi-dm",
"version": "1.0.0",
"download_url": "https://github.com/emi-dm/spec-kit-retrospective/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/emi-dm/spec-kit-retrospective",
"homepage": "https://github.com/emi-dm/spec-kit-retrospective",
"documentation": "https://github.com/emi-dm/spec-kit-retrospective/blob/main/README.md",
"changelog": "https://github.com/emi-dm/spec-kit-retrospective/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"retrospective",
"spec-drift",
"quality",
"analysis",
"governance"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-02-24T00:00:00Z",
"updated_at": "2026-02-24T00:00:00Z"
},
"review": {
"name": "Review Extension",
"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",
"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",
"changelog": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 7,
"hooks": 1
},
"tags": ["code-review", "quality", "review", "testing", "error-handling", "type-design", "simplification"],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-06T00:00:00Z",
"updated_at": "2026-03-06T00:00:00Z"
},
"sync": {
"name": "Spec Sync",
"id": "sync",
"description": "Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval.",
"author": "bgervin",
"version": "0.1.0",
"download_url": "https://github.com/bgervin/spec-kit-sync/archive/refs/tags/v0.1.0.zip",
"repository": "https://github.com/bgervin/spec-kit-sync",
"homepage": "https://github.com/bgervin/spec-kit-sync",
"documentation": "https://github.com/bgervin/spec-kit-sync/blob/main/README.md",
"changelog": "https://github.com/bgervin/spec-kit-sync/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 5,
"hooks": 1
},
"tags": [
"sync",
"drift",
"validation",
"bidirectional",
"backfill"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-02T00:00:00Z",
"updated_at": "2026-03-02T00:00:00Z"
},
"understanding": {
"name": "Understanding",
"id": "understanding",
"description": "Automated requirements quality analysis — validates specs against IEEE/ISO standards using 31 deterministic metrics. Catches ambiguity, missing testability, and structural issues before they reach implementation. Includes experimental energy-based ambiguity detection using local LM token perplexity.",
"author": "Ladislav Bihari",
"version": "3.4.0",
"download_url": "https://github.com/Testimonial/understanding/archive/refs/tags/v3.4.0.zip",
"repository": "https://github.com/Testimonial/understanding",
"homepage": "https://github.com/Testimonial/understanding",
"documentation": "https://github.com/Testimonial/understanding/blob/main/extension/README.md",
"changelog": "https://github.com/Testimonial/understanding/blob/main/extension/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "understanding",
"version": ">=3.4.0",
"required": true
}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"quality",
"metrics",
"requirements",
"validation",
"readability",
"IEEE-830",
"ISO-29148"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-07T00:00:00Z",
"updated_at": "2026-03-07T00:00:00Z"
},
"v-model": {
"name": "V-Model Extension Pack",
"id": "v-model",
"description": "Enforces V-Model paired generation of development specs and test specs with full traceability.",
"author": "leocamello",
"version": "0.4.0",
"download_url": "https://github.com/leocamello/spec-kit-v-model/archive/refs/tags/v0.4.0.zip",
"repository": "https://github.com/leocamello/spec-kit-v-model",
"homepage": "https://github.com/leocamello/spec-kit-v-model",
"documentation": "https://github.com/leocamello/spec-kit-v-model/blob/main/README.md",
"changelog": "https://github.com/leocamello/spec-kit-v-model/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 9,
"hooks": 1
},
"tags": [
"v-model",
"traceability",
"testing",
"compliance",
"safety-critical"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-02-20T00:00:00Z",
"updated_at": "2026-02-22T00:00:00Z"
},
"verify": {
"name": "Verify Extension",
"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",
"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",
"changelog": "https://github.com/ismaelJimenez/spec-kit-verify/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"verification",
"quality-gate",
"implementation",
"spec-adherence",
"compliance"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-03T00:00:00Z",
"updated_at": "2026-03-03T00:00:00Z"
}
}
}

21
extensions/catalog.json Normal file
View File

@@ -0,0 +1,21 @@
{
"schema_version": "1.0",
"updated_at": "2026-03-10T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
"extensions": {
"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

@@ -0,0 +1,69 @@
---
description: "Validate the lifecycle of an extension from the catalog."
---
# Extension Self-Test: `$ARGUMENTS`
This command drives a self-test simulating the developer experience with the `$ARGUMENTS` extension.
## Goal
Validate the end-to-end lifecycle (discovery, installation, registration) for the extension: `$ARGUMENTS`.
If `$ARGUMENTS` is empty, you must tell the user to provide an extension name, for example: `/speckit.selftest.extension linear`.
## Steps
### Step 1: Catalog Discovery Validation
Check if the extension exists in the Spec Kit catalog.
Execute this command and verify that it completes successfully and that the returned extension ID exactly matches `$ARGUMENTS`. If the command fails or the ID does not match `$ARGUMENTS`, fail the test.
```bash
specify extension info "$ARGUMENTS"
```
### Step 2: Simulate Installation
First, try to add the extension to the current workspace configuration directly. If the catalog provides the extension as `install_allowed: false` (discovery-only), this step is *expected* to fail.
```bash
specify extension add "$ARGUMENTS"
```
Then, simulate adding the extension by installing it from its catalog download URL, which should bypass the restriction.
Obtain the extension's `download_url` from the catalog metadata (for example, via a catalog info command or UI), then run:
```bash
specify extension add "$ARGUMENTS" --from "<download_url>"
```
### Step 3: Registration Verification
Once the `add` command completes, verify the installation by checking the project configuration.
Use terminal tools (like `cat`) to verify that the following file contains a record for `$ARGUMENTS`.
```bash
cat .specify/extensions/.registry/$ARGUMENTS.json
```
### Step 4: Verification Report
Analyze the standard output of the three steps.
Generate a terminal-style test output format detailing the results of discovery, installation, and registration. Return this directly to the user.
Example output format:
```text
============================= test session starts ==============================
collected 3 items
test_selftest_discovery.py::test_catalog_search [PASS/FAIL]
Details: [Provide execution result of specify extension search]
test_selftest_installation.py::test_extension_add [PASS/FAIL]
Details: [Provide execution result of specify extension add]
test_selftest_registration.py::test_config_verification [PASS/FAIL]
Details: [Provide execution result of registry record verification]
============================== [X] passed in ... ==============================
```

View File

@@ -0,0 +1,16 @@
schema_version: "1.0"
extension:
id: selftest
name: Spec Kit Self-Test Utility
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
license: MIT
requires:
speckit_version: ">=0.2.0"
provides:
commands:
- name: speckit.selftest.extension
file: commands/selftest.md
description: Validate the lifecycle of an extension from the catalog.

39
extensions/template/.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# Local configuration overrides
*-config.local.yml
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Build artifacts
dist/
build/
*.egg-info/
# Temporary files
*.tmp
.cache/

View File

@@ -0,0 +1,39 @@
# Changelog
All notable changes to this extension will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Planned
- Feature ideas for future versions
- Enhancements
- Bug fixes
## [1.0.0] - YYYY-MM-DD
### Added
- Initial release of extension
- Command: `/speckit.my-extension.example` - Example command functionality
- Configuration system with template
- Documentation and examples
### Features
- Feature 1 description
- Feature 2 description
- Feature 3 description
### Requirements
- Spec Kit: >=0.1.0
- External dependencies (if any)
---
[Unreleased]: https://github.com/your-org/spec-kit-my-extension/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/your-org/spec-kit-my-extension/releases/tag/v1.0.0

View File

@@ -0,0 +1,158 @@
# EXAMPLE: Extension README
This is an example of what your extension README should look like after customization.
**Delete this file and replace README.md with content similar to this.**
---
# My Extension
<!-- CUSTOMIZE: Replace with your extension description -->
Brief description of what your extension does and why it's useful.
## Features
<!-- CUSTOMIZE: List key features -->
- Feature 1: Description
- Feature 2: Description
- Feature 3: Description
## Installation
```bash
# Install from catalog
specify extension add my-extension
# Or install from local development directory
specify extension add --dev /path/to/my-extension
```
## Configuration
1. Create configuration file:
```bash
cp .specify/extensions/my-extension/config-template.yml \
.specify/extensions/my-extension/my-extension-config.yml
```
2. Edit configuration:
```bash
vim .specify/extensions/my-extension/my-extension-config.yml
```
3. Set required values:
<!-- CUSTOMIZE: List required configuration -->
```yaml
connection:
url: "https://api.example.com"
api_key: "your-api-key"
project:
id: "your-project-id"
```
## Usage
<!-- CUSTOMIZE: Add usage examples -->
### Command: example
Description of what this command does.
```bash
# In Claude Code
> /speckit.my-extension.example
```
**Prerequisites**:
- Prerequisite 1
- Prerequisite 2
**Output**:
- What this command produces
- Where results are saved
## Configuration Reference
<!-- CUSTOMIZE: Document all configuration options -->
### Connection Settings
| Setting | Type | Required | Description |
|---------|------|----------|-------------|
| `connection.url` | string | Yes | API endpoint URL |
| `connection.api_key` | string | Yes | API authentication key |
### Project Settings
| Setting | Type | Required | Description |
|---------|------|----------|-------------|
| `project.id` | string | Yes | Project identifier |
| `project.workspace` | string | No | Workspace or organization |
## Environment Variables
Override configuration with environment variables:
```bash
# Override connection settings
export SPECKIT_MY_EXTENSION_CONNECTION_URL="https://custom-api.com"
export SPECKIT_MY_EXTENSION_CONNECTION_API_KEY="custom-key"
```
## Examples
<!-- CUSTOMIZE: Add real-world examples -->
### Example 1: Basic Workflow
```bash
# Step 1: Create specification
> /speckit.spec
# Step 2: Generate tasks
> /speckit.tasks
# Step 3: Use extension
> /speckit.my-extension.example
```
## Troubleshooting
<!-- CUSTOMIZE: Add common issues -->
### Issue: Configuration not found
**Solution**: Create config from template (see Configuration section)
### Issue: Command not available
**Solutions**:
1. Check extension is installed: `specify extension list`
2. Restart AI agent
3. Reinstall extension
## License
MIT License - see LICENSE file
## Support
- **Issues**: <https://github.com/your-org/spec-kit-my-extension/issues>
- **Spec Kit Docs**: <https://github.com/statsperform/spec-kit>
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history.
---
*Extension Version: 1.0.0*
*Spec Kit: >=0.1.0*

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 [Your Name or Organization]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,79 @@
# Extension Template
Starter template for creating a Spec Kit extension.
## Quick Start
1. **Copy this template**:
```bash
cp -r extensions/template my-extension
cd my-extension
```
2. **Customize `extension.yml`**:
- Change extension ID, name, description
- Update author and repository
- Define your commands
3. **Create commands**:
- Add command files in `commands/` directory
- Use Markdown format with YAML frontmatter
4. **Create config template**:
- Define configuration options
- Document all settings
5. **Write documentation**:
- Update README.md with usage instructions
- Add examples
6. **Test locally**:
```bash
cd /path/to/spec-kit-project
specify extension add --dev /path/to/my-extension
```
7. **Publish** (optional):
- Create GitHub repository
- Create release
- Submit to catalog (see EXTENSION-PUBLISHING-GUIDE.md)
## Files in This Template
- `extension.yml` - Extension manifest (CUSTOMIZE THIS)
- `config-template.yml` - Configuration template (CUSTOMIZE THIS)
- `commands/example.md` - Example command (REPLACE THIS)
- `README.md` - Extension documentation (REPLACE THIS)
- `LICENSE` - MIT License (REVIEW THIS)
- `CHANGELOG.md` - Version history (UPDATE THIS)
- `.gitignore` - Git ignore rules
## Customization Checklist
- [ ] Update `extension.yml` with your extension details
- [ ] Change extension ID to your extension name
- [ ] Update author information
- [ ] Define your commands
- [ ] Create command files in `commands/`
- [ ] Update config template
- [ ] Write README with usage instructions
- [ ] Add examples
- [ ] Update LICENSE if needed
- [ ] Test extension locally
- [ ] Create git repository
- [ ] Create first release
## Need Help?
- **Development Guide**: See EXTENSION-DEVELOPMENT-GUIDE.md
- **API Reference**: See EXTENSION-API-REFERENCE.md
- **Publishing Guide**: See EXTENSION-PUBLISHING-GUIDE.md
- **User Guide**: See EXTENSION-USER-GUIDE.md
## Template Version
- Version: 1.0.0
- Last Updated: 2026-01-28
- Compatible with Spec Kit: >=0.1.0

View File

@@ -0,0 +1,210 @@
---
description: "Example command that demonstrates extension functionality"
# CUSTOMIZE: List MCP tools this command uses
tools:
- 'example-mcp-server/example_tool'
---
# Example Command
<!-- CUSTOMIZE: Replace this entire file with your command documentation -->
This is an example command that demonstrates how to create commands for Spec Kit extensions.
## Purpose
Describe what this command does and when to use it.
## Prerequisites
List requirements before using this command:
1. Prerequisite 1 (e.g., "MCP server configured")
2. Prerequisite 2 (e.g., "Configuration file exists")
3. Prerequisite 3 (e.g., "Valid API credentials")
## User Input
$ARGUMENTS
## Steps
### Step 1: Load Configuration
<!-- CUSTOMIZE: Replace with your actual steps -->
Load extension configuration from the project:
``bash
config_file=".specify/extensions/my-extension/my-extension-config.yml"
if [ ! -f "$config_file" ]; then
echo "❌ Error: Configuration not found at $config_file"
echo "Run 'specify extension add my-extension' to install and configure"
exit 1
fi
# Read configuration values
setting_value=$(yq eval '.settings.key' "$config_file")
# Apply environment variable overrides
setting_value="${SPECKIT_MY_EXTENSION_KEY:-$setting_value}"
# Validate configuration
if [ -z "$setting_value" ]; then
echo "❌ Error: Configuration value not set"
echo "Edit $config_file and set 'settings.key'"
exit 1
fi
echo "📋 Configuration loaded: $setting_value"
``
### Step 2: Perform Main Action
<!-- CUSTOMIZE: Replace with your command logic -->
Describe what this step does:
``markdown
Use MCP tools to perform the main action:
- Tool: example-mcp-server example_tool
- Parameters: { "key": "$setting_value" }
This calls the MCP server tool to execute the operation.
``
### Step 3: Process Results
<!-- CUSTOMIZE: Add more steps as needed -->
Process the results and provide output:
`` bash
echo ""
echo "✅ Command completed successfully!"
echo ""
echo "Results:"
echo " • Item 1: Value"
echo " • Item 2: Value"
echo ""
``
### Step 4: Save Output (Optional)
Save results to a file if needed:
``bash
output_file=".specify/my-extension-output.json"
cat > "$output_file" <<EOF
{
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"setting": "$setting_value",
"results": []
}
EOF
echo "💾 Output saved to $output_file"
``
## Configuration Reference
<!-- CUSTOMIZE: Document configuration options -->
This command uses the following configuration from `my-extension-config.yml`:
- **settings.key**: Description of what this setting does
- Type: string
- Required: Yes
- Example: `"example-value"`
- **settings.another_key**: Description of another setting
- Type: boolean
- Required: No
- Default: `false`
- Example: `true`
## Environment Variables
<!-- CUSTOMIZE: Document environment variable overrides -->
Configuration can be overridden with environment variables:
- `SPECKIT_MY_EXTENSION_KEY` - Overrides `settings.key`
- `SPECKIT_MY_EXTENSION_ANOTHER_KEY` - Overrides `settings.another_key`
Example:
``bash
export SPECKIT_MY_EXTENSION_KEY="override-value"
``
## Troubleshooting
<!-- CUSTOMIZE: Add common issues and solutions -->
### "Configuration not found"
**Solution**: Install the extension and create configuration:
``bash
specify extension add my-extension
cp .specify/extensions/my-extension/config-template.yml \
.specify/extensions/my-extension/my-extension-config.yml
``
### "MCP tool not available"
**Solution**: Ensure MCP server is configured in your AI agent settings.
### "Permission denied"
**Solution**: Check credentials and permissions in the external service.
## Notes
<!-- CUSTOMIZE: Add helpful notes and tips -->
- This command requires an active connection to the external service
- Results are cached for performance
- Re-run the command to refresh data
## Examples
<!-- CUSTOMIZE: Add usage examples -->
### Example 1: Basic Usage
``bash
# Run with default configuration
>
> /speckit.my-extension.example
``
### Example 2: With Environment Override
``bash
# Override configuration with environment variable
export SPECKIT_MY_EXTENSION_KEY="custom-value"
> /speckit.my-extension.example
``
### Example 3: After Core Command
``bash
# Use as part of a workflow
>
> /speckit.tasks
> /speckit.my-extension.example
``
---
*For more information, see the extension README or run `specify extension info my-extension`*

View File

@@ -0,0 +1,75 @@
# Extension Configuration Template
# Copy this to my-extension-config.yml and customize for your project
# CUSTOMIZE: Add your configuration sections below
# Example: Connection settings
connection:
# URL to external service
url: "" # REQUIRED: e.g., "https://api.example.com"
# API key or token
api_key: "" # REQUIRED: Your API key
# Example: Project settings
project:
# Project identifier
id: "" # REQUIRED: e.g., "my-project"
# Workspace or organization
workspace: "" # OPTIONAL: e.g., "my-org"
# Example: Feature flags
features:
# Enable/disable main functionality
enabled: true
# Automatic synchronization
auto_sync: false
# Verbose logging
verbose: false
# Example: Default values
defaults:
# Labels to apply
labels: [] # e.g., ["automated", "spec-kit"]
# Priority level
priority: "medium" # Options: "low", "medium", "high"
# Assignee
assignee: "" # OPTIONAL: Default assignee
# Example: Field mappings
# Map internal names to external field IDs
field_mappings:
# Example mappings
# internal_field: "external_field_id"
# status: "customfield_10001"
# Example: Advanced settings
advanced:
# Timeout in seconds
timeout: 30
# Retry attempts
retry_count: 3
# Cache duration in seconds
cache_duration: 3600
# Environment Variable Overrides:
# You can override any setting with environment variables using this pattern:
# SPECKIT_MY_EXTENSION_{SECTION}_{KEY}
#
# Examples:
# - SPECKIT_MY_EXTENSION_CONNECTION_API_KEY: Override connection.api_key
# - SPECKIT_MY_EXTENSION_PROJECT_ID: Override project.id
# - SPECKIT_MY_EXTENSION_FEATURES_ENABLED: Override features.enabled
#
# Note: Use uppercase and replace dots with underscores
# Local Overrides:
# For local development, create my-extension-config.local.yml (gitignored)
# to override settings without affecting the team configuration

View File

@@ -0,0 +1,97 @@
schema_version: "1.0"
extension:
# CUSTOMIZE: Change 'my-extension' to your extension ID (lowercase, hyphen-separated)
id: "my-extension"
# CUSTOMIZE: Human-readable name for your extension
name: "My Extension"
# CUSTOMIZE: Update version when releasing (semantic versioning: X.Y.Z)
version: "1.0.0"
# CUSTOMIZE: Brief description (under 200 characters)
description: "Brief description of what your extension does"
# CUSTOMIZE: Your name or organization name
author: "Your Name"
# CUSTOMIZE: GitHub repository URL (create before publishing)
repository: "https://github.com/your-org/spec-kit-my-extension"
# REVIEW: License (MIT is recommended for open source)
license: "MIT"
# CUSTOMIZE: Extension homepage (can be same as repository)
homepage: "https://github.com/your-org/spec-kit-my-extension"
# Requirements for this extension
requires:
# CUSTOMIZE: Minimum spec-kit version required
# Use >=X.Y.Z for minimum version
# Use >=X.Y.Z,<Y.0.0 for version range
speckit_version: ">=0.1.0"
# CUSTOMIZE: Add MCP tools or other dependencies
# Remove if no external tools required
tools:
- name: "example-mcp-server"
version: ">=1.0.0"
required: true
# Commands provided by this extension
provides:
commands:
# CUSTOMIZE: Define your commands
# Pattern: speckit.{extension-id}.{command-name}
- name: "speckit.my-extension.example"
file: "commands/example.md"
description: "Example command that demonstrates functionality"
# Optional: Add aliases for shorter command names
aliases: ["speckit.example"]
# ADD MORE COMMANDS: Copy this block for each command
# - name: "speckit.my-extension.another-command"
# file: "commands/another-command.md"
# description: "Another command"
# CUSTOMIZE: Define configuration files
config:
- name: "my-extension-config.yml"
template: "config-template.yml"
description: "Extension configuration"
required: true # Set to false if config is optional
# CUSTOMIZE: Define hooks (optional)
# Remove if no hooks needed
hooks:
# Hook that runs after /speckit.tasks
after_tasks:
command: "speckit.my-extension.example"
optional: true # User will be prompted
prompt: "Run example command?"
description: "Demonstrates hook functionality"
condition: null # Future: conditional execution
# ADD MORE HOOKS: Copy this block for other events
# after_implement:
# command: "speckit.my-extension.another"
# optional: false # Auto-execute without prompting
# description: "Runs automatically after implementation"
# CUSTOMIZE: Add relevant tags (2-5 recommended)
# Used for discovery in catalog
tags:
- "example"
- "template"
# ADD MORE: "category", "tool-name", etc.
# CUSTOMIZE: Default configuration values (optional)
# These are merged with user config
defaults:
# Example default values
feature:
enabled: true
auto_sync: false
# ADD MORE: Any default settings for your extension

View File

@@ -0,0 +1,54 @@
# Spec Kit - February 2026 Newsletter
This edition covers Spec Kit activity in February 2026. Versions v0.1.7 through v0.1.13 shipped during the month, addressing bugs and adding features including a dual-catalog extension system and additional agent integrations. Community activity included blog posts, tutorials, and meetup sessions. A category summary is in the table below, followed by details.
| **Spec Kit Core (Feb 2026)** | **Community & Content** | **Roadmap & Next** |
| --- | --- | --- |
| Versions **v0.1.7** through **v0.1.13** shipped with bug fixes and features, including a **dual-catalog extension system** and new agent integrations. Over 300 issues were closed (of ~800 filed). The repo reached 71k stars and 6.4k forks. [\[github.com\]](https://github.com/github/spec-kit/releases) [\[github.com\]](https://github.com/github/spec-kit/issues) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit) | Eduardo Luz published a LinkedIn article on SDD and Spec Kit [\[linkedin.com\]](https://www.linkedin.com/pulse/specification-driven-development-sdd-github-spec-kit-elevating-luz-tojmc?tl=en). Erick Matsen blogged a walkthrough of building a bioinformatics pipeline with Spec Kit [\[matsen.fredhutch.org\]](https://matsen.fredhutch.org/general/2026/02/10/spec-kit-walkthrough.html). Microsoft MVP [Eric Boyd](https://ericboyd.com/) (not the Microsoft AI Platform VP of the same name) presented at the Cleveland .NET User Group [\[ericboyd.com\]](https://ericboyd.com/events/cleveland-csharp-user-group-february-25-2026-spec-driven-development-sdd-github-spec-kit). | **v0.2.0** was released in early March, consolidating February's work. It added extensions for Jira and Azure DevOps, community plugin support, and agents for Tabnine CLI and Kiro CLI [\[github.com\]](https://github.com/github/spec-kit/releases). Future work includes spec lifecycle management and progress toward a stable 1.0 release [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html). |
***
## Spec Kit Project Updates
Spec Kit released versions **v0.1.7** through **v0.1.13** during February. Version 0.1.7 (early February) updated documentation for the newly introduced **dual-catalog extension system**, which allows both core and community extension catalogs to coexist. Subsequent patches (0.1.8, 0.1.9, etc.) bumped dependencies such as GitHub Actions versions and resolved minor issues. **v0.1.10** fixed YAML front-matter handling in generated files. By late February, **v0.1.12** and **v0.1.13** shipped with additional fixes in preparation for the next version bump. [\[github.com\]](https://github.com/github/spec-kit/releases)
The main architectural addition was the **modular extension system** with separate "core" and "community" extension catalogs for third-party add-ons. Multiple community-contributed extensions were merged during the month, including a **Jira extension** for issue tracker integration, an **Azure DevOps extension**, and utility extensions for code review, retrospective documentation, and CI/CD sync. The pending 0.2.0 release changelog lists over a dozen changes from February, including the extension additions and support for **multiple agent catalogs concurrently**. [\[github.com\]](https://github.com/github/spec-kit/releases)
By end of February, **over 330 issues/feature requests had been closed on GitHub** (out of ~870 filed to date). External contributors submitted pull requests including the **Tabnine CLI support**, which was merged in late February. The repository reached ~71k stars and crossed 6,000 forks. [\[github.com\]](https://github.com/github/spec-kit/issues) [\[github.com\]](https://github.com/github/spec-kit/releases) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit)
On the stability side, February's work focused on tightening core workflows and fixing edge-case bugs in the specification, planning, and task-generation commands. The team addressed file-handling issues (e.g., clarifying how output files are created/appended) and improved the reliability of the automated release pipeline. The project also added **Kiro CLI** to the supported agent list and updated integration scripts for Cursor and Code Interpreter, bringing the total number of supported AI coding assistants to over 20. [\[github.com\]](https://github.com/github/spec-kit/releases) [\[github.com\]](https://github.com/github/spec-kit)
## Community & Content
**Eduardo Luz** published a LinkedIn article on Feb 15 titled *"Specification Driven Development (SDD) and the GitHub Spec Kit: Elevating Software Engineering."* The article draws on his experience as a senior engineer to describe common causes of technical debt and inconsistent designs, and how SDD addresses them. It walks through Spec Kit's **four-layer approach** (Constitution, Design, Tasks, Implementation) and discusses treating specifications as a source of truth. The post generated discussion among software architects on LinkedIn about reducing misunderstandings and rework through spec-driven workflows. [\[linkedin.com\]](https://www.linkedin.com/pulse/specification-driven-development-sdd-github-spec-kit-elevating-luz-tojmc?tl=en)
**Erick Matsen** (Fred Hutchinson Cancer Center) posted a detailed walkthrough on Feb 10 titled *"Spec-Driven Development with spec-kit."* He describes building a **bioinformatics pipeline** in a single day using Spec Kit's workflow (from `speckit.constitution` to `speckit.implement`). The post includes command outputs and notes on decisions made along the way, such as refining the spec to add domain-specific requirements. He writes: "I really recommend this approach. This feels like the way software development should be." [\[matsen.fredhutch.org\]](https://matsen.fredhutch.org/general/2026/02/10/spec-kit-walkthrough.html) [\[github.com\]](https://github.com/mnriem/spec-kit-dotnet-cli-demo)
Several other tutorials and guides appeared during the month. An article on *IntuitionLabs* (updated Feb 21) provided a guide to Spec Kit covering the philosophy behind SDD and a walkthrough of the four-phase workflow with examples. A piece by Ry Walker (Feb 22) summarized key aspects of Spec Kit, noting its agent-agnostic design and 71k-star count. Microsoft's Developer Blog post from late 2025 (*"Diving Into Spec-Driven Development with GitHub Spec Kit"* by Den Delimarsky) continued to circulate among new users. [\[intuitionlabs.ai\]](https://intuitionlabs.ai/articles/spec-driven-development-spec-kit) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit)
On **Feb 25**, the Cleveland C# .NET User Group hosted a session titled *"Spec Driven Development with GitHub Spec Kit."* The talk was delivered by Microsoft MVP **[Eric Boyd](https://ericboyd.com/)** (Cleveland-based .NET developer; not to be confused with the Microsoft AI Platform VP of the same name). Boyd covered how specs change an AI coding assistant's output, patterns for iterating and refining specs over multiple cycles, and moving from ad-hoc prompting to a repeatable spec-driven workflow. Other groups, including GDG Madison, also listed sessions on spec-driven development in late February and early March. [\[ericboyd.com\]](https://ericboyd.com/events/cleveland-csharp-user-group-february-25-2026-spec-driven-development-sdd-github-spec-kit)
On GitHub, the **Spec Kit Discussions forum** saw activity around installation troubleshooting, handling multi-feature projects with Spec Kit's branching model, and feature suggestions. One thread discussed how Spec Kit treats each spec as a short-lived artifact tied to a feature branch, which led to discussion about future support for long-running "spec of record" use cases. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
## SDD Ecosystem
Other spec-driven development tools also saw activity in February.
AWS **Kiro** released version 0.10 on Feb 18 with two new spec workflows: a **Design-First** mode (starting from architecture/pseudocode to derive requirements) and a **Bugfix** mode (structured root-cause analysis producing a `bugfix.md` spec file). Kiro also added hunk-level code review for AI-generated changes and pre/post task hooks for custom automation. AWS expanded Kiro to GovCloud regions on Feb 17 for government compliance use cases. [\[kiro.dev\]](https://kiro.dev/changelog/)
**OpenSpec** (by Fission AI), a lightweight SDD framework, reached ~29.3k stars and nearly 2k forks. Its community published guides and comparisons during the month, including *"Spec-Driven Development Made Easy: A Practical Guide with OpenSpec."* OpenSpec emphasizes simplicity and flexibility, integrating with multiple AI coding assistants via YAML configs.
**Tessl** remained in private beta. As described by Thoughtworks writer Birgitta Boeckeler, Tessl pursues a **spec-as-source** model where specifications are maintained long-term and directly generate code files one-to-one, with generated code labeled as "do not edit." This contrasts with Spec Kit's current approach of creating specs per feature/branch. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
An **arXiv preprint** (January 2026) categorized SDD implementations into three levels: *spec-first*, *spec-anchored*, and *spec-as-source*. Spec Kit was identified as primarily spec-first with elements of spec-anchored. Tech media published reviews including a *Vibe Coding* "GitHub Spec Kit Review (2026)" and a blog post titled *"Putting Spec Kit Through Its Paces: Radical Idea or Reinvented Waterfall?"* which concluded that SDD with AI assistance is more iterative than traditional Waterfall. [\[intuitionlabs.ai\]](https://intuitionlabs.ai/articles/spec-driven-development-spec-kit) [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
## Roadmap
**v0.2.0** was released on March 10, 2026, consolidating the month's work. It includes new extensions (Jira, Azure DevOps, review, sync), support for multiple extension catalogs and community plugins, and additional agent integrations (Tabnine CLI, Kiro CLI). [\[github.com\]](https://github.com/github/spec-kit/releases)
Areas under discussion or in progress for future development:
- **Spec lifecycle management** -- supporting longer-lived specifications that can evolve across multiple iterations, rather than being tied to a single feature branch. Users have raised this in GitHub Discussions, and the concept of "spec-anchored" development is under consideration. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
- **CI/CD integration** -- incorporating Spec Kit verification (e.g., `speckit.checklist` or `speckit.verify`) into pull request workflows and project management tools. February's Jira and Azure DevOps extensions are a step in this direction. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **Continued agent support** -- adding integrations as new AI coding assistants emerge. The project currently supports over 20 agents and has been adding new ones (Kiro CLI, Tabnine CLI) as they become available. [\[github.com\]](https://github.com/github/spec-kit)
- **Community ecosystem** -- the open extension model allows external contributors to add functionality directly. February's Jira and Azure DevOps plugins were community-contributed. The Spec Kit README now links to community walkthrough demos for .NET, Spring Boot, and other stacks. [\[github.com\]](https://github.com/github/spec-kit)

157
presets/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,157 @@
# Preset System Architecture
This document describes the internal architecture of the preset system — how template resolution, command registration, and catalog management work under the hood.
For usage instructions, see [README.md](README.md).
## Template Resolution
When Spec Kit needs a template (e.g. `spec-template`), the `PresetResolver` walks a priority stack and returns the first match:
```mermaid
flowchart TD
A["resolve_template('spec-template')"] --> B{Override exists?}
B -- Yes --> C[".specify/templates/overrides/spec-template.md"]
B -- No --> D{Preset provides it?}
D -- Yes --> E[".specify/presets/preset-id/templates/spec-template.md"]
D -- No --> F{Extension provides it?}
F -- Yes --> G[".specify/extensions/ext-id/templates/spec-template.md"]
F -- No --> H[".specify/templates/spec-template.md"]
E -- "multiple presets?" --> I["lowest priority number wins"]
I --> E
style C fill:#4caf50,color:#fff
style E fill:#2196f3,color:#fff
style G fill:#ff9800,color:#fff
style H fill:#9e9e9e,color:#fff
```
| Priority | Source | Path | Use case |
|----------|--------|------|----------|
| 1 (highest) | Override | `.specify/templates/overrides/` | One-off project-local tweaks |
| 2 | Preset | `.specify/presets/<id>/templates/` | Shareable, stackable customizations |
| 3 | Extension | `.specify/extensions/<id>/templates/` | Extension-provided templates |
| 4 (lowest) | Core | `.specify/templates/` | Shipped defaults |
When multiple presets are installed, they're sorted by their `priority` field (lower number = higher precedence). This is set via `--priority` on `specify preset add`.
The resolution is implemented three times to ensure consistency:
- **Python**: `PresetResolver` in `src/specify_cli/presets.py`
- **Bash**: `resolve_template()` in `scripts/bash/common.sh`
- **PowerShell**: `Resolve-Template` in `scripts/powershell/common.ps1`
## Command Registration
When a preset is installed with `type: "command"` entries, the `PresetManager` registers them into all detected agent directories using the shared `CommandRegistrar` from `src/specify_cli/agents.py`.
```mermaid
flowchart TD
A["specify preset add my-preset"] --> B{Preset has type: command?}
B -- No --> Z["done (templates only)"]
B -- Yes --> C{Extension command?}
C -- "speckit.myext.cmd\n(3+ dot segments)" --> D{Extension installed?}
D -- No --> E["skip (extension not active)"]
D -- Yes --> F["register command"]
C -- "speckit.specify\n(core command)" --> F
F --> G["detect agent directories"]
G --> H[".claude/commands/"]
G --> I[".gemini/commands/"]
G --> J[".github/agents/"]
G --> K["... (17+ agents)"]
H --> L["write .md (Markdown format)"]
I --> M["write .toml (TOML format)"]
J --> N["write .agent.md + .prompt.md"]
style E fill:#ff5722,color:#fff
style L fill:#4caf50,color:#fff
style M fill:#4caf50,color:#fff
style N fill:#4caf50,color:#fff
```
### Extension safety check
Command names follow the pattern `speckit.<ext-id>.<cmd-name>`. When a command has 3+ dot segments, the system extracts the extension ID and checks if `.specify/extensions/<ext-id>/` exists. If the extension isn't installed, the command is skipped — preventing orphan files referencing non-existent extensions.
Core commands (e.g. `speckit.specify`, with only 2 segments) are always registered.
### Agent format rendering
The `CommandRegistrar` renders commands differently per agent:
| Agent | Format | Extension | Arg placeholder |
|-------|--------|-----------|-----------------|
| Claude, Cursor, opencode, Windsurf, etc. | Markdown | `.md` | `$ARGUMENTS` |
| Copilot | Markdown | `.agent.md` + `.prompt.md` | `$ARGUMENTS` |
| Gemini, Qwen, Tabnine | TOML | `.toml` | `{{args}}` |
### Cleanup on removal
When `specify preset remove` is called, the registered commands are read from the registry metadata and the corresponding files are deleted from each agent directory, including Copilot companion `.prompt.md` files.
## Catalog System
```mermaid
flowchart TD
A["specify preset search"] --> B["PresetCatalog.get_active_catalogs()"]
B --> C{SPECKIT_PRESET_CATALOG_URL set?}
C -- Yes --> D["single custom catalog"]
C -- No --> E{.specify/preset-catalogs.yml exists?}
E -- Yes --> F["project-level catalog stack"]
E -- No --> G{"~/.specify/preset-catalogs.yml exists?"}
G -- Yes --> H["user-level catalog stack"]
G -- No --> I["built-in defaults"]
I --> J["default (install allowed)"]
I --> K["community (discovery only)"]
style D fill:#ff9800,color:#fff
style F fill:#2196f3,color:#fff
style H fill:#2196f3,color:#fff
style J fill:#4caf50,color:#fff
style K fill:#9e9e9e,color:#fff
```
Catalogs are fetched with a 1-hour cache (per-URL, SHA256-hashed cache files). Each catalog entry has a `priority` (for merge ordering) and `install_allowed` flag.
## Repository Layout
```
presets/
├── ARCHITECTURE.md # This file
├── PUBLISHING.md # Guide for submitting presets to the catalog
├── README.md # User guide
├── catalog.json # Official preset catalog
├── catalog.community.json # Community preset catalog
├── scaffold/ # Scaffold for creating new presets
│ ├── preset.yml # Example manifest
│ ├── README.md # Guide for customizing the scaffold
│ ├── commands/
│ │ ├── speckit.specify.md # Core command override example
│ │ └── speckit.myext.myextcmd.md # Extension command override example
│ └── templates/
│ ├── spec-template.md # Core template override example
│ └── myext-template.md # Extension template override example
└── self-test/ # Self-test preset (overrides all core templates)
├── preset.yml
├── commands/
│ └── speckit.specify.md
└── templates/
├── spec-template.md
├── plan-template.md
├── tasks-template.md
├── checklist-template.md
├── constitution-template.md
└── agent-file-template.md
```
## Module Structure
```
src/specify_cli/
├── agents.py # CommandRegistrar — shared infrastructure for writing
│ # command files to agent directories
├── presets.py # PresetManifest, PresetRegistry, PresetManager,
│ # PresetCatalog, PresetCatalogEntry, PresetResolver
└── __init__.py # CLI commands: specify preset list/add/remove/search/
# resolve/info, specify preset catalog list/add/remove
```

295
presets/PUBLISHING.md Normal file
View File

@@ -0,0 +1,295 @@
# Preset Publishing Guide
This guide explains how to publish your preset to the Spec Kit preset catalog, making it discoverable by `specify preset search`.
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Prepare Your Preset](#prepare-your-preset)
3. [Submit to Catalog](#submit-to-catalog)
4. [Verification Process](#verification-process)
5. [Release Workflow](#release-workflow)
6. [Best Practices](#best-practices)
---
## Prerequisites
Before publishing a preset, ensure you have:
1. **Valid Preset**: A working preset with a valid `preset.yml` manifest
2. **Git Repository**: Preset hosted on GitHub (or other public git hosting)
3. **Documentation**: README.md with description and usage instructions
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
5. **Versioning**: Semantic versioning (e.g., 1.0.0)
6. **Testing**: Preset tested on real projects with `specify preset add --dev`
---
## Prepare Your Preset
### 1. Preset Structure
Ensure your preset follows the standard structure:
```text
your-preset/
├── preset.yml # Required: Preset manifest
├── README.md # Required: Documentation
├── LICENSE # Required: License file
├── CHANGELOG.md # Recommended: Version history
├── templates/ # Template overrides
│ ├── spec-template.md
│ ├── plan-template.md
│ └── ...
└── commands/ # Command overrides (optional)
└── speckit.specify.md
```
Start from the [scaffold](scaffold/) if you're creating a new preset.
### 2. preset.yml Validation
Verify your manifest is valid:
```yaml
schema_version: "1.0"
preset:
id: "your-preset" # Unique lowercase-hyphenated ID
name: "Your Preset Name" # Human-readable name
version: "1.0.0" # Semantic version
description: "Brief description (one sentence)"
author: "Your Name or Organization"
repository: "https://github.com/your-org/spec-kit-preset-your-preset"
license: "MIT"
requires:
speckit_version: ">=0.1.0" # Required spec-kit version
provides:
templates:
- type: "template"
name: "spec-template"
file: "templates/spec-template.md"
description: "Custom spec template"
replaces: "spec-template"
tags: # 2-5 relevant tags
- "category"
- "workflow"
```
**Validation Checklist**:
-`id` is lowercase with hyphens only (no underscores, spaces, or special characters)
-`version` follows semantic versioning (X.Y.Z)
-`description` is concise (under 200 characters)
-`repository` URL is valid and public
- ✅ All template and command files exist in the preset directory
- ✅ Template names are lowercase with hyphens only
- ✅ Command names use dot notation (e.g. `speckit.specify`)
- ✅ Tags are lowercase and descriptive
### 3. Test Locally
```bash
# Install from local directory
specify preset add --dev /path/to/your-preset
# Verify templates resolve from your preset
specify preset resolve spec-template
# Verify preset info
specify preset info your-preset
# List installed presets
specify preset list
# Remove when done testing
specify preset remove your-preset
```
If your preset includes command overrides, verify they appear in the agent directories:
```bash
# Check Claude commands (if using Claude)
ls .claude/commands/speckit.*.md
# Check Copilot commands (if using Copilot)
ls .github/agents/speckit.*.agent.md
# Check Gemini commands (if using Gemini)
ls .gemini/commands/speckit.*.toml
```
### 4. Create GitHub Release
Create a GitHub release for your preset version:
```bash
# Tag the release
git tag v1.0.0
git push origin v1.0.0
```
The release archive URL will be:
```text
https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip
```
### 5. Test Installation from Archive
```bash
specify preset add --from https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip
```
---
## Submit to Catalog
### Understanding the Catalogs
Spec Kit uses a dual-catalog system:
- **`catalog.json`** — Official, verified presets (install allowed by default)
- **`catalog.community.json`** — Community-contributed presets (discovery only by default)
All community presets should be submitted to `catalog.community.json`.
### 1. Fork the spec-kit Repository
```bash
git clone https://github.com/YOUR-USERNAME/spec-kit.git
cd spec-kit
```
### 2. Add Preset to Community Catalog
Edit `presets/catalog.community.json` and add your preset.
> **⚠️ Entries must be sorted alphabetically by preset ID.** Insert your preset in the correct position within the `"presets"` object.
```json
{
"schema_version": "1.0",
"updated_at": "2026-03-10T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
"presets": {
"your-preset": {
"name": "Your Preset Name",
"description": "Brief description of what your preset provides",
"author": "Your Name",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/your-org/spec-kit-preset-your-preset",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"templates": 3,
"commands": 1
},
"tags": [
"category",
"workflow"
],
"created_at": "2026-03-10T00:00:00Z",
"updated_at": "2026-03-10T00:00:00Z"
}
}
}
```
### 3. Submit Pull Request
```bash
git checkout -b add-your-preset
git add presets/catalog.community.json
git commit -m "Add your-preset to community catalog
- Preset ID: your-preset
- Version: 1.0.0
- Author: Your Name
- Description: Brief description
"
git push origin add-your-preset
```
**Pull Request Checklist**:
```markdown
## Preset Submission
**Preset Name**: Your Preset Name
**Preset ID**: your-preset
**Version**: 1.0.0
**Repository**: https://github.com/your-org/spec-kit-preset-your-preset
### Checklist
- [ ] Valid preset.yml manifest
- [ ] README.md with description and usage
- [ ] LICENSE file included
- [ ] GitHub release created
- [ ] Preset tested with `specify preset add --dev`
- [ ] Templates resolve correctly (`specify preset resolve`)
- [ ] Commands register to agent directories (if applicable)
- [ ] Commands match template sections (command + template are coherent)
- [ ] Added to presets/catalog.community.json
```
---
## Verification Process
After submission, maintainers will review:
1. **Manifest validation** — valid `preset.yml`, all files exist
2. **Template quality** — templates are useful and well-structured
3. **Command coherence** — commands reference sections that exist in templates
4. **Security** — no malicious content, safe file operations
5. **Documentation** — clear README explaining what the preset does
Once verified, `verified: true` is set and the preset appears in `specify preset search`.
---
## Release Workflow
When releasing a new version:
1. Update `version` in `preset.yml`
2. Update CHANGELOG.md
3. Tag and push: `git tag v1.1.0 && git push origin v1.1.0`
4. Submit PR to update `version` and `download_url` in `presets/catalog.community.json`
---
## Best Practices
### Template Design
- **Keep sections clear** — use headings and placeholder text the LLM can replace
- **Match commands to templates** — if your preset overrides a command, make sure it references the sections in your template
- **Document customization points** — use HTML comments to guide users on what to change
### Naming
- Preset IDs should be descriptive: `healthcare-compliance`, `enterprise-safe`, `startup-lean`
- Avoid generic names: `my-preset`, `custom`, `test`
### Stacking
- Design presets to work well when stacked with others
- Only override templates you need to change
- Document which templates and commands your preset modifies
### Command Overrides
- Only override commands when the workflow needs to change, not just the output format
- If you only need different template sections, a template override is sufficient
- Test command overrides with multiple agents (Claude, Gemini, Copilot)

115
presets/README.md Normal file
View File

@@ -0,0 +1,115 @@
# Presets
Presets are stackable, priority-ordered collections of template and command overrides for Spec Kit. They let you customize both the artifacts produced by the Spec-Driven Development workflow (specs, plans, tasks, checklists, constitutions) and the commands that guide the LLM in creating them — without forking or modifying core files.
## How It Works
When Spec Kit needs a template (e.g. `spec-template`), it walks a resolution stack:
1. `.specify/templates/overrides/` — project-local one-off overrides
2. `.specify/presets/<preset-id>/templates/` — installed presets (sorted by priority)
3. `.specify/extensions/<ext-id>/templates/` — extension-provided templates
4. `.specify/templates/` — core templates shipped with Spec Kit
If no preset is installed, core templates are used — exactly the same behavior as before presets existed.
For detailed resolution and command registration flows, see [ARCHITECTURE.md](ARCHITECTURE.md).
## Command Overrides
Presets can also override the commands that guide the SDD workflow. Templates define *what* gets produced (specs, plans, constitutions); commands define *how* the LLM produces them (the step-by-step instructions).
When a preset includes `type: "command"` entries, the commands are automatically registered into all detected agent directories (`.claude/commands/`, `.gemini/commands/`, etc.) in the correct format (Markdown or TOML with appropriate argument placeholders). When the preset is removed, the registered commands are cleaned up.
## Quick Start
```bash
# Search available presets
specify preset search
# Install a preset from the catalog
specify preset add healthcare-compliance
# Install from a local directory (for development)
specify preset add --dev ./my-preset
# Install with a specific priority (lower = higher precedence)
specify preset add healthcare-compliance --priority 5
# List installed presets
specify preset list
# See which template a name resolves to
specify preset resolve spec-template
# Get detailed info about a preset
specify preset info healthcare-compliance
# Remove a preset
specify preset remove healthcare-compliance
```
## Stacking Presets
Multiple presets can be installed simultaneously. The `--priority` flag controls which one wins when two presets provide the same template (lower number = higher precedence):
```bash
specify preset add enterprise-safe --priority 10 # base layer
specify preset add healthcare-compliance --priority 5 # overrides enterprise-safe
specify preset add pm-workflow --priority 1 # overrides everything
```
Presets **override**, they don't merge. If two presets both provide `spec-template`, the one with the lowest priority number wins entirely.
## Catalog Management
Presets are discovered through catalogs. By default, Spec Kit uses the official and community catalogs:
```bash
# List active catalogs
specify preset catalog list
# Add a custom catalog
specify preset catalog add https://example.com/catalog.json --name my-org --install-allowed
# Remove a catalog
specify preset catalog remove my-org
```
## Creating a Preset
See [scaffold/](scaffold/) for a scaffold you can copy to create your own preset.
1. Copy `scaffold/` to a new directory
2. Edit `preset.yml` with your preset's metadata
3. Add or replace templates in `templates/`
4. Test locally with `specify preset add --dev .`
5. Verify with `specify preset resolve spec-template`
## Environment Variables
| Variable | Description |
|----------|-------------|
| `SPECKIT_PRESET_CATALOG_URL` | Override the catalog URL (replaces all defaults) |
## Configuration Files
| File | Scope | Description |
|------|-------|-------------|
| `.specify/preset-catalogs.yml` | Project | Custom catalog stack for this project |
| `~/.specify/preset-catalogs.yml` | User | Custom catalog stack for all projects |
## Future Considerations
The following enhancements are under consideration for future releases:
- **Composition strategies** — Allow presets to declare a `strategy` per template instead of the default `replace`:
| Type | `replace` | `prepend` | `append` | `wrap` |
|------|-----------|-----------|----------|--------|
| **template** | ✓ (default) | ✓ | ✓ | ✓ |
| **command** | ✓ (default) | ✓ | ✓ | ✓ |
| **script** | ✓ (default) | — | — | ✓ |
For artifacts and commands (which are LLM directives), `wrap` would inject preset content before and after the core template using a `{CORE_TEMPLATE}` placeholder. For scripts, `wrap` would run custom logic before/after the core script via a `$CORE_SCRIPT` variable.
- **Script overrides** — Enable presets to provide alternative versions of core scripts (e.g. `create-new-feature.sh`) for workflow customization. A `strategy: "wrap"` option could allow presets to run custom logic before/after the core script without fully replacing it.

View File

@@ -0,0 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-03-09T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
"presets": {}
}

6
presets/catalog.json Normal file
View File

@@ -0,0 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-03-10T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.json",
"presets": {}
}

View File

@@ -0,0 +1,46 @@
# My Preset
A custom preset for Spec Kit. Copy this directory and customize it to create your own.
## Templates Included
| Template | Type | Description |
|----------|------|-------------|
| `spec-template` | template | Custom feature specification template (overrides core and extensions) |
| `myext-template` | template | Override of the myext extension's report template |
| `speckit.specify` | command | Custom specification command (overrides core) |
| `speckit.myext.myextcmd` | command | Override of the myext extension's myextcmd command |
## Development
1. Copy this directory: `cp -r presets/scaffold my-preset`
2. Edit `preset.yml` — set your preset's ID, name, description, and templates
3. Add or modify templates in `templates/`
4. Test locally: `specify preset add --dev ./my-preset`
5. Verify resolution: `specify preset resolve spec-template`
6. Remove when done testing: `specify preset remove my-preset`
## Manifest Reference (`preset.yml`)
Required fields:
- `schema_version` — always `"1.0"`
- `preset.id` — lowercase alphanumeric with hyphens
- `preset.name` — human-readable name
- `preset.version` — semantic version (e.g. `1.0.0`)
- `preset.description` — brief description
- `requires.speckit_version` — version constraint (e.g. `>=0.1.0`)
- `provides.templates` — list of templates with `type`, `name`, and `file`
## Template Types
- **template** — Document scaffolds (spec-template.md, plan-template.md, tasks-template.md, etc.)
- **command** — AI agent workflow prompts (e.g. speckit.specify, speckit.plan)
- **script** — Custom scripts (reserved for future use)
## Publishing
See the [Preset Publishing Guide](../PUBLISHING.md) for details on submitting to the catalog.
## License
MIT

View File

@@ -0,0 +1,20 @@
---
description: "Override of the myext extension's myextcmd command"
---
<!-- Preset override for speckit.myext.myextcmd -->
You are following a customized version of the myext extension's myextcmd command.
When executing this command:
1. Read the user's input from $ARGUMENTS
2. Follow the standard myextcmd workflow
3. Additionally, apply the following customizations from this preset:
- Add compliance checks before proceeding
- Include audit trail entries in the output
> CUSTOMIZE: Replace the instructions above with your own.
> This file overrides the command that the "myext" extension provides.
> When this preset is installed, all agents (Claude, Gemini, Copilot, etc.)
> will use this version instead of the extension's original.

View File

@@ -0,0 +1,23 @@
---
description: "Create a feature specification (preset override)"
scripts:
sh: scripts/bash/create-new-feature.sh "{ARGS}"
ps: scripts/powershell/create-new-feature.ps1 "{ARGS}"
---
## User Input
```text
$ARGUMENTS
```
Given the feature description above:
1. **Create the feature branch** by running the script:
- Bash: `{SCRIPT} --json --short-name "<short-name>" "<description>"`
- The JSON output contains BRANCH_NAME and SPEC_FILE paths.
2. **Read the spec-template** to see the sections you need to fill.
3. **Write the specification** to SPEC_FILE, replacing the placeholders in each section
(Overview, Requirements, Acceptance Criteria) with details from the user's description.

View File

@@ -0,0 +1,91 @@
schema_version: "1.0"
preset:
# CUSTOMIZE: Change 'my-preset' to your preset ID (lowercase, hyphen-separated)
id: "my-preset"
# CUSTOMIZE: Human-readable name for your preset
name: "My Preset"
# CUSTOMIZE: Update version when releasing (semantic versioning: X.Y.Z)
version: "1.0.0"
# CUSTOMIZE: Brief description (under 200 characters)
description: "Brief description of what your preset provides"
# CUSTOMIZE: Your name or organization name
author: "Your Name"
# CUSTOMIZE: GitHub repository URL (create before publishing)
repository: "https://github.com/your-org/spec-kit-preset-my-preset"
# REVIEW: License (MIT is recommended for open source)
license: "MIT"
# Requirements for this preset
requires:
# CUSTOMIZE: Minimum spec-kit version required
speckit_version: ">=0.1.0"
# Templates provided by this preset
provides:
templates:
# CUSTOMIZE: Define your template overrides
# Templates are document scaffolds (spec-template.md, plan-template.md, etc.)
- type: "template"
name: "spec-template"
file: "templates/spec-template.md"
description: "Custom feature specification template"
replaces: "spec-template" # Which core template this overrides (optional)
# ADD MORE TEMPLATES: Copy this block for each template
# - type: "template"
# name: "plan-template"
# file: "templates/plan-template.md"
# description: "Custom plan template"
# replaces: "plan-template"
# OVERRIDE EXTENSION TEMPLATES:
# Presets sit above extensions in the resolution stack, so you can
# override templates provided by any installed extension.
# For example, if the "myext" extension provides a spec-template,
# the preset's version above will take priority automatically.
# Override a template provided by the "myext" extension:
- type: "template"
name: "myext-template"
file: "templates/myext-template.md"
description: "Override myext's report template"
replaces: "myext-template"
# Command overrides (AI agent workflow prompts)
# Presets can override both core and extension commands.
# Commands are automatically registered into all detected agent
# directories (.claude/commands/, .gemini/commands/, etc.)
# Override a core command:
- type: "command"
name: "speckit.specify"
file: "commands/speckit.specify.md"
description: "Custom specification command"
replaces: "speckit.specify"
# Override an extension command (e.g. from the "myext" extension):
- type: "command"
name: "speckit.myext.myextcmd"
file: "commands/speckit.myext.myextcmd.md"
description: "Override myext's myextcmd command with custom workflow"
replaces: "speckit.myext.myextcmd"
# Script templates (reserved for future use)
# - type: "script"
# name: "create-new-feature"
# file: "scripts/bash/create-new-feature.sh"
# description: "Custom feature creation script"
# replaces: "create-new-feature"
# CUSTOMIZE: Add relevant tags (2-5 recommended)
# Used for discovery in catalog
tags:
- "example"
- "preset"

View File

@@ -0,0 +1,24 @@
# MyExt Report
> This template overrides the one provided by the "myext" extension.
> Customize it to match your needs.
## Summary
Brief summary of the report.
## Details
- Detail 1
- Detail 2
## Actions
- [ ] Action 1
- [ ] Action 2
<!--
CUSTOMIZE: This template takes priority over the myext extension's
version of myext-template. The extension's original is still available
if you remove this preset.
-->

View File

@@ -0,0 +1,18 @@
# Feature Specification: [FEATURE NAME]
**Created**: [DATE]
**Status**: Draft
## Overview
[Brief description of the feature]
## Requirements
- [ ] Requirement 1
- [ ] Requirement 2
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2

View File

@@ -0,0 +1,15 @@
---
description: "Self-test override of the specify command"
---
<!-- preset:self-test -->
You are following the self-test preset's version of the specify command.
When creating a specification, follow this process:
1. Read the user's requirements from $ARGUMENTS
2. Create a specification document using the spec-template
3. Include all standard sections plus the self-test marker
> This command is provided by the self-test preset.

View File

@@ -0,0 +1,61 @@
schema_version: "1.0"
preset:
id: "self-test"
name: "Self-Test Preset"
version: "1.0.0"
description: "A preset that overrides all core templates for testing purposes"
author: "github"
repository: "https://github.com/github/spec-kit"
license: "MIT"
requires:
speckit_version: ">=0.1.0"
provides:
templates:
- type: "template"
name: "spec-template"
file: "templates/spec-template.md"
description: "Self-test spec template"
replaces: "spec-template"
- type: "template"
name: "plan-template"
file: "templates/plan-template.md"
description: "Self-test plan template"
replaces: "plan-template"
- type: "template"
name: "tasks-template"
file: "templates/tasks-template.md"
description: "Self-test tasks template"
replaces: "tasks-template"
- type: "template"
name: "checklist-template"
file: "templates/checklist-template.md"
description: "Self-test checklist template"
replaces: "checklist-template"
- type: "template"
name: "constitution-template"
file: "templates/constitution-template.md"
description: "Self-test constitution template"
replaces: "constitution-template"
- type: "template"
name: "agent-file-template"
file: "templates/agent-file-template.md"
description: "Self-test agent file template"
replaces: "agent-file-template"
- type: "command"
name: "speckit.specify"
file: "commands/speckit.specify.md"
description: "Self-test override of the specify command"
replaces: "speckit.specify"
tags:
- "testing"
- "self-test"

View File

@@ -0,0 +1,9 @@
# Agent File (Self-Test Preset)
<!-- preset:self-test -->
> This template is provided by the self-test preset.
## Agent Instructions
Follow these guidelines when working on this project.

View File

@@ -0,0 +1,15 @@
# Checklist (Self-Test Preset)
<!-- preset:self-test -->
> This template is provided by the self-test preset.
## Pre-Implementation
- [ ] Spec reviewed
- [ ] Plan approved
## Post-Implementation
- [ ] Tests passing
- [ ] Documentation updated

View File

@@ -0,0 +1,15 @@
# Constitution (Self-Test Preset)
<!-- preset:self-test -->
> This template is provided by the self-test preset.
## Principles
1. Principle 1
2. Principle 2
## Guidelines
- Guideline 1
- Guideline 2

View File

@@ -0,0 +1,22 @@
# Implementation Plan (Self-Test Preset)
<!-- preset:self-test -->
> This template is provided by the self-test preset.
## Approach
Describe the implementation approach.
## Steps
1. Step 1
2. Step 2
## Dependencies
- Dependency 1
## Risks
- Risk 1

View File

@@ -0,0 +1,23 @@
# Feature Specification (Self-Test Preset)
<!-- preset:self-test -->
> This template is provided by the self-test preset.
## Overview
Brief description of the feature.
## Requirements
- Requirement 1
- Requirement 2
## Design
Describe the design approach.
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2

View File

@@ -0,0 +1,17 @@
# Tasks (Self-Test Preset)
<!-- preset:self-test -->
> This template is provided by the self-test preset.
## Task List
- [ ] Task 1
- [ ] Task 2
## Estimation
| Task | Estimate |
|------|----------|
| Task 1 | TBD |
| Task 2 | TBD |

View File

@@ -1,15 +1,19 @@
[project]
name = "specify-cli"
version = "0.0.22"
version = "0.3.0"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [
"typer",
"click>=8.1",
"rich",
"httpx[socks]",
"platformdirs",
"readchar",
"truststore>=0.10.4",
"pyyaml>=6.0",
"packaging>=23.0",
"pathspec>=0.12.0",
]
[project.scripts]
@@ -22,3 +26,29 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/specify_cli"]
[project.optional-dependencies]
test = [
"pytest>=7.0",
"pytest-cov>=4.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--tb=short",
]
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/__pycache__/*"]
[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false

View File

@@ -79,15 +79,28 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get feature paths and validate branch
eval $(get_feature_paths)
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
eval "$_paths_output"
unset _paths_output
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
if $PATHS_ONLY; then
if $JSON_MODE; then
# Minimal JSON paths payload (no validation performed)
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
"$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
if has_jq; then
jq -cn \
--arg repo_root "$REPO_ROOT" \
--arg branch "$CURRENT_BRANCH" \
--arg feature_dir "$FEATURE_DIR" \
--arg feature_spec "$FEATURE_SPEC" \
--arg impl_plan "$IMPL_PLAN" \
--arg tasks "$TASKS" \
'{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'
else
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
"$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"
fi
else
echo "REPO_ROOT: $REPO_ROOT"
echo "BRANCH: $CURRENT_BRANCH"
@@ -141,14 +154,25 @@ fi
# Output results
if $JSON_MODE; then
# Build JSON array of documents
if [[ ${#docs[@]} -eq 0 ]]; then
json_docs="[]"
if has_jq; then
if [[ ${#docs[@]} -eq 0 ]]; then
json_docs="[]"
else
json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
fi
jq -cn \
--arg feature_dir "$FEATURE_DIR" \
--argjson docs "$json_docs" \
'{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'
else
json_docs=$(printf '"%s",' "${docs[@]}")
json_docs="[${json_docs%,}]"
if [[ ${#docs[@]} -eq 0 ]]; then
json_docs="[]"
else
json_docs=$(printf '"%s",' "${docs[@]}")
json_docs="[${json_docs%,}]"
fi
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"
fi
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
else
# Text output
echo "FEATURE_DIR:$FEATURE_DIR"

View File

@@ -120,7 +120,7 @@ find_feature_dir_by_prefix() {
# Multiple matches - this shouldn't happen with proper naming convention
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
echo "Please ensure only one spec directory exists per numeric prefix." >&2
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
return 1
fi
}
@@ -134,23 +134,120 @@ get_feature_paths() {
fi
# Use prefix-based lookup to support multiple branches per spec
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
local feature_dir
if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
echo "ERROR: Failed to resolve feature directory" >&2
return 1
fi
cat <<EOF
REPO_ROOT='$repo_root'
CURRENT_BRANCH='$current_branch'
HAS_GIT='$has_git_repo'
FEATURE_DIR='$feature_dir'
FEATURE_SPEC='$feature_dir/spec.md'
IMPL_PLAN='$feature_dir/plan.md'
TASKS='$feature_dir/tasks.md'
RESEARCH='$feature_dir/research.md'
DATA_MODEL='$feature_dir/data-model.md'
QUICKSTART='$feature_dir/quickstart.md'
CONTRACTS_DIR='$feature_dir/contracts'
EOF
# Use printf '%q' to safely quote values, preventing shell injection
# via crafted branch names or paths containing special characters
printf 'REPO_ROOT=%q\n' "$repo_root"
printf 'CURRENT_BRANCH=%q\n' "$current_branch"
printf 'HAS_GIT=%q\n' "$has_git_repo"
printf 'FEATURE_DIR=%q\n' "$feature_dir"
printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
printf 'TASKS=%q\n' "$feature_dir/tasks.md"
printf 'RESEARCH=%q\n' "$feature_dir/research.md"
printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
}
# Check if jq is available for safe JSON construction
has_jq() {
command -v jq >/dev/null 2>&1
}
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
# Handles backslash, double-quote, and control characters (newline, tab, carriage return).
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/\\r}"
printf '%s' "$s"
}
check_file() { [[ -f "$1" ]] && echo "$2" || echo "$2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo "$2" || echo "$2"; }
# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
# 3. .specify/extensions/<ext-id>/templates/
# 4. .specify/templates/ (core)
resolve_template() {
local template_name="$1"
local repo_root="$2"
local base="$repo_root/.specify/templates"
# Priority 1: Project overrides
local override="$base/overrides/${template_name}.md"
[ -f "$override" ] && echo "$override" && return 0
# Priority 2: Installed presets (sorted by priority from .registry)
local presets_dir="$repo_root/.specify/presets"
if [ -d "$presets_dir" ]; then
local registry_file="$presets_dir/.registry"
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
# Read preset IDs sorted by priority (lower number = higher precedence)
local sorted_presets
sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
import json, sys, os
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$sorted_presets" ]; then
while IFS= read -r preset_id; do
local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done <<< "$sorted_presets"
else
# python3 returned empty list — fall through to directory scan
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi
else
# Fallback: alphabetical directory order (no python3 available)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi
fi
# Priority 3: Extension-provided templates
local ext_dir="$repo_root/.specify/extensions"
if [ -d "$ext_dir" ]; then
for ext in "$ext_dir"/*/; do
[ -d "$ext" ] || continue
# Skip hidden directories (e.g. .backup, .cache)
case "$(basename "$ext")" in .*) continue;; esac
local candidate="$ext/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi
# Priority 4: Core templates
local core="$base/${template_name}.md"
[ -f "$core" ] && echo "$core" && return 0
# Return success with empty output so callers using set -e don't abort;
# callers check [ -n "$TEMPLATE" ] to detect "not found".
return 0
}

View File

@@ -67,6 +67,13 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
exit 1
fi
# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
exit 1
fi
# Function to find the repository root by searching for existing project markers
find_repo_root() {
local dir="$1"
@@ -155,10 +162,22 @@ clean_branch_name() {
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
}
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/\\r}"
printf '%s' "$s"
}
# Resolve repository root. Prefer git information when available, but fall back
# to searching for repository markers so the workflow still functions in repositories that
# were initialised with --no-git.
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
if git rev-parse --show-toplevel >/dev/null 2>&1; then
REPO_ROOT=$(git rev-parse --show-toplevel)
@@ -272,7 +291,16 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
fi
if [ "$HAS_GIT" = true ]; then
git checkout -b "$BRANCH_NAME"
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
# Check if branch already exists
if git branch --list "$BRANCH_NAME" | grep -q .; then
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
exit 1
else
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
exit 1
fi
fi
else
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
fi
@@ -280,18 +308,26 @@ fi
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
mkdir -p "$FEATURE_DIR"
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT")
SPEC_FILE="$FEATURE_DIR/spec.md"
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
# Set the SPECIFY_FEATURE environment variable for the current session
export SPECIFY_FEATURE="$BRANCH_NAME"
# Inform the user how to persist the feature variable in their own shell
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
if $JSON_MODE; then
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
if command -v jq >/dev/null 2>&1; then
jq -cn \
--arg branch_name "$BRANCH_NAME" \
--arg spec_file "$SPEC_FILE" \
--arg feature_num "$FEATURE_NUM" \
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
else
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
fi
else
echo "BRANCH_NAME: $BRANCH_NAME"
echo "SPEC_FILE: $SPEC_FILE"
echo "FEATURE_NUM: $FEATURE_NUM"
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
fi

View File

@@ -28,7 +28,9 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions
eval $(get_feature_paths)
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
eval "$_paths_output"
unset _paths_output
# Check if we're on a proper feature branch (only for git repos)
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
@@ -37,20 +39,30 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
mkdir -p "$FEATURE_DIR"
# Copy plan template if it exists
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
if [[ -f "$TEMPLATE" ]]; then
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT")
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
cp "$TEMPLATE" "$IMPL_PLAN"
echo "Copied plan template to $IMPL_PLAN"
else
echo "Warning: Plan template not found at $TEMPLATE"
echo "Warning: Plan template not found"
# Create a basic plan file if template doesn't exist
touch "$IMPL_PLAN"
fi
# Output results
if $JSON_MODE; then
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
if has_jq; then
jq -cn \
--arg feature_spec "$FEATURE_SPEC" \
--arg impl_plan "$IMPL_PLAN" \
--arg specs_dir "$FEATURE_DIR" \
--arg branch "$CURRENT_BRANCH" \
--arg has_git "$HAS_GIT" \
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'
else
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"
fi
else
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"

View File

@@ -30,12 +30,12 @@
#
# 5. Multi-Agent Support
# - Handles agent-specific file paths and naming conventions
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, or Amazon Q Developer CLI
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Antigravity or Generic
# - Can update single agents or all existing agent files
# - Creates default Claude file if no agent files exist
#
# Usage: ./update-agent-context.sh [agent_type]
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|shai|q|bob|qoder
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic
# Leave empty to update all existing agent files
set -e
@@ -53,7 +53,9 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions
eval $(get_feature_paths)
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
eval "$_paths_output"
unset _paths_output
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
AGENT_TYPE="${1:-}"
@@ -71,10 +73,16 @@ AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
QODER_FILE="$REPO_ROOT/QODER.md"
AMP_FILE="$REPO_ROOT/AGENTS.md"
# AMP, Kiro CLI, and IBM Bob all share AGENTS.md — use AGENTS_FILE to avoid
# updating the same file multiple times.
AMP_FILE="$AGENTS_FILE"
SHAI_FILE="$REPO_ROOT/SHAI.md"
Q_FILE="$REPO_ROOT/AGENTS.md"
BOB_FILE="$REPO_ROOT/AGENTS.md"
TABNINE_FILE="$REPO_ROOT/TABNINE.md"
KIRO_FILE="$AGENTS_FILE"
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
BOB_FILE="$AGENTS_FILE"
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
KIMI_FILE="$REPO_ROOT/KIMI.md"
# Template file
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
@@ -108,6 +116,8 @@ log_warning() {
# Cleanup function for temporary files
cleanup() {
local exit_code=$?
# Disarm traps to prevent re-entrant loop
trap - EXIT INT TERM
rm -f /tmp/agent_update_*_$$
rm -f /tmp/manual_additions_$$
exit $exit_code
@@ -350,10 +360,19 @@ create_new_agent_file() {
# Convert \n sequences to actual newlines
newline=$(printf '\n')
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
# Clean up backup files
rm -f "$temp_file.bak" "$temp_file.bak2"
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
if [[ "$target_file" == *.mdc ]]; then
local frontmatter_file
frontmatter_file=$(mktemp) || return 1
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
cat "$temp_file" >> "$frontmatter_file"
mv "$frontmatter_file" "$temp_file"
fi
return 0
}
@@ -463,7 +482,7 @@ update_existing_agent_file() {
fi
# Update timestamp
if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
if [[ "$line" =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
else
echo "$line" >> "$temp_file"
@@ -491,13 +510,24 @@ update_existing_agent_file() {
changes_entries_added=true
fi
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
if [[ "$target_file" == *.mdc ]]; then
if ! head -1 "$temp_file" | grep -q '^---'; then
local frontmatter_file
frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
cat "$temp_file" >> "$frontmatter_file"
mv "$frontmatter_file" "$temp_file"
fi
fi
# Move temp file to target atomically
if ! mv "$temp_file" "$target_file"; then
log_error "Failed to update target file"
rm -f "$temp_file"
return 1
fi
return 0
}
#==============================================================================
@@ -583,59 +613,74 @@ update_specific_agent() {
case "$agent_type" in
claude)
update_agent_file "$CLAUDE_FILE" "Claude Code"
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
;;
gemini)
update_agent_file "$GEMINI_FILE" "Gemini CLI"
update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1
;;
copilot)
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1
;;
cursor-agent)
update_agent_file "$CURSOR_FILE" "Cursor IDE"
update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1
;;
qwen)
update_agent_file "$QWEN_FILE" "Qwen Code"
update_agent_file "$QWEN_FILE" "Qwen Code" || return 1
;;
opencode)
update_agent_file "$AGENTS_FILE" "opencode"
update_agent_file "$AGENTS_FILE" "opencode" || return 1
;;
codex)
update_agent_file "$AGENTS_FILE" "Codex CLI"
update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1
;;
windsurf)
update_agent_file "$WINDSURF_FILE" "Windsurf"
update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1
;;
kilocode)
update_agent_file "$KILOCODE_FILE" "Kilo Code"
update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1
;;
auggie)
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1
;;
roo)
update_agent_file "$ROO_FILE" "Roo Code"
update_agent_file "$ROO_FILE" "Roo Code" || return 1
;;
codebuddy)
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1
;;
qoder)
update_agent_file "$QODER_FILE" "Qoder CLI"
qodercli)
update_agent_file "$QODER_FILE" "Qoder CLI" || return 1
;;
amp)
update_agent_file "$AMP_FILE" "Amp"
update_agent_file "$AMP_FILE" "Amp" || return 1
;;
shai)
update_agent_file "$SHAI_FILE" "SHAI"
update_agent_file "$SHAI_FILE" "SHAI" || return 1
;;
q)
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
tabnine)
update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1
;;
kiro-cli)
update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1
;;
agy)
update_agent_file "$AGY_FILE" "Antigravity" || return 1
;;
bob)
update_agent_file "$BOB_FILE" "IBM Bob"
update_agent_file "$BOB_FILE" "IBM Bob" || return 1
;;
vibe)
update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1
;;
kimi)
update_agent_file "$KIMI_FILE" "Kimi Code" || return 1
;;
generic)
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
;;
*)
log_error "Unknown agent type '$agent_type'"
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|amp|shai|q|bob|qoder"
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic"
exit 1
;;
esac
@@ -643,87 +688,53 @@ update_specific_agent() {
update_all_existing_agents() {
local found_agent=false
# Check each possible agent file and update if it exists
if [[ -f "$CLAUDE_FILE" ]]; then
update_agent_file "$CLAUDE_FILE" "Claude Code"
found_agent=true
fi
if [[ -f "$GEMINI_FILE" ]]; then
update_agent_file "$GEMINI_FILE" "Gemini CLI"
found_agent=true
fi
if [[ -f "$COPILOT_FILE" ]]; then
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
found_agent=true
fi
if [[ -f "$CURSOR_FILE" ]]; then
update_agent_file "$CURSOR_FILE" "Cursor IDE"
found_agent=true
fi
if [[ -f "$QWEN_FILE" ]]; then
update_agent_file "$QWEN_FILE" "Qwen Code"
found_agent=true
fi
if [[ -f "$AGENTS_FILE" ]]; then
update_agent_file "$AGENTS_FILE" "Codex/opencode"
found_agent=true
fi
if [[ -f "$WINDSURF_FILE" ]]; then
update_agent_file "$WINDSURF_FILE" "Windsurf"
found_agent=true
fi
if [[ -f "$KILOCODE_FILE" ]]; then
update_agent_file "$KILOCODE_FILE" "Kilo Code"
found_agent=true
fi
local _updated_paths=()
if [[ -f "$AUGGIE_FILE" ]]; then
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
# Helper: skip non-existent files and files already updated (dedup by
# realpath so that variables pointing to the same file — e.g. AMP_FILE,
# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once).
# Uses a linear array instead of associative array for bash 3.2 compatibility.
update_if_new() {
local file="$1" name="$2"
[[ -f "$file" ]] || return 0
local real_path
real_path=$(realpath "$file" 2>/dev/null || echo "$file")
local p
if [[ ${#_updated_paths[@]} -gt 0 ]]; then
for p in "${_updated_paths[@]}"; do
[[ "$p" == "$real_path" ]] && return 0
done
fi
update_agent_file "$file" "$name" || return 1
_updated_paths+=("$real_path")
found_agent=true
fi
if [[ -f "$ROO_FILE" ]]; then
update_agent_file "$ROO_FILE" "Roo Code"
found_agent=true
fi
}
if [[ -f "$CODEBUDDY_FILE" ]]; then
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
found_agent=true
fi
update_if_new "$CLAUDE_FILE" "Claude Code"
update_if_new "$GEMINI_FILE" "Gemini CLI"
update_if_new "$COPILOT_FILE" "GitHub Copilot"
update_if_new "$CURSOR_FILE" "Cursor IDE"
update_if_new "$QWEN_FILE" "Qwen Code"
update_if_new "$AGENTS_FILE" "Codex/opencode"
update_if_new "$AMP_FILE" "Amp"
update_if_new "$KIRO_FILE" "Kiro CLI"
update_if_new "$BOB_FILE" "IBM Bob"
update_if_new "$WINDSURF_FILE" "Windsurf"
update_if_new "$KILOCODE_FILE" "Kilo Code"
update_if_new "$AUGGIE_FILE" "Auggie CLI"
update_if_new "$ROO_FILE" "Roo Code"
update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI"
update_if_new "$SHAI_FILE" "SHAI"
update_if_new "$TABNINE_FILE" "Tabnine CLI"
update_if_new "$QODER_FILE" "Qoder CLI"
update_if_new "$AGY_FILE" "Antigravity"
update_if_new "$VIBE_FILE" "Mistral Vibe"
update_if_new "$KIMI_FILE" "Kimi Code"
if [[ -f "$SHAI_FILE" ]]; then
update_agent_file "$SHAI_FILE" "SHAI"
found_agent=true
fi
if [[ -f "$QODER_FILE" ]]; then
update_agent_file "$QODER_FILE" "Qoder CLI"
found_agent=true
fi
if [[ -f "$Q_FILE" ]]; then
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
found_agent=true
fi
if [[ -f "$BOB_FILE" ]]; then
update_agent_file "$BOB_FILE" "IBM Bob"
found_agent=true
fi
# If no agent files exist, create a default Claude file
if [[ "$found_agent" == false ]]; then
log_info "No existing agent files found, creating default Claude file..."
update_agent_file "$CLAUDE_FILE" "Claude Code"
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
fi
}
print_summary() {
@@ -743,8 +754,7 @@ print_summary() {
fi
echo
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|codebuddy|shai|q|bob|qoder]"
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic]"
}
#==============================================================================
@@ -796,4 +806,3 @@ main() {
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -135,3 +135,70 @@ function Test-DirHasFiles {
}
}
# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
# 3. .specify/extensions/<ext-id>/templates/
# 4. .specify/templates/ (core)
function Resolve-Template {
param(
[Parameter(Mandatory=$true)][string]$TemplateName,
[Parameter(Mandatory=$true)][string]$RepoRoot
)
$base = Join-Path $RepoRoot '.specify/templates'
# Priority 1: Project overrides
$override = Join-Path $base "overrides/$TemplateName.md"
if (Test-Path $override) { return $override }
# Priority 2: Installed presets (sorted by priority from .registry)
$presetsDir = Join-Path $RepoRoot '.specify/presets'
if (Test-Path $presetsDir) {
$registryFile = Join-Path $presetsDir '.registry'
$sortedPresets = @()
if (Test-Path $registryFile) {
try {
$registryData = Get-Content $registryFile -Raw | ConvertFrom-Json
$presets = $registryData.presets
if ($presets) {
$sortedPresets = $presets.PSObject.Properties |
Sort-Object { if ($null -ne $_.Value.priority) { $_.Value.priority } else { 10 } } |
ForEach-Object { $_.Name }
}
} catch {
# Fallback: alphabetical directory order
$sortedPresets = @()
}
}
if ($sortedPresets.Count -gt 0) {
foreach ($presetId in $sortedPresets) {
$candidate = Join-Path $presetsDir "$presetId/templates/$TemplateName.md"
if (Test-Path $candidate) { return $candidate }
}
} else {
# Fallback: alphabetical directory order
foreach ($preset in Get-ChildItem -Path $presetsDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' }) {
$candidate = Join-Path $preset.FullName "templates/$TemplateName.md"
if (Test-Path $candidate) { return $candidate }
}
}
}
# Priority 3: Extension-provided templates
$extDir = Join-Path $RepoRoot '.specify/extensions'
if (Test-Path $extDir) {
foreach ($ext in Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' } | Sort-Object Name) {
$candidate = Join-Path $ext.FullName "templates/$TemplateName.md"
if (Test-Path $candidate) { return $candidate }
}
}
# Priority 4: Core templates
$core = Join-Path $base "$TemplateName.md"
if (Test-Path $core) { return $core }
return $null
}

View File

@@ -35,6 +35,12 @@ if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
$featureDesc = ($FeatureDescription -join ' ').Trim()
# Validate description is not empty after trimming (e.g., user passed only whitespace)
if ([string]::IsNullOrWhiteSpace($featureDesc)) {
Write-Error "Error: Feature description cannot be empty or contain only whitespace"
exit 1
}
# Resolve repository root. Prefer git information when available, but fall back
# to searching for repository markers so the workflow still functions in repositories that
# were initialized with --no-git.
@@ -135,6 +141,9 @@ if (-not $fallbackRoot) {
exit 1
}
# Load common functions (includes Resolve-Template)
. "$PSScriptRoot/common.ps1"
try {
$repoRoot = git rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -eq 0) {
@@ -242,10 +251,26 @@ if ($branchName.Length -gt $maxBranchLength) {
}
if ($hasGit) {
$branchCreated = $false
try {
git checkout -b $branchName | Out-Null
git checkout -q -b $branchName 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
$branchCreated = $true
}
} catch {
Write-Warning "Failed to create git branch: $branchName"
# Exception during git command
}
if (-not $branchCreated) {
# Check if branch already exists
$existingBranch = git branch --list $branchName 2>$null
if ($existingBranch) {
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
exit 1
} else {
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
exit 1
}
}
} else {
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
@@ -254,9 +279,9 @@ if ($hasGit) {
$featureDir = Join-Path $specsDir $branchName
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
$specFile = Join-Path $featureDir 'spec.md'
if (Test-Path $template) {
if ($template -and (Test-Path $template)) {
Copy-Item $template $specFile -Force
} else {
New-Item -ItemType File -Path $specFile | Out-Null

View File

@@ -32,12 +32,12 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GI
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
# Copy plan template if it exists, otherwise note it or create empty file
$template = Join-Path $paths.REPO_ROOT '.specify/templates/plan-template.md'
if (Test-Path $template) {
$template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT
if ($template -and (Test-Path $template)) {
Copy-Item $template $paths.IMPL_PLAN -Force
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
} else {
Write-Warning "Plan template not found at $template"
Write-Warning "Plan template not found"
# Create a basic plan file if template doesn't exist
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
}

View File

@@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh:
2. Plan Data Extraction
3. Agent File Management (create from template or update existing)
4. Content Generation (technology stack, recent changes, timestamp)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, q, bob, qoder)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, generic)
.PARAMETER AgentType
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
@@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1
#>
param(
[Parameter(Position=0)]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','q','bob','qoder')]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','qodercli','vibe','kimi','generic')]
[string]$AgentType
)
@@ -58,8 +58,12 @@ $CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md'
$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
$Q_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$TABNINE_FILE = Join-Path $REPO_ROOT 'TABNINE.md'
$KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-agents.md'
$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md'
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
@@ -257,6 +261,12 @@ function New-AgentFile {
# Convert literal \n sequences introduced by Escape to real newlines
$content = $content -replace '\\n',[Environment]::NewLine
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
if ($TargetFile -match '\.mdc$') {
$frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') -join [Environment]::NewLine
$content = $frontmatter + $content
}
$parent = Split-Path -Parent $TargetFile
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null }
Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8
@@ -321,7 +331,7 @@ function Update-ExistingAgentFile {
if ($existingChanges -lt 2) { $output.Add($line); $existingChanges++ }
continue
}
if ($line -match '\*\*Last updated\*\*: .*\d{4}-\d{2}-\d{2}') {
if ($line -match '(\*\*)?Last updated(\*\*)?: .*\d{4}-\d{2}-\d{2}') {
$output.Add(($line -replace '\d{4}-\d{2}-\d{2}',$Date.ToString('yyyy-MM-dd')))
continue
}
@@ -333,6 +343,12 @@ function Update-ExistingAgentFile {
$newTechEntries | ForEach-Object { $output.Add($_) }
}
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
if ($TargetFile -match '\.mdc$' -and $output.Count -gt 0 -and $output[0] -ne '---') {
$frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','')
$output.InsertRange(0, $frontmatter)
}
Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8
return $true
}
@@ -383,12 +399,17 @@ function Update-SpecificAgent {
'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' }
'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' }
'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI' }
'qoder' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
'q' { Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI' }
'tabnine' { Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI' }
'kiro-cli' { Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI' }
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|bob|qoder'; return $false }
'vibe' { Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe' }
'kimi' { Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code' }
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic'; return $false }
}
}
@@ -408,8 +429,12 @@ function Update-AllExistingAgents {
if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true }
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
if (Test-Path $Q_FILE) { if (-not (Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI')) { $ok = $false }; $found = $true }
if (Test-Path $TABNINE_FILE) { if (-not (Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI')) { $ok = $false }; $found = $true }
if (Test-Path $KIRO_FILE) { if (-not (Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI')) { $ok = $false }; $found = $true }
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true }
if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
if (Test-Path $VIBE_FILE) { if (-not (Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }; $found = $true }
if (Test-Path $KIMI_FILE) { if (-not (Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false }; $found = $true }
if (-not $found) {
Write-Info 'No existing agent files found, creating default Claude file...'
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
@@ -424,7 +449,7 @@ function Print-Summary {
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
Write-Host ''
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|bob|qoder]'
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic]'
}
function Main {
@@ -445,4 +470,3 @@ function Main {
}
Main

8
spec-kit.code-workspace Normal file
View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

File diff suppressed because it is too large Load Diff

422
src/specify_cli/agents.py Normal file
View File

@@ -0,0 +1,422 @@
"""
Agent Command Registrar for Spec Kit
Shared infrastructure for registering commands with AI agents.
Used by both the extension system and the preset system to write
command files into agent-specific directories in the correct format.
"""
from pathlib import Path
from typing import Dict, List, Any
import yaml
class CommandRegistrar:
"""Handles registration of commands with AI agents.
Supports writing command files in Markdown or TOML format to the
appropriate agent directory, with correct argument placeholders
and companion files (e.g. Copilot .prompt.md).
"""
# Agent configurations with directory, format, and argument placeholder
AGENT_CONFIGS = {
"claude": {
"dir": ".claude/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"gemini": {
"dir": ".gemini/commands",
"format": "toml",
"args": "{{args}}",
"extension": ".toml"
},
"copilot": {
"dir": ".github/agents",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".agent.md"
},
"cursor": {
"dir": ".cursor/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"qwen": {
"dir": ".qwen/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"opencode": {
"dir": ".opencode/command",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"codex": {
"dir": ".codex/prompts",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"windsurf": {
"dir": ".windsurf/workflows",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"kilocode": {
"dir": ".kilocode/workflows",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"auggie": {
"dir": ".augment/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"roo": {
"dir": ".roo/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"codebuddy": {
"dir": ".codebuddy/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"qodercli": {
"dir": ".qoder/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"kiro-cli": {
"dir": ".kiro/prompts",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"amp": {
"dir": ".agents/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"shai": {
"dir": ".shai/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"tabnine": {
"dir": ".tabnine/agent/commands",
"format": "toml",
"args": "{{args}}",
"extension": ".toml"
},
"bob": {
"dir": ".bob/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"kimi": {
"dir": ".kimi/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md"
}
}
@staticmethod
def parse_frontmatter(content: str) -> tuple[dict, str]:
"""Parse YAML frontmatter from Markdown content.
Args:
content: Markdown content with YAML frontmatter
Returns:
Tuple of (frontmatter_dict, body_content)
"""
if not content.startswith("---"):
return {}, content
# Find second ---
end_marker = content.find("---", 3)
if end_marker == -1:
return {}, content
frontmatter_str = content[3:end_marker].strip()
body = content[end_marker + 3:].strip()
try:
frontmatter = yaml.safe_load(frontmatter_str) or {}
except yaml.YAMLError:
frontmatter = {}
return frontmatter, body
@staticmethod
def render_frontmatter(fm: dict) -> str:
"""Render frontmatter dictionary as YAML.
Args:
fm: Frontmatter dictionary
Returns:
YAML-formatted frontmatter with delimiters
"""
if not fm:
return ""
yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False)
return f"---\n{yaml_str}---\n"
def _adjust_script_paths(self, frontmatter: dict) -> dict:
"""Adjust script paths from extension-relative to repo-relative.
Args:
frontmatter: Frontmatter dictionary
Returns:
Modified frontmatter with adjusted paths
"""
if "scripts" in frontmatter:
for key in frontmatter["scripts"]:
script_path = frontmatter["scripts"][key]
if script_path.startswith("../../scripts/"):
frontmatter["scripts"][key] = f".specify/scripts/{script_path[14:]}"
return frontmatter
def render_markdown_command(
self,
frontmatter: dict,
body: str,
source_id: str,
context_note: str = None
) -> str:
"""Render command in Markdown format.
Args:
frontmatter: Command frontmatter
body: Command body content
source_id: Source identifier (extension or preset ID)
context_note: Custom context comment (default: <!-- Source: {source_id} -->)
Returns:
Formatted Markdown command file content
"""
if context_note is None:
context_note = f"\n<!-- Source: {source_id} -->\n"
return self.render_frontmatter(frontmatter) + "\n" + context_note + body
def render_toml_command(
self,
frontmatter: dict,
body: str,
source_id: str
) -> str:
"""Render command in TOML format.
Args:
frontmatter: Command frontmatter
body: Command body content
source_id: Source identifier (extension or preset ID)
Returns:
Formatted TOML command file content
"""
toml_lines = []
if "description" in frontmatter:
desc = frontmatter["description"].replace('"', '\\"')
toml_lines.append(f'description = "{desc}"')
toml_lines.append("")
toml_lines.append(f"# Source: {source_id}")
toml_lines.append("")
toml_lines.append('prompt = """')
toml_lines.append(body)
toml_lines.append('"""')
return "\n".join(toml_lines)
def _convert_argument_placeholder(self, content: str, from_placeholder: str, to_placeholder: str) -> str:
"""Convert argument placeholder format.
Args:
content: Command content
from_placeholder: Source placeholder (e.g., "$ARGUMENTS")
to_placeholder: Target placeholder (e.g., "{{args}}")
Returns:
Content with converted placeholders
"""
return content.replace(from_placeholder, to_placeholder)
def register_commands(
self,
agent_name: str,
commands: List[Dict[str, Any]],
source_id: str,
source_dir: Path,
project_root: Path,
context_note: str = None
) -> List[str]:
"""Register commands for a specific agent.
Args:
agent_name: Agent name (claude, gemini, copilot, etc.)
commands: List of command info dicts with 'name', 'file', and optional 'aliases'
source_id: Identifier of the source (extension or preset ID)
source_dir: Directory containing command source files
project_root: Path to project root
context_note: Custom context comment for markdown output
Returns:
List of registered command names
Raises:
ValueError: If agent is not supported
"""
if agent_name not in self.AGENT_CONFIGS:
raise ValueError(f"Unsupported agent: {agent_name}")
agent_config = self.AGENT_CONFIGS[agent_name]
commands_dir = project_root / agent_config["dir"]
commands_dir.mkdir(parents=True, exist_ok=True)
registered = []
for cmd_info in commands:
cmd_name = cmd_info["name"]
cmd_file = cmd_info["file"]
source_file = source_dir / cmd_file
if not source_file.exists():
continue
content = source_file.read_text(encoding="utf-8")
frontmatter, body = self.parse_frontmatter(content)
frontmatter = self._adjust_script_paths(frontmatter)
body = self._convert_argument_placeholder(
body, "$ARGUMENTS", agent_config["args"]
)
if agent_config["format"] == "markdown":
output = self.render_markdown_command(frontmatter, body, source_id, context_note)
elif agent_config["format"] == "toml":
output = self.render_toml_command(frontmatter, body, source_id)
else:
raise ValueError(f"Unsupported format: {agent_config['format']}")
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
dest_file.parent.mkdir(parents=True, exist_ok=True)
dest_file.write_text(output, encoding="utf-8")
if agent_name == "copilot":
self.write_copilot_prompt(project_root, cmd_name)
registered.append(cmd_name)
for alias in cmd_info.get("aliases", []):
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
alias_file.parent.mkdir(parents=True, exist_ok=True)
alias_file.write_text(output, encoding="utf-8")
if agent_name == "copilot":
self.write_copilot_prompt(project_root, alias)
registered.append(alias)
return registered
@staticmethod
def write_copilot_prompt(project_root: Path, cmd_name: str) -> None:
"""Generate a companion .prompt.md file for a Copilot agent command.
Args:
project_root: Path to project root
cmd_name: Command name (e.g. 'speckit.my-ext.example')
"""
prompts_dir = project_root / ".github" / "prompts"
prompts_dir.mkdir(parents=True, exist_ok=True)
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n", encoding="utf-8")
def register_commands_for_all_agents(
self,
commands: List[Dict[str, Any]],
source_id: str,
source_dir: Path,
project_root: Path,
context_note: str = None
) -> Dict[str, List[str]]:
"""Register commands for all detected agents in the project.
Args:
commands: List of command info dicts
source_id: Identifier of the source (extension or preset ID)
source_dir: Directory containing command source files
project_root: Path to project root
context_note: Custom context comment for markdown output
Returns:
Dictionary mapping agent names to list of registered commands
"""
results = {}
for agent_name, agent_config in self.AGENT_CONFIGS.items():
agent_dir = project_root / agent_config["dir"].split("/")[0]
if agent_dir.exists():
try:
registered = self.register_commands(
agent_name, commands, source_id, source_dir, project_root,
context_note=context_note
)
if registered:
results[agent_name] = registered
except ValueError:
continue
return results
def unregister_commands(
self,
registered_commands: Dict[str, List[str]],
project_root: Path
) -> None:
"""Remove previously registered command files from agent directories.
Args:
registered_commands: Dict mapping agent names to command name lists
project_root: Path to project root
"""
for agent_name, cmd_names in registered_commands.items():
if agent_name not in self.AGENT_CONFIGS:
continue
agent_config = self.AGENT_CONFIGS[agent_name]
commands_dir = project_root / agent_config["dir"]
for cmd_name in cmd_names:
cmd_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
if cmd_file.exists():
cmd_file.unlink()
if agent_name == "copilot":
prompt_file = project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
if prompt_file.exists():
prompt_file.unlink()

File diff suppressed because it is too large Load Diff

1530
src/specify_cli/presets.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -94,9 +94,10 @@ You **MUST** consider the user input before proceeding (if not empty).
- Generate unique checklist filename:
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
- Format: `[domain].md`
- If file exists, append to existing file
- Number items sequentially starting from CHK001
- Each `/speckit.checklist` run creates a NEW file (never overwrites existing checklists)
- File handling behavior:
- If file does NOT exist: Create new file and number items starting from CHK001
- If file exists: Append new items to existing file, continuing from the last CHK ID (e.g., if last item is CHK015, start new items at CHK016)
- Never delete or replace existing checklist content - always preserve and append
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
@@ -208,13 +209,13 @@ You **MUST** consider the user input before proceeding (if not empty).
6. **Structure Reference**: Generate the checklist following the canonical template in `templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
7. **Report**: Output full path to created checklist, item count, and remind user that each run creates a new file. Summarize:
7. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize:
- Focus areas selected
- Depth level
- Actor/timing
- Any explicit user-specified must-have items incorporated
**Important**: Each `/speckit.checklist` command invocation creates a checklist file using short, descriptive names unless file already exists. This allows:
**Important**: Each `/speckit.checklist` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows:
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
- Simple, memorable filenames that indicate checklist purpose

View File

@@ -89,7 +89,7 @@ Execution steps:
- Information is better deferred to planning phase (note internally)
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
- Maximum of 10 total questions across the whole session.
- Maximum of 5 total questions across the whole session.
- Each question must be answerable with EITHER:
- A short multiplechoice selection (25 distinct, mutually exclusive options), OR
- A one-word / shortphrase answer (explicitly constrain: "Answer in <=5 words").

View File

@@ -16,11 +16,13 @@ You **MUST** consider the user input before proceeding (if not empty).
## Outline
You are updating the project constitution at `/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
**Note**: If `.specify/memory/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
Follow this execution flow:
1. Load the existing constitution template at `/memory/constitution.md`.
1. Load the existing constitution at `.specify/memory/constitution.md`.
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
@@ -41,10 +43,10 @@ Follow this execution flow:
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
4. Consistency propagation checklist (convert prior checklist into active validations):
- Read `/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
- Read `/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
- Read `/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
- Read each command file in `/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
@@ -61,7 +63,7 @@ Follow this execution flow:
- Dates ISO format YYYY-MM-DD.
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
7. Write the completed constitution back to `/memory/constitution.md` (overwrite).
7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite).
8. Output a final summary to the user with:
- New version and bump rationale.
@@ -79,4 +81,4 @@ If the user supplies partial updates (e.g., only one principle revision), still
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
Do not create a new template; always operate on the existing `/memory/constitution.md` file.
Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file.

View File

@@ -13,6 +13,40 @@ $ARGUMENTS
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before implementation)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_implement` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter to only hooks where `enabled: true`
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Pre-Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Pre-Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
Wait for the result of the hook command before proceeding to the Outline.
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Outline
1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
@@ -88,7 +122,7 @@ You **MUST** consider the user input before proceeding (if not empty).
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `Makefile`, `config.log`, `.idea/`, `*.log`, `.env*`
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `*.dll`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*`
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
@@ -136,3 +170,32 @@ You **MUST** consider the user input before proceeding (if not empty).
- Report final status with summary of completed work
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_implement` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter to only hooks where `enabled: true`
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently

View File

@@ -75,10 +75,11 @@ You **MUST** consider the user input before proceeding (if not empty).
- Validation rules from requirements
- State transitions if applicable
2. **Generate API contracts** from functional requirements:
- For each user action → endpoint
- Use standard REST/GraphQL patterns
- Output OpenAPI/GraphQL schema to `/contracts/`
2. **Define interface contracts** (if project has external interfaces) → `/contracts/`:
- Identify what interfaces the project exposes to users or other systems
- Document the contract format appropriate for the project type
- Examples: public APIs for libraries, command schemas for CLI tools, endpoints for web services, grammars for parsers, UI contracts for applications
- Skip if project is purely internal (build scripts, one-off tools, etc.)
3. **Agent context update**:
- Run `{AGENT_SCRIPT}`

View File

@@ -9,8 +9,8 @@ handoffs:
prompt: Clarify specification requirements
send: true
scripts:
sh: scripts/bash/create-new-feature.sh --json "{ARGS}"
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
sh: scripts/bash/create-new-feature.sh "{ARGS}"
ps: scripts/powershell/create-new-feature.ps1 "{ARGS}"
---
## User Input
@@ -39,33 +39,14 @@ Given that feature description, do this:
- "Create a dashboard for analytics" → "analytics-dashboard"
- "Fix payment processing timeout bug" → "fix-payment-timeout"
2. **Check for existing branches before creating new one**:
2. **Create the feature branch** by running the script with `--short-name` (and `--json`), and do NOT pass `--number` (the script auto-detects the next globally available number across all branches and spec directories):
a. First, fetch all remote branches to ensure we have the latest information:
```bash
git fetch --all --prune
```
b. Find the highest feature number across all sources for the short-name:
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
c. Determine the next available number:
- Extract all numbers from all three sources
- Find the highest number N
- Use N+1 for the new branch number
d. Run the script `{SCRIPT}` with the calculated number and short-name:
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
- Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"`
- PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
- Bash example: `{SCRIPT} --json --short-name "user-auth" "Add user authentication"`
- PowerShell example: `{SCRIPT} -Json -ShortName "user-auth" "Add user authentication"`
**IMPORTANT**:
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
- Only match branches/directories with the exact short-name pattern
- If no existing branches/directories found with this short-name, start with number 1
- Do NOT pass `--number` the script determines the correct next number automatically
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
- You must only ever run this script once per feature
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
@@ -148,7 +129,7 @@ Given that feature description, do this:
c. **Handle Validation Results**:
- **If all items pass**: Mark checklist complete and proceed to step 6
- **If all items pass**: Mark checklist complete and proceed to step 7
- **If items fail (excluding [NEEDS CLARIFICATION])**:
1. List the failing items and specific issues
@@ -197,8 +178,6 @@ Given that feature description, do this:
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
## General Guidelines
## Quick Guidelines
- Focus on **WHAT** users need and **WHY**.
@@ -235,7 +214,7 @@ When creating this spec from a user prompt:
- Performance targets: Standard web/mobile app expectations unless specified
- Error handling: User-friendly messages with appropriate fallbacks
- Authentication method: Standard session-based or OAuth2 for web apps
- Integration patterns: RESTful APIs unless specified otherwise
- Integration patterns: Use project-appropriate patterns (REST/GraphQL for web services, function calls for libraries, CLI args for tools, etc.)
### Success Criteria Guidelines

View File

@@ -22,20 +22,54 @@ $ARGUMENTS
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before tasks generation)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_tasks` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter to only hooks where `enabled: true`
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Pre-Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Pre-Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
Wait for the result of the hook command before proceeding to the Outline.
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Outline
1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load design documents**: Read from FEATURE_DIR:
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
- **Optional**: data-model.md (entities), contracts/ (API endpoints), research.md (decisions), quickstart.md (test scenarios)
- **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md (test scenarios)
- Note: Not all projects have all documents. Generate tasks based on what's available.
3. **Execute task generation workflow**:
- Load plan.md and extract tech stack, libraries, project structure
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
- If data-model.md exists: Extract entities and map to user stories
- If contracts/ exists: Map endpoints to user stories
- If contracts/ exists: Map interface contracts to user stories
- If research.md exists: Extract decisions for setup tasks
- Generate tasks organized by user story (see Task Generation Rules below)
- Generate dependency graph showing user story completion order
@@ -63,6 +97,35 @@ You **MUST** consider the user input before proceeding (if not empty).
- Suggested MVP scope (typically just User Story 1)
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
6. **Check for extension hooks**: After tasks.md is generated, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_tasks` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter to only hooks where `enabled: true`
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
Context for task generation: {ARGS}
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
@@ -112,13 +175,13 @@ Every task MUST strictly follow this format:
- Map all related components to their story:
- Models needed for that story
- Services needed for that story
- Endpoints/UI needed for that story
- Interfaces/UI needed for that story
- If tests requested: Tests specific to that story
- Mark story dependencies (most stories should be independent)
2. **From Contracts**:
- Map each contract/endpoint → to the user story it serves
- If tests requested: Each contract → contract test task [P] before implementation in that story's phase
- Map each interface contract → to the user story it serves
- If tests requested: Each interface contract → contract test task [P] before implementation in that story's phase
3. **From Data Model**:
- Map each entity to the user story(ies) that need it

View File

@@ -3,7 +3,7 @@
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
## Summary
@@ -22,7 +22,7 @@
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
**Project Type**: [single/web/mobile - determines source structure]
**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Unit tests for Spec Kit."""

View File

@@ -0,0 +1,34 @@
hooks:
before_implement:
- id: pre_test
enabled: true
optional: false
extension: "test-extension"
command: "pre_implement_test"
description: "Test before implement hook execution"
after_implement:
- id: post_test
enabled: true
optional: true
extension: "test-extension"
command: "post_implement_test"
description: "Test after implement hook execution"
prompt: "Would you like to run the post-implement test?"
before_tasks:
- id: pre_tasks_test
enabled: true
optional: false
extension: "test-extension"
command: "pre_tasks_test"
description: "Test before tasks hook execution"
after_tasks:
- id: post_tasks_test
enabled: true
optional: true
extension: "test-extension"
command: "post_tasks_test"
description: "Test after tasks hook execution"
prompt: "Would you like to run the post-tasks test?"

30
tests/hooks/TESTING.md Normal file
View File

@@ -0,0 +1,30 @@
# Testing Extension Hooks
This directory contains a mock project to verify that LLM agents correctly identify and execute hook commands defined in `.specify/extensions.yml`.
## Test 1: Testing `before_tasks` and `after_tasks`
1. Open a chat with an LLM (like GitHub Copilot) in this project.
2. Ask it to generate tasks for the current directory:
> "Please follow `/speckit.tasks` for the `./tests/hooks` directory."
3. **Expected Behavior**:
- Before doing any generation, the LLM should notice the `AUTOMATIC Pre-Hook` in `.specify/extensions.yml` under `before_tasks`.
- It should state it is executing `EXECUTE_COMMAND: pre_tasks_test`.
- It should then proceed to read the `.md` docs and produce a `tasks.md`.
- After generation, it should output the optional `after_tasks` hook (`post_tasks_test`) block, asking if you want to run it.
## Test 2: Testing `before_implement` and `after_implement`
*(Requires `tasks.md` from Test 1 to exist)*
1. In the same (or new) chat, ask the LLM to implement the tasks:
> "Please follow `/speckit.implement` for the `./tests/hooks` directory."
2. **Expected Behavior**:
- The LLM should first check for `before_implement` hooks.
- It should state it is executing `EXECUTE_COMMAND: pre_implement_test` BEFORE doing any actual task execution.
- It should evaluate the checklists and execute the code writing tasks.
- Upon completion, it should output the optional `after_implement` hook (`post_implement_test`) block.
## How it works
The templates for these commands in `templates/commands/tasks.md` and `templates/commands/implement.md` contains strict ordered lists. The new `before_*` hooks are explicitly formulated in a **Pre-Execution Checks** section prior to the outline to ensure they're evaluated first without breaking template step numbers.

3
tests/hooks/plan.md Normal file
View File

@@ -0,0 +1,3 @@
# Test Setup for Hooks
This feature is designed to test if LLMs correctly invoke Spec Kit extensions hooks when generating tasks and implementing code.

1
tests/hooks/spec.md Normal file
View File

@@ -0,0 +1 @@
- **User Story 1:** I want a test script that prints "Hello hooks!".

1
tests/hooks/tasks.md Normal file
View File

@@ -0,0 +1 @@
- [ ] T001 [US1] Create script that prints 'Hello hooks!' in hello.py

View File

@@ -0,0 +1,235 @@
"""Consistency checks for agent configuration across runtime and packaging scripts."""
import re
from pathlib import Path
from specify_cli import AGENT_CONFIG, AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP
from specify_cli.extensions import CommandRegistrar
REPO_ROOT = Path(__file__).resolve().parent.parent
class TestAgentConfigConsistency:
"""Ensure kiro-cli migration stays synchronized across key surfaces."""
def test_runtime_config_uses_kiro_cli_and_removes_q(self):
"""AGENT_CONFIG should include kiro-cli and exclude legacy q."""
assert "kiro-cli" in AGENT_CONFIG
assert AGENT_CONFIG["kiro-cli"]["folder"] == ".kiro/"
assert AGENT_CONFIG["kiro-cli"]["commands_subdir"] == "prompts"
assert "q" not in AGENT_CONFIG
def test_extension_registrar_uses_kiro_cli_and_removes_q(self):
"""Extension command registrar should target .kiro/prompts."""
cfg = CommandRegistrar.AGENT_CONFIGS
assert "kiro-cli" in cfg
assert cfg["kiro-cli"]["dir"] == ".kiro/prompts"
assert "q" not in cfg
def test_extension_registrar_includes_codex(self):
"""Extension command registrar should include codex targeting .codex/prompts."""
cfg = CommandRegistrar.AGENT_CONFIGS
assert "codex" in cfg
assert cfg["codex"]["dir"] == ".codex/prompts"
def test_release_agent_lists_include_kiro_cli_and_exclude_q(self):
"""Bash and PowerShell release scripts should agree on agent key set for Kiro."""
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
sh_match = re.search(r"ALL_AGENTS=\(([^)]*)\)", sh_text)
assert sh_match is not None
sh_agents = sh_match.group(1).split()
ps_match = re.search(r"\$AllAgents = @\(([^)]*)\)", ps_text)
assert ps_match is not None
ps_agents = re.findall(r"'([^']+)'", ps_match.group(1))
assert "kiro-cli" in sh_agents
assert "kiro-cli" in ps_agents
assert "shai" in sh_agents
assert "shai" in ps_agents
assert "agy" in sh_agents
assert "agy" in ps_agents
assert "q" not in sh_agents
assert "q" not in ps_agents
def test_release_ps_switch_has_shai_and_agy_generation(self):
"""PowerShell release builder must generate files for shai and agy agents."""
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
assert re.search(r"'shai'\s*\{.*?\.shai/commands", ps_text, re.S) is not None
assert re.search(r"'agy'\s*\{.*?\.agent/commands", ps_text, re.S) is not None
def test_release_sh_switch_has_shai_and_agy_generation(self):
"""Bash release builder must generate files for shai and agy agents."""
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
assert re.search(r"shai\)\s*\n.*?\.shai/commands", sh_text, re.S) is not None
assert re.search(r"agy\)\s*\n.*?\.agent/commands", sh_text, re.S) is not None
def test_init_ai_help_includes_roo_and_kiro_alias(self):
"""CLI help text for --ai should stay in sync with agent config and alias guidance."""
assert "roo" in AI_ASSISTANT_HELP
for alias, target in AI_ASSISTANT_ALIASES.items():
assert alias in AI_ASSISTANT_HELP
assert target in AI_ASSISTANT_HELP
def test_devcontainer_kiro_installer_uses_pinned_checksum(self):
"""Devcontainer installer should always verify Kiro installer via pinned SHA256."""
post_create_text = (REPO_ROOT / ".devcontainer" / "post-create.sh").read_text(encoding="utf-8")
assert 'KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"' in post_create_text
assert "sha256sum -c -" in post_create_text
assert "KIRO_SKIP_KIRO_INSTALLER_VERIFY" not in post_create_text
def test_release_output_targets_kiro_prompt_dir(self):
"""Packaging and release scripts should no longer emit amazonq artifacts."""
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
gh_release_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-github-release.sh").read_text(encoding="utf-8")
assert ".kiro/prompts" in sh_text
assert ".kiro/prompts" in ps_text
assert ".amazonq/prompts" not in sh_text
assert ".amazonq/prompts" not in ps_text
assert "spec-kit-template-kiro-cli-sh-" in gh_release_text
assert "spec-kit-template-kiro-cli-ps-" in gh_release_text
assert "spec-kit-template-q-sh-" not in gh_release_text
assert "spec-kit-template-q-ps-" not in gh_release_text
def test_agent_context_scripts_use_kiro_cli(self):
"""Agent context scripts should advertise kiro-cli and not legacy q agent key."""
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
assert "kiro-cli" in bash_text
assert "kiro-cli" in pwsh_text
assert "Amazon Q Developer CLI" not in bash_text
assert "Amazon Q Developer CLI" not in pwsh_text
# --- Tabnine CLI consistency checks ---
def test_runtime_config_includes_tabnine(self):
"""AGENT_CONFIG should include tabnine with correct folder and subdir."""
assert "tabnine" in AGENT_CONFIG
assert AGENT_CONFIG["tabnine"]["folder"] == ".tabnine/agent/"
assert AGENT_CONFIG["tabnine"]["commands_subdir"] == "commands"
assert AGENT_CONFIG["tabnine"]["requires_cli"] is True
assert AGENT_CONFIG["tabnine"]["install_url"] is not None
def test_extension_registrar_includes_tabnine(self):
"""CommandRegistrar.AGENT_CONFIGS should include tabnine with correct TOML config."""
from specify_cli.extensions import CommandRegistrar
assert "tabnine" in CommandRegistrar.AGENT_CONFIGS
cfg = CommandRegistrar.AGENT_CONFIGS["tabnine"]
assert cfg["dir"] == ".tabnine/agent/commands"
assert cfg["format"] == "toml"
assert cfg["args"] == "{{args}}"
assert cfg["extension"] == ".toml"
def test_release_agent_lists_include_tabnine(self):
"""Bash and PowerShell release scripts should include tabnine in agent lists."""
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
sh_match = re.search(r"ALL_AGENTS=\(([^)]*)\)", sh_text)
assert sh_match is not None
sh_agents = sh_match.group(1).split()
ps_match = re.search(r"\$AllAgents = @\(([^)]*)\)", ps_text)
assert ps_match is not None
ps_agents = re.findall(r"'([^']+)'", ps_match.group(1))
assert "tabnine" in sh_agents
assert "tabnine" in ps_agents
def test_release_scripts_generate_tabnine_toml_commands(self):
"""Release scripts should generate TOML commands for tabnine in .tabnine/agent/commands."""
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
assert ".tabnine/agent/commands" in sh_text
assert ".tabnine/agent/commands" in ps_text
assert re.search(r"'tabnine'\s*\{.*?\.tabnine/agent/commands", ps_text, re.S) is not None
def test_github_release_includes_tabnine_packages(self):
"""GitHub release script should include tabnine template packages."""
gh_release_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-github-release.sh").read_text(encoding="utf-8")
assert "spec-kit-template-tabnine-sh-" in gh_release_text
assert "spec-kit-template-tabnine-ps-" in gh_release_text
def test_agent_context_scripts_include_tabnine(self):
"""Agent context scripts should support tabnine agent type."""
bash_text = (REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh").read_text(encoding="utf-8")
pwsh_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
assert "tabnine" in bash_text
assert "TABNINE_FILE" in bash_text
assert "tabnine" in pwsh_text
assert "TABNINE_FILE" in pwsh_text
def test_ai_help_includes_tabnine(self):
"""CLI help text for --ai should include tabnine."""
assert "tabnine" in AI_ASSISTANT_HELP
# --- Kimi Code CLI consistency checks ---
def test_kimi_in_agent_config(self):
"""AGENT_CONFIG should include kimi with correct folder and commands_subdir."""
assert "kimi" in AGENT_CONFIG
assert AGENT_CONFIG["kimi"]["folder"] == ".kimi/"
assert AGENT_CONFIG["kimi"]["commands_subdir"] == "skills"
assert AGENT_CONFIG["kimi"]["requires_cli"] is True
def test_kimi_in_extension_registrar(self):
"""Extension command registrar should include kimi using .kimi/skills and SKILL.md."""
cfg = CommandRegistrar.AGENT_CONFIGS
assert "kimi" in cfg
kimi_cfg = cfg["kimi"]
assert kimi_cfg["dir"] == ".kimi/skills"
assert kimi_cfg["extension"] == "/SKILL.md"
def test_kimi_in_release_agent_lists(self):
"""Bash and PowerShell release scripts should include kimi in agent lists."""
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
sh_match = re.search(r"ALL_AGENTS=\(([^)]*)\)", sh_text)
assert sh_match is not None
sh_agents = sh_match.group(1).split()
ps_match = re.search(r"\$AllAgents = @\(([^)]*)\)", ps_text)
assert ps_match is not None
ps_agents = re.findall(r"'([^']+)'", ps_match.group(1))
assert "kimi" in sh_agents
assert "kimi" in ps_agents
def test_kimi_in_powershell_validate_set(self):
"""PowerShell update-agent-context script should include 'kimi' in ValidateSet."""
ps_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
assert validate_set_match is not None
validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1))
assert "kimi" in validate_set_values
def test_kimi_in_github_release_output(self):
"""GitHub release script should include kimi template packages."""
gh_release_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-github-release.sh").read_text(encoding="utf-8")
assert "spec-kit-template-kimi-sh-" in gh_release_text
assert "spec-kit-template-kimi-ps-" in gh_release_text
def test_ai_help_includes_kimi(self):
"""CLI help text for --ai should include kimi."""
assert "kimi" in AI_ASSISTANT_HELP

865
tests/test_ai_skills.py Normal file
View File

@@ -0,0 +1,865 @@
"""
Unit tests for AI agent skills installation.
Tests cover:
- Skills directory resolution for different agents (_get_skills_dir)
- YAML frontmatter parsing and SKILL.md generation (install_ai_skills)
- Cleanup of duplicate command files when --ai-skills is used
- Missing templates directory handling
- Malformed template error handling
- CLI validation: --ai-skills requires --ai
"""
import re
import pytest
import tempfile
import shutil
import yaml
from pathlib import Path
from unittest.mock import patch
import specify_cli
from specify_cli import (
_get_skills_dir,
install_ai_skills,
AGENT_SKILLS_DIR_OVERRIDES,
DEFAULT_SKILLS_DIR,
SKILL_DESCRIPTIONS,
AGENT_CONFIG,
app,
)
# ===== Fixtures =====
@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests."""
tmpdir = tempfile.mkdtemp()
yield Path(tmpdir)
shutil.rmtree(tmpdir)
@pytest.fixture
def project_dir(temp_dir):
"""Create a mock project directory."""
proj_dir = temp_dir / "test-project"
proj_dir.mkdir()
return proj_dir
@pytest.fixture
def templates_dir(project_dir):
"""Create mock command templates in the project's agent commands directory.
This simulates what download_and_extract_template() does: it places
command .md files into project_path/<agent_folder>/commands/.
install_ai_skills() now reads from here instead of from the repo
source tree.
"""
tpl_root = project_dir / ".claude" / "commands"
tpl_root.mkdir(parents=True, exist_ok=True)
# Template with valid YAML frontmatter
(tpl_root / "specify.md").write_text(
"---\n"
"description: Create or update the feature specification.\n"
"handoffs:\n"
" - label: Build Plan\n"
" agent: speckit.plan\n"
"scripts:\n"
" sh: scripts/bash/create-new-feature.sh\n"
"---\n"
"\n"
"# Specify Command\n"
"\n"
"Run this to create a spec.\n",
encoding="utf-8",
)
# Template with minimal frontmatter
(tpl_root / "plan.md").write_text(
"---\n"
"description: Generate implementation plan.\n"
"---\n"
"\n"
"# Plan Command\n"
"\n"
"Plan body content.\n",
encoding="utf-8",
)
# Template with no frontmatter
(tpl_root / "tasks.md").write_text(
"# Tasks Command\n"
"\n"
"Body without frontmatter.\n",
encoding="utf-8",
)
# Template with empty YAML frontmatter (yaml.safe_load returns None)
(tpl_root / "empty_fm.md").write_text(
"---\n"
"---\n"
"\n"
"# Empty Frontmatter Command\n"
"\n"
"Body with empty frontmatter.\n",
encoding="utf-8",
)
return tpl_root
@pytest.fixture
def commands_dir_claude(project_dir):
"""Create a populated .claude/commands directory simulating template extraction."""
cmd_dir = project_dir / ".claude" / "commands"
cmd_dir.mkdir(parents=True, exist_ok=True)
for name in ["speckit.specify.md", "speckit.plan.md", "speckit.tasks.md"]:
(cmd_dir / name).write_text(f"# {name}\nContent here\n")
return cmd_dir
@pytest.fixture
def commands_dir_gemini(project_dir):
"""Create a populated .gemini/commands directory (TOML format)."""
cmd_dir = project_dir / ".gemini" / "commands"
cmd_dir.mkdir(parents=True)
for name in ["speckit.specify.toml", "speckit.plan.toml", "speckit.tasks.toml"]:
(cmd_dir / name).write_text(f'[command]\nname = "{name}"\n')
return cmd_dir
@pytest.fixture
def commands_dir_qwen(project_dir):
"""Create a populated .qwen/commands directory (Markdown format)."""
cmd_dir = project_dir / ".qwen" / "commands"
cmd_dir.mkdir(parents=True, exist_ok=True)
for name in ["speckit.specify.md", "speckit.plan.md", "speckit.tasks.md"]:
(cmd_dir / name).write_text(f"# {name}\nContent here\n")
return cmd_dir
# ===== _get_skills_dir Tests =====
class TestGetSkillsDir:
"""Test the _get_skills_dir() helper function."""
def test_claude_skills_dir(self, project_dir):
"""Claude should use .claude/skills/."""
result = _get_skills_dir(project_dir, "claude")
assert result == project_dir / ".claude" / "skills"
def test_gemini_skills_dir(self, project_dir):
"""Gemini should use .gemini/skills/."""
result = _get_skills_dir(project_dir, "gemini")
assert result == project_dir / ".gemini" / "skills"
def test_tabnine_skills_dir(self, project_dir):
"""Tabnine should use .tabnine/agent/skills/."""
result = _get_skills_dir(project_dir, "tabnine")
assert result == project_dir / ".tabnine" / "agent" / "skills"
def test_copilot_skills_dir(self, project_dir):
"""Copilot should use .github/skills/."""
result = _get_skills_dir(project_dir, "copilot")
assert result == project_dir / ".github" / "skills"
def test_codex_uses_override(self, project_dir):
"""Codex should use the AGENT_SKILLS_DIR_OVERRIDES value."""
result = _get_skills_dir(project_dir, "codex")
assert result == project_dir / ".agents" / "skills"
def test_cursor_agent_skills_dir(self, project_dir):
"""Cursor should use .cursor/skills/."""
result = _get_skills_dir(project_dir, "cursor-agent")
assert result == project_dir / ".cursor" / "skills"
def test_kiro_cli_skills_dir(self, project_dir):
"""Kiro CLI should use .kiro/skills/."""
result = _get_skills_dir(project_dir, "kiro-cli")
assert result == project_dir / ".kiro" / "skills"
def test_unknown_agent_uses_default(self, project_dir):
"""Unknown agents should fall back to DEFAULT_SKILLS_DIR."""
result = _get_skills_dir(project_dir, "nonexistent-agent")
assert result == project_dir / DEFAULT_SKILLS_DIR
def test_all_configured_agents_resolve(self, project_dir):
"""Every agent in AGENT_CONFIG should resolve to a valid path."""
for agent_key in AGENT_CONFIG:
result = _get_skills_dir(project_dir, agent_key)
assert result is not None
assert str(result).startswith(str(project_dir))
# Should always end with "skills"
assert result.name == "skills"
def test_override_takes_precedence_over_config(self, project_dir):
"""AGENT_SKILLS_DIR_OVERRIDES should take precedence over AGENT_CONFIG."""
for agent_key in AGENT_SKILLS_DIR_OVERRIDES:
result = _get_skills_dir(project_dir, agent_key)
expected = project_dir / AGENT_SKILLS_DIR_OVERRIDES[agent_key]
assert result == expected
# ===== install_ai_skills Tests =====
class TestInstallAiSkills:
"""Test SKILL.md generation and installation logic."""
def test_skills_installed_with_correct_structure(self, project_dir, templates_dir):
"""Verify SKILL.md files have correct agentskills.io structure."""
result = install_ai_skills(project_dir, "claude")
assert result is True
skills_dir = project_dir / ".claude" / "skills"
assert skills_dir.exists()
# Check that skill directories were created
skill_dirs = sorted([d.name for d in skills_dir.iterdir() if d.is_dir()])
assert "speckit-plan" in skill_dirs
assert "speckit-specify" in skill_dirs
assert "speckit-tasks" in skill_dirs
assert "speckit-empty_fm" in skill_dirs
# Verify SKILL.md content for speckit-specify
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
assert skill_file.exists()
content = skill_file.read_text()
# Check agentskills.io frontmatter
assert content.startswith("---\n")
assert "name: speckit-specify" in content
assert "description:" in content
assert "compatibility:" in content
assert "metadata:" in content
assert "author: github-spec-kit" in content
assert "source: templates/commands/specify.md" in content
# Check body content is included
assert "# Speckit Specify Skill" in content
assert "Run this to create a spec." in content
def test_generated_skill_has_parseable_yaml(self, project_dir, templates_dir):
"""Generated SKILL.md should contain valid, parseable YAML frontmatter."""
install_ai_skills(project_dir, "claude")
skill_file = project_dir / ".claude" / "skills" / "speckit-specify" / "SKILL.md"
content = skill_file.read_text()
# Extract and parse frontmatter
assert content.startswith("---\n")
parts = content.split("---", 2)
assert len(parts) >= 3
parsed = yaml.safe_load(parts[1])
assert isinstance(parsed, dict)
assert "name" in parsed
assert parsed["name"] == "speckit-specify"
assert "description" in parsed
def test_empty_yaml_frontmatter(self, project_dir, templates_dir):
"""Templates with empty YAML frontmatter (---\\n---) should not crash."""
result = install_ai_skills(project_dir, "claude")
assert result is True
skill_file = project_dir / ".claude" / "skills" / "speckit-empty_fm" / "SKILL.md"
assert skill_file.exists()
content = skill_file.read_text()
assert "name: speckit-empty_fm" in content
assert "Body with empty frontmatter." in content
def test_enhanced_descriptions_used_when_available(self, project_dir, templates_dir):
"""SKILL_DESCRIPTIONS take precedence over template frontmatter descriptions."""
install_ai_skills(project_dir, "claude")
skill_file = project_dir / ".claude" / "skills" / "speckit-specify" / "SKILL.md"
content = skill_file.read_text()
# Parse the generated YAML to compare the description value
# (yaml.safe_dump may wrap long strings across multiple lines)
parts = content.split("---", 2)
parsed = yaml.safe_load(parts[1])
if "specify" in SKILL_DESCRIPTIONS:
assert parsed["description"] == SKILL_DESCRIPTIONS["specify"]
def test_template_without_frontmatter(self, project_dir, templates_dir):
"""Templates without YAML frontmatter should still produce valid skills."""
install_ai_skills(project_dir, "claude")
skill_file = project_dir / ".claude" / "skills" / "speckit-tasks" / "SKILL.md"
assert skill_file.exists()
content = skill_file.read_text()
# Should still have valid SKILL.md structure
assert "name: speckit-tasks" in content
assert "Body without frontmatter." in content
def test_missing_templates_directory(self, project_dir):
"""Returns False when no command templates exist anywhere."""
# No .claude/commands/ exists, and __file__ fallback won't find anything
fake_init = project_dir / "nonexistent" / "src" / "specify_cli" / "__init__.py"
fake_init.parent.mkdir(parents=True, exist_ok=True)
fake_init.touch()
with patch.object(specify_cli, "__file__", str(fake_init)):
result = install_ai_skills(project_dir, "claude")
assert result is False
# Skills directory should not exist
skills_dir = project_dir / ".claude" / "skills"
assert not skills_dir.exists()
def test_empty_templates_directory(self, project_dir):
"""Returns False when commands directory has no .md files."""
# Create empty .claude/commands/
empty_cmds = project_dir / ".claude" / "commands"
empty_cmds.mkdir(parents=True)
# Block the __file__ fallback so it can't find real templates
fake_init = project_dir / "nowhere" / "src" / "specify_cli" / "__init__.py"
fake_init.parent.mkdir(parents=True, exist_ok=True)
fake_init.touch()
with patch.object(specify_cli, "__file__", str(fake_init)):
result = install_ai_skills(project_dir, "claude")
assert result is False
def test_malformed_yaml_frontmatter(self, project_dir):
"""Malformed YAML in a template should be handled gracefully, not crash."""
# Create .claude/commands/ with a broken template
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "broken.md").write_text(
"---\n"
"description: [unclosed bracket\n"
" invalid: yaml: content: here\n"
"---\n"
"\n"
"# Broken\n",
encoding="utf-8",
)
# Should not raise — errors are caught per-file
result = install_ai_skills(project_dir, "claude")
# The broken template should be skipped but not crash the process
assert result is False
def test_additive_does_not_overwrite_other_files(self, project_dir, templates_dir):
"""Installing skills should not remove non-speckit files in the skills dir."""
# Pre-create a custom skill
custom_dir = project_dir / ".claude" / "skills" / "my-custom-skill"
custom_dir.mkdir(parents=True)
custom_file = custom_dir / "SKILL.md"
custom_file.write_text("# My Custom Skill\n")
install_ai_skills(project_dir, "claude")
# Custom skill should still exist
assert custom_file.exists()
assert custom_file.read_text() == "# My Custom Skill\n"
def test_return_value(self, project_dir, templates_dir):
"""install_ai_skills returns True when skills installed, False otherwise."""
assert install_ai_skills(project_dir, "claude") is True
def test_return_false_when_no_templates(self, project_dir):
"""install_ai_skills returns False when no templates found."""
fake_init = project_dir / "missing" / "src" / "specify_cli" / "__init__.py"
fake_init.parent.mkdir(parents=True, exist_ok=True)
fake_init.touch()
with patch.object(specify_cli, "__file__", str(fake_init)):
assert install_ai_skills(project_dir, "claude") is False
def test_non_md_commands_dir_falls_back(self, project_dir):
"""When extracted commands are .toml (e.g. gemini), fall back to repo templates."""
# Simulate gemini template extraction: .gemini/commands/ with .toml files only
cmds_dir = project_dir / ".gemini" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "speckit.specify.toml").write_text('[command]\nname = "specify"\n')
(cmds_dir / "speckit.plan.toml").write_text('[command]\nname = "plan"\n')
# The __file__ fallback should find the real repo templates/commands/*.md
result = install_ai_skills(project_dir, "gemini")
assert result is True
skills_dir = project_dir / ".gemini" / "skills"
assert skills_dir.exists()
# Should have installed skills from the fallback .md templates
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
assert len(skill_dirs) >= 1
# .toml commands should be untouched
assert (cmds_dir / "speckit.specify.toml").exists()
def test_qwen_md_commands_dir_installs_skills(self, project_dir):
"""Qwen now uses Markdown format; skills should install directly from .qwen/commands/."""
cmds_dir = project_dir / ".qwen" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "speckit.specify.md").write_text(
"---\ndescription: Create or update the feature specification.\n---\n\n# Specify\n\nBody.\n"
)
(cmds_dir / "speckit.plan.md").write_text(
"---\ndescription: Generate implementation plan.\n---\n\n# Plan\n\nBody.\n"
)
result = install_ai_skills(project_dir, "qwen")
assert result is True
skills_dir = project_dir / ".qwen" / "skills"
assert skills_dir.exists()
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
assert len(skill_dirs) >= 1
# .md commands should be untouched
assert (cmds_dir / "speckit.specify.md").exists()
assert (cmds_dir / "speckit.plan.md").exists()
@pytest.mark.parametrize("agent_key", [k for k in AGENT_CONFIG.keys() if k != "generic"])
def test_skills_install_for_all_agents(self, temp_dir, agent_key):
"""install_ai_skills should produce skills for every configured agent."""
proj = temp_dir / f"proj-{agent_key}"
proj.mkdir()
# Place .md templates in the agent's commands directory
agent_folder = AGENT_CONFIG[agent_key]["folder"]
cmds_dir = proj / agent_folder.rstrip("/") / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "specify.md").write_text(
"---\ndescription: Test command\n---\n\n# Test\n\nBody.\n"
)
result = install_ai_skills(proj, agent_key)
assert result is True
skills_dir = _get_skills_dir(proj, agent_key)
assert skills_dir.exists()
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
# Kimi uses dot-separator (speckit.specify) to match /skill:speckit.* invocation;
# all other agents use hyphen-separator (speckit-specify).
expected_skill_name = "speckit.specify" if agent_key == "kimi" else "speckit-specify"
assert expected_skill_name in skill_dirs
assert (skills_dir / expected_skill_name / "SKILL.md").exists()
class TestCommandCoexistence:
"""Verify install_ai_skills never touches command files.
Cleanup of freshly-extracted commands for NEW projects is handled
in init(), not in install_ai_skills(). These tests confirm that
install_ai_skills leaves existing commands intact.
"""
def test_existing_commands_preserved_claude(self, project_dir, templates_dir, commands_dir_claude):
"""install_ai_skills must NOT remove pre-existing .claude/commands files."""
# Verify commands exist before
assert len(list(commands_dir_claude.glob("speckit.*"))) == 3
install_ai_skills(project_dir, "claude")
# Commands must still be there — install_ai_skills never touches them
remaining = list(commands_dir_claude.glob("speckit.*"))
assert len(remaining) == 3
def test_existing_commands_preserved_gemini(self, project_dir, templates_dir, commands_dir_gemini):
"""install_ai_skills must NOT remove pre-existing .gemini/commands files."""
assert len(list(commands_dir_gemini.glob("speckit.*"))) == 3
install_ai_skills(project_dir, "gemini")
remaining = list(commands_dir_gemini.glob("speckit.*"))
assert len(remaining) == 3
def test_existing_commands_preserved_qwen(self, project_dir, templates_dir, commands_dir_qwen):
"""install_ai_skills must NOT remove pre-existing .qwen/commands files."""
assert len(list(commands_dir_qwen.glob("speckit.*"))) == 3
install_ai_skills(project_dir, "qwen")
remaining = list(commands_dir_qwen.glob("speckit.*"))
assert len(remaining) == 3
def test_commands_dir_not_removed(self, project_dir, templates_dir, commands_dir_claude):
"""install_ai_skills must not remove the commands directory."""
install_ai_skills(project_dir, "claude")
assert commands_dir_claude.exists()
def test_no_commands_dir_no_error(self, project_dir, templates_dir):
"""No error when installing skills — commands dir has templates and is preserved."""
result = install_ai_skills(project_dir, "claude")
# Should succeed since templates are in .claude/commands/ via fixture
assert result is True
# ===== New-Project Command Skip Tests =====
class TestNewProjectCommandSkip:
"""Test that init() removes extracted commands for new projects only.
These tests run init() end-to-end via CliRunner with
download_and_extract_template patched to create local fixtures.
"""
def _fake_extract(self, agent, project_path, **_kwargs):
"""Simulate template extraction: create agent commands dir."""
agent_cfg = AGENT_CONFIG.get(agent, {})
agent_folder = agent_cfg.get("folder", "")
commands_subdir = agent_cfg.get("commands_subdir", "commands")
if agent_folder:
cmds_dir = project_path / agent_folder.rstrip("/") / commands_subdir
cmds_dir.mkdir(parents=True, exist_ok=True)
(cmds_dir / "speckit.specify.md").write_text("# spec")
def test_new_project_commands_removed_after_skills_succeed(self, tmp_path):
"""For new projects, commands should be removed when skills succeed."""
from typer.testing import CliRunner
runner = CliRunner()
target = tmp_path / "new-proj"
def fake_download(project_path, *args, **kwargs):
self._fake_extract("claude", project_path)
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
patch("specify_cli.ensure_executable_scripts"), \
patch("specify_cli.ensure_constitution_from_template"), \
patch("specify_cli.install_ai_skills", return_value=True) as mock_skills, \
patch("specify_cli.is_git_repo", return_value=False), \
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
result = runner.invoke(app, ["init", str(target), "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"])
assert result.exit_code == 0
# Skills should have been called
mock_skills.assert_called_once()
# Commands dir should have been removed after skills succeeded
cmds_dir = target / ".claude" / "commands"
assert not cmds_dir.exists()
def test_new_project_nonstandard_commands_subdir_removed_after_skills_succeed(self, tmp_path):
"""For non-standard agents, configured commands_subdir should be removed on success."""
from typer.testing import CliRunner
runner = CliRunner()
target = tmp_path / "new-kiro-proj"
def fake_download(project_path, *args, **kwargs):
self._fake_extract("kiro-cli", project_path)
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
patch("specify_cli.ensure_executable_scripts"), \
patch("specify_cli.ensure_constitution_from_template"), \
patch("specify_cli.install_ai_skills", return_value=True) as mock_skills, \
patch("specify_cli.is_git_repo", return_value=False), \
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
result = runner.invoke(app, ["init", str(target), "--ai", "kiro-cli", "--ai-skills", "--script", "sh", "--no-git"])
assert result.exit_code == 0
mock_skills.assert_called_once()
prompts_dir = target / ".kiro" / "prompts"
assert not prompts_dir.exists()
def test_commands_preserved_when_skills_fail(self, tmp_path):
"""If skills fail, commands should NOT be removed (safety net)."""
from typer.testing import CliRunner
runner = CliRunner()
target = tmp_path / "fail-proj"
def fake_download(project_path, *args, **kwargs):
self._fake_extract("claude", project_path)
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
patch("specify_cli.ensure_executable_scripts"), \
patch("specify_cli.ensure_constitution_from_template"), \
patch("specify_cli.install_ai_skills", return_value=False), \
patch("specify_cli.is_git_repo", return_value=False), \
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
result = runner.invoke(app, ["init", str(target), "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"])
assert result.exit_code == 0
# Commands should still exist since skills failed
cmds_dir = target / ".claude" / "commands"
assert cmds_dir.exists()
assert (cmds_dir / "speckit.specify.md").exists()
def test_here_mode_commands_preserved(self, tmp_path, monkeypatch):
"""For --here on existing repos, commands must NOT be removed."""
from typer.testing import CliRunner
runner = CliRunner()
# Create a mock existing project with commands already present
target = tmp_path / "existing"
target.mkdir()
agent_folder = AGENT_CONFIG["claude"]["folder"]
cmds_dir = target / agent_folder.rstrip("/") / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "speckit.specify.md").write_text("# spec")
# --here uses CWD, so chdir into the target
monkeypatch.chdir(target)
def fake_download(project_path, *args, **kwargs):
pass # commands already exist, no need to re-create
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
patch("specify_cli.ensure_executable_scripts"), \
patch("specify_cli.ensure_constitution_from_template"), \
patch("specify_cli.install_ai_skills", return_value=True), \
patch("specify_cli.is_git_repo", return_value=True), \
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
result = runner.invoke(app, ["init", "--here", "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"], input="y\n")
assert result.exit_code == 0
# Commands must remain for --here
assert cmds_dir.exists()
assert (cmds_dir / "speckit.specify.md").exists()
# ===== Skip-If-Exists Tests =====
class TestSkipIfExists:
"""Test that install_ai_skills does not overwrite existing SKILL.md files."""
def test_existing_skill_not_overwritten(self, project_dir, templates_dir):
"""Pre-existing SKILL.md should not be replaced on re-run."""
# Pre-create a custom SKILL.md for speckit-specify
skill_dir = project_dir / ".claude" / "skills" / "speckit-specify"
skill_dir.mkdir(parents=True)
custom_content = "# My Custom Specify Skill\nUser-modified content\n"
(skill_dir / "SKILL.md").write_text(custom_content)
result = install_ai_skills(project_dir, "claude")
# The custom SKILL.md should be untouched
assert (skill_dir / "SKILL.md").read_text() == custom_content
# But other skills should still be installed
assert result is True
assert (project_dir / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
assert (project_dir / ".claude" / "skills" / "speckit-tasks" / "SKILL.md").exists()
def test_fresh_install_writes_all_skills(self, project_dir, templates_dir):
"""On first install (no pre-existing skills), all should be written."""
result = install_ai_skills(project_dir, "claude")
assert result is True
skills_dir = project_dir / ".claude" / "skills"
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
# All 4 templates should produce skills (specify, plan, tasks, empty_fm)
assert len(skill_dirs) == 4
# ===== SKILL_DESCRIPTIONS Coverage Tests =====
class TestSkillDescriptions:
"""Test SKILL_DESCRIPTIONS constants."""
def test_all_known_commands_have_descriptions(self):
"""All standard spec-kit commands should have enhanced descriptions."""
expected_commands = [
"specify", "plan", "tasks", "implement", "analyze",
"clarify", "constitution", "checklist", "taskstoissues",
]
for cmd in expected_commands:
assert cmd in SKILL_DESCRIPTIONS, f"Missing description for '{cmd}'"
assert len(SKILL_DESCRIPTIONS[cmd]) > 20, f"Description for '{cmd}' is too short"
# ===== CLI Validation Tests =====
class TestCliValidation:
"""Test --ai-skills CLI flag validation."""
def test_ai_skills_without_ai_fails(self):
"""--ai-skills without --ai should fail with exit code 1."""
from typer.testing import CliRunner
runner = CliRunner()
result = runner.invoke(app, ["init", "test-proj", "--ai-skills"])
assert result.exit_code == 1
assert "--ai-skills requires --ai" in result.output
def test_ai_skills_without_ai_shows_usage(self):
"""Error message should include usage hint."""
from typer.testing import CliRunner
runner = CliRunner()
result = runner.invoke(app, ["init", "test-proj", "--ai-skills"])
assert "Usage:" in result.output
assert "--ai" in result.output
def test_agy_without_ai_skills_fails(self):
"""--ai agy without --ai-skills should fail with exit code 1."""
from typer.testing import CliRunner
runner = CliRunner()
result = runner.invoke(app, ["init", "test-proj", "--ai", "agy"])
assert result.exit_code == 1
assert "Explicit command support was deprecated in Antigravity version 1.20.5." in result.output
assert "--ai-skills" in result.output
def test_interactive_agy_without_ai_skills_prompts_skills(self, monkeypatch):
"""Interactive selector returning agy without --ai-skills should automatically enable --ai-skills."""
from typer.testing import CliRunner
# Mock select_with_arrows to simulate the user picking 'agy' for AI,
# and return a deterministic default for any other prompts to avoid
# calling the real interactive implementation.
def _fake_select_with_arrows(*args, **kwargs):
options = kwargs.get("options")
if options is None and len(args) >= 1:
options = args[0]
# If the options include 'agy', simulate selecting it.
if isinstance(options, dict) and "agy" in options:
return "agy"
if isinstance(options, (list, tuple)) and "agy" in options:
return "agy"
# For any other prompt, return a deterministic, non-interactive default:
# pick the first option if available.
if isinstance(options, dict) and options:
return next(iter(options.keys()))
if isinstance(options, (list, tuple)) and options:
return options[0]
# If no options are provided, fall back to None (should not occur in normal use).
return None
monkeypatch.setattr("specify_cli.select_with_arrows", _fake_select_with_arrows)
# Mock download_and_extract_template to prevent real HTTP downloads during testing
monkeypatch.setattr("specify_cli.download_and_extract_template", lambda *args, **kwargs: None)
# We need to bypass the `git init` step, wait, it has `--no-git` by default in tests maybe?
runner = CliRunner()
# Create temp dir to avoid directory already exists errors or whatever
with runner.isolated_filesystem():
result = runner.invoke(app, ["init", "test-proj", "--no-git"])
# Interactive selection should NOT raise the deprecation error!
assert result.exit_code == 0
assert "Explicit command support was deprecated" not in result.output
def test_ai_skills_flag_appears_in_help(self):
"""--ai-skills should appear in init --help output."""
from typer.testing import CliRunner
runner = CliRunner()
result = runner.invoke(app, ["init", "--help"])
plain = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
assert "--ai-skills" in plain
assert "agent skills" in plain.lower()
def test_kiro_alias_normalized_to_kiro_cli(self, tmp_path):
"""--ai kiro should normalize to canonical kiro-cli agent key."""
from typer.testing import CliRunner
runner = CliRunner()
target = tmp_path / "kiro-alias-proj"
with patch("specify_cli.download_and_extract_template") as mock_download, \
patch("specify_cli.ensure_executable_scripts"), \
patch("specify_cli.ensure_constitution_from_template"), \
patch("specify_cli.is_git_repo", return_value=False), \
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
result = runner.invoke(
app,
[
"init",
str(target),
"--ai",
"kiro",
"--ignore-agent-tools",
"--script",
"sh",
"--no-git",
],
)
assert result.exit_code == 0
assert mock_download.called
# download_and_extract_template(project_path, ai_assistant, script_type, ...)
assert mock_download.call_args.args[1] == "kiro-cli"
def test_q_removed_from_agent_config(self):
"""Amazon Q legacy key should not remain in AGENT_CONFIG."""
assert "q" not in AGENT_CONFIG
assert "kiro-cli" in AGENT_CONFIG
class TestParameterOrderingIssue:
"""Test fix for GitHub issue #1641: parameter ordering issues."""
def test_ai_flag_consuming_here_flag(self):
"""--ai without value should not consume --here flag (issue #1641)."""
from typer.testing import CliRunner
runner = CliRunner()
# This used to fail with "Must specify project name" because --here was consumed by --ai
result = runner.invoke(app, ["init", "--ai-skills", "--ai", "--here"])
assert result.exit_code == 1
assert "Invalid value for --ai" in result.output
assert "--here" in result.output # Should mention the invalid value
def test_ai_flag_consuming_ai_skills_flag(self):
"""--ai without value should not consume --ai-skills flag."""
from typer.testing import CliRunner
runner = CliRunner()
# This should fail with helpful error about missing --ai value
result = runner.invoke(app, ["init", "--here", "--ai", "--ai-skills"])
assert result.exit_code == 1
assert "Invalid value for --ai" in result.output
assert "--ai-skills" in result.output # Should mention the invalid value
def test_error_message_provides_hint(self):
"""Error message should provide helpful hint about missing value."""
from typer.testing import CliRunner
runner = CliRunner()
result = runner.invoke(app, ["init", "--ai", "--here"])
assert result.exit_code == 1
assert "Hint:" in result.output or "hint" in result.output.lower()
assert "forget to provide a value" in result.output.lower()
def test_error_message_lists_available_agents(self):
"""Error message should list available agents."""
from typer.testing import CliRunner
runner = CliRunner()
result = runner.invoke(app, ["init", "--ai", "--here"])
assert result.exit_code == 1
# Should mention some known agents
output_lower = result.output.lower()
assert any(agent in output_lower for agent in ["claude", "copilot", "gemini"])
def test_ai_commands_dir_consuming_flag(self):
"""--ai-commands-dir without value should not consume next flag."""
from typer.testing import CliRunner
runner = CliRunner()
result = runner.invoke(app, ["init", "myproject", "--ai", "generic", "--ai-commands-dir", "--here"])
assert result.exit_code == 1
assert "Invalid value for --ai-commands-dir" in result.output
assert "--here" in result.output

View File

@@ -0,0 +1,263 @@
"""
Tests for Cursor .mdc frontmatter generation (issue #669).
Verifies that update-agent-context.sh properly prepends YAML frontmatter
to .mdc files so that Cursor IDE auto-includes the rules.
"""
import os
import shutil
import subprocess
import textwrap
import pytest
SCRIPT_PATH = os.path.join(
os.path.dirname(__file__),
os.pardir,
"scripts",
"bash",
"update-agent-context.sh",
)
EXPECTED_FRONTMATTER_LINES = [
"---",
"description: Project Development Guidelines",
'globs: ["**/*"]',
"alwaysApply: true",
"---",
]
requires_git = pytest.mark.skipif(
shutil.which("git") is None,
reason="git is not installed",
)
class TestScriptFrontmatterPattern:
"""Static analysis — no git required."""
def test_create_new_has_mdc_frontmatter_logic(self):
"""create_new_agent_file() must contain .mdc frontmatter logic."""
with open(SCRIPT_PATH, encoding="utf-8") as f:
content = f.read()
assert 'if [[ "$target_file" == *.mdc ]]' in content
assert "alwaysApply: true" in content
def test_update_existing_has_mdc_frontmatter_logic(self):
"""update_existing_agent_file() must also handle .mdc frontmatter."""
with open(SCRIPT_PATH, encoding="utf-8") as f:
content = f.read()
# There should be two occurrences of the .mdc check — one per function
occurrences = content.count('if [[ "$target_file" == *.mdc ]]')
assert occurrences >= 2, (
f"Expected at least 2 .mdc frontmatter checks, found {occurrences}"
)
def test_powershell_script_has_mdc_frontmatter_logic(self):
"""PowerShell script must also handle .mdc frontmatter."""
ps_path = os.path.join(
os.path.dirname(__file__),
os.pardir,
"scripts",
"powershell",
"update-agent-context.ps1",
)
with open(ps_path, encoding="utf-8") as f:
content = f.read()
assert "alwaysApply: true" in content
occurrences = content.count(r"\.mdc$")
assert occurrences >= 2, (
f"Expected at least 2 .mdc frontmatter checks in PS script, found {occurrences}"
)
@requires_git
class TestCursorFrontmatterIntegration:
"""Integration tests using a real git repo."""
@pytest.fixture
def git_repo(self, tmp_path):
"""Create a minimal git repo with the spec-kit structure."""
repo = tmp_path / "repo"
repo.mkdir()
# Init git repo
subprocess.run(
["git", "init"], cwd=str(repo), capture_output=True, check=True
)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=str(repo),
capture_output=True,
check=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=str(repo),
capture_output=True,
check=True,
)
# Create .specify dir with config
specify_dir = repo / ".specify"
specify_dir.mkdir()
(specify_dir / "config.yaml").write_text(
textwrap.dedent("""\
project_type: webapp
language: python
framework: fastapi
database: N/A
""")
)
# Create template
templates_dir = specify_dir / "templates"
templates_dir.mkdir()
(templates_dir / "agent-file-template.md").write_text(
"# [PROJECT NAME] Development Guidelines\n\n"
"Auto-generated from all feature plans. Last updated: [DATE]\n\n"
"## Active Technologies\n\n"
"[EXTRACTED FROM ALL PLAN.MD FILES]\n\n"
"## Project Structure\n\n"
"[ACTUAL STRUCTURE FROM PLANS]\n\n"
"## Development Commands\n\n"
"[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]\n\n"
"## Coding Conventions\n\n"
"[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]\n\n"
"## Recent Changes\n\n"
"[LAST 3 FEATURES AND WHAT THEY ADDED]\n"
)
# Create initial commit
subprocess.run(
["git", "add", "-A"], cwd=str(repo), capture_output=True, check=True
)
subprocess.run(
["git", "commit", "-m", "init"],
cwd=str(repo),
capture_output=True,
check=True,
)
# Create a feature branch so CURRENT_BRANCH detection works
subprocess.run(
["git", "checkout", "-b", "001-test-feature"],
cwd=str(repo),
capture_output=True,
check=True,
)
# Create a spec so the script detects the feature
spec_dir = repo / "specs" / "001-test-feature"
spec_dir.mkdir(parents=True)
(spec_dir / "plan.md").write_text(
"# Test Feature Plan\n\n"
"## Technology Stack\n\n"
"- Language: Python\n"
"- Framework: FastAPI\n"
)
return repo
def _run_update(self, repo, agent_type="cursor-agent"):
"""Run update-agent-context.sh for a specific agent type."""
script = os.path.abspath(SCRIPT_PATH)
result = subprocess.run(
["bash", script, agent_type],
cwd=str(repo),
capture_output=True,
text=True,
timeout=30,
)
return result
def test_new_mdc_file_has_frontmatter(self, git_repo):
"""Creating a new .mdc file must include YAML frontmatter."""
result = self._run_update(git_repo)
assert result.returncode == 0, f"Script failed: {result.stderr}"
mdc_file = git_repo / ".cursor" / "rules" / "specify-rules.mdc"
assert mdc_file.exists(), "Cursor .mdc file was not created"
content = mdc_file.read_text()
lines = content.splitlines()
# First line must be the opening ---
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
# Check all frontmatter lines are present
for expected in EXPECTED_FRONTMATTER_LINES:
assert expected in content, f"Missing frontmatter line: {expected}"
# Content after frontmatter should be the template content
assert "Development Guidelines" in content
def test_existing_mdc_without_frontmatter_gets_it_added(self, git_repo):
"""Updating an existing .mdc file that lacks frontmatter must add it."""
# First, create the file WITHOUT frontmatter (simulating pre-fix state)
cursor_dir = git_repo / ".cursor" / "rules"
cursor_dir.mkdir(parents=True, exist_ok=True)
mdc_file = cursor_dir / "specify-rules.mdc"
mdc_file.write_text(
"# repo Development Guidelines\n\n"
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
"## Active Technologies\n\n"
"- Python + FastAPI (main)\n\n"
"## Recent Changes\n\n"
"- main: Added Python + FastAPI\n"
)
result = self._run_update(git_repo)
assert result.returncode == 0, f"Script failed: {result.stderr}"
content = mdc_file.read_text()
lines = content.splitlines()
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
for expected in EXPECTED_FRONTMATTER_LINES:
assert expected in content, f"Missing frontmatter line: {expected}"
def test_existing_mdc_with_frontmatter_not_duplicated(self, git_repo):
"""Updating an .mdc file that already has frontmatter must not duplicate it."""
cursor_dir = git_repo / ".cursor" / "rules"
cursor_dir.mkdir(parents=True, exist_ok=True)
mdc_file = cursor_dir / "specify-rules.mdc"
frontmatter = (
"---\n"
"description: Project Development Guidelines\n"
'globs: ["**/*"]\n'
"alwaysApply: true\n"
"---\n\n"
)
body = (
"# repo Development Guidelines\n\n"
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
"## Active Technologies\n\n"
"- Python + FastAPI (main)\n\n"
"## Recent Changes\n\n"
"- main: Added Python + FastAPI\n"
)
mdc_file.write_text(frontmatter + body)
result = self._run_update(git_repo)
assert result.returncode == 0, f"Script failed: {result.stderr}"
content = mdc_file.read_text()
# Count occurrences of the frontmatter delimiter
assert content.count("alwaysApply: true") == 1, (
"Frontmatter was duplicated"
)
def test_non_mdc_file_has_no_frontmatter(self, git_repo):
"""Non-.mdc agent files (e.g., Claude) must NOT get frontmatter."""
result = self._run_update(git_repo, agent_type="claude")
assert result.returncode == 0, f"Script failed: {result.stderr}"
claude_file = git_repo / ".claude" / "CLAUDE.md"
if claude_file.exists():
content = claude_file.read_text()
assert not content.startswith("---"), (
"Non-mdc file should not have frontmatter"
)

2339
tests/test_extensions.py Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More