* feat(cli): embed core pack in wheel + offline-first init (#1711, #1752)
Bundle templates, commands, and scripts inside the specify-cli wheel so
that `specify init` works without any network access by default.
Changes:
- pyproject.toml: add hatchling force-include for core_pack assets; bump
version to 0.2.1
- __init__.py: add _locate_core_pack(), _generate_agent_commands() (Python
port of generate_commands() shell function), and scaffold_from_core_pack();
modify init() to scaffold from bundled assets by default; add --from-github
flag to opt back in to the GitHub download path
- release.yml: build wheel during CI release job
- create-github-release.sh: attach .whl as a release asset
- docs/installation.md: add Enterprise/Air-Gapped Installation section
- README.md: add Option 3 enterprise install with accurate offline story
Closes#1711
Addresses #1752
* fix(tests): update kiro alias test for offline-first scaffold path
* feat(cli): invoke bundled release script at runtime for offline scaffold
- Embed release scripts (bash + PowerShell) in wheel via pyproject.toml
- Replace Python _generate_agent_commands() with subprocess invocation of
the canonical create-release-packages.sh, guaranteeing byte-for-byte
parity between 'specify init --offline' and GitHub release ZIPs
- Fix macOS bash 3.2 compat in release script: replace cp --parents,
local -n (nameref), and mapfile with POSIX-safe alternatives
- Fix _TOML_AGENTS: remove qwen (uses markdown per release script)
- Rename --from-github to --offline (opt-in to bundled assets)
- Add _locate_release_script() for cross-platform script discovery
- Update tests: remove bash 4+/GNU coreutils requirements, handle
Kimi directory-per-skill layout, 576 tests passing
- Update CHANGELOG and docs/installation.md
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(offline): error out if --offline fails instead of falling back to network
- _locate_core_pack() docstring now accurately describes that it only
finds wheel-bundled core_pack/; source-checkout fallback lives in callers
- init() --offline + no bundled assets now exits with a clear error
(previously printed a warning and silently fell back to GitHub download)
- init() scaffold failure under --offline now exits with an error
instead of retrying via download_and_extract_template
Addresses reviewer comment: https://github.com/github/spec-kit/pull/1803
* fix(offline): address PR review comments
- fix(shell): harden validate_subset against glob injection in case patterns
- fix(shell): make GENRELEASES_DIR overridable via env var for test isolation
- fix(cli): probe pwsh then powershell on Windows instead of hardcoding pwsh
- fix(cli): remove unreachable fallback branch when --offline fails
- fix(cli): improve --offline error message with common failure causes
- fix(release): move wheel build step after create-release-packages.sh
- fix(docs): add --offline to installation.md air-gapped example
- fix(tests): remove unused genreleases_dir param from _run_release_script
- fix(tests): rewrite parity test to run one agent at a time with isolated
temp dirs, preventing cross-agent interference from rm -rf
* fix(offline): address second round of review comments
- fix(shell): replace case-pattern membership with explicit loop + == check
for unambiguous glob-safety in validate_subset()
- fix(cli): require pwsh (PowerShell 7) only; drop powershell (PS5) fallback
since the bundled script uses #requires -Version 7.0
- fix(cli): add bash and zip preflight checks in scaffold_from_core_pack()
with clear error messages if either is missing
- fix(build): list individual template files in pyproject.toml force-include
to avoid duplicating templates/commands/ in the wheel
* fix(offline): address third round of review comments
- Add 120s timeout to subprocess.run in scaffold_from_core_pack to prevent
indefinite hangs during offline scaffolding
- Add test_pyproject_force_include_covers_all_templates to catch missing
template files in wheel bundling
- Tighten kiro alias test to assert specific scaffold path (download vs offline)
* fix(offline): address Copilot review round 4
- fix(offline): use handle_vscode_settings() merge for --here --offline
to prevent data loss on existing .vscode/settings.json
- fix(release): glob wheel filename in create-github-release.sh instead
of hardcoding version, preventing upload failures on version mismatch
- docs(release): add comment noting pyproject.toml version is synced by
release-trigger.yml before the tag is pushed
* fix(offline): address review round 5 + offline bundle ZIP
- fix(offline): pwsh-only, no powershell.exe fallback; clarify error message
- fix(offline): tighten _has_bundled to check scripts dir for source checkouts
- feat(release): build specify-bundle-v*.zip with all deps at release time
- feat(release): attach offline bundle ZIP to GitHub release assets
- docs: simplify air-gapped install to single ZIP download from releases
- docs: add Windows PowerShell 7+ (pwsh) requirement note
* fix(tests): session-scoped scaffold cache + timeout + dead code removal
- Add timeout=300 and returncode check to _run_release_script() to fail
fast with clear output on script hangs or failures
- Remove unused import specify_cli, _SOURCE_TEMPLATES, bundled_project fixture
- Add session-scoped scaffolded_sh/scaffolded_ps fixtures that scaffold
once per agent and reuse the output directory across all invariant tests
- Reduces test_core_pack_scaffold runtime from ~175s to ~51s (3.4x faster)
- Parity tests still scaffold independently for isolation
* fix(offline): remove wheel from release, update air-gapped docs to use pip download
* fix(tests): handle codex skills layout and iflow agent in scaffold tests
Codex now uses create_skills() with hyphenated separator (speckit-plan/SKILL.md)
instead of generate_commands(). Update _SKILL_AGENTS, _expected_ext, and
_list_command_files to handle both codex ('-') and kimi ('.') skill agents.
Also picks up iflow as a new testable agent automatically via AGENT_CONFIG.
* fix(offline): require wheel core_pack for --offline, remove source-checkout fallback
--offline now strictly requires _locate_core_pack() to find the wheel's
bundled core_pack/ directory. Source-checkout fallbacks are no longer
accepted at the init() level — if core_pack/ is missing, the CLI errors
out with a clear message pointing to the installation docs.
scaffold_from_core_pack() retains its internal source-checkout fallbacks
so parity tests can call it directly from a source checkout.
* fix(offline): remove stale [Unreleased] CHANGELOG section, scope httpx.Client to download path
- Remove entire [Unreleased] section — CHANGELOG is auto-generated at release
- Move httpx.Client into use_github branch with context manager so --offline
path doesn't allocate an unused network client
* fix(offline): remove dead --from-github flag, fix typer.Exit handling, add page templates validation
- Remove unused --from-github CLI option and docstring example
- Add (typer.Exit, SystemExit) re-raise before broad except Exception
to prevent duplicate error panel on offline scaffold failure
- Validate page templates directory exists in scaffold_from_core_pack()
to fail fast on incomplete wheel installs
- Fix ruff lint: remove unused shutil import, remove f-prefix on
strings without placeholders in test_core_pack_scaffold.py
* docs(offline): add v0.6.0 deprecation notice with rationale
- Help text: note bundled assets become default in v0.6.0
- Docstring: explain why GitHub download is being retired (no network
dependency, no proxy/firewall issues, guaranteed version match)
- Runtime nudge: when bundled assets are available but user takes the
GitHub download path, suggest --offline with rationale
- docs/installation.md: add deprecation notice with full rationale
* fix(offline): allow --offline in source checkouts, fix CHANGELOG truncation
- Simplify use_github logic: use_github = not offline (let
scaffold_from_core_pack handle fallback to source-checkout paths)
- Remove hard-fail when core_pack/ is absent — scaffold_from_core_pack
already falls back to repo-root templates/scripts/commands
- Fix truncated 'skill…' → 'skills' in CHANGELOG.md
* fix(offline): sandbox GENRELEASES_DIR and clean up on failure
- Pin GENRELEASES_DIR to temp dir in scaffold_from_core_pack() so a
user-exported value cannot redirect output or cause rm -rf outside
the sandbox
- Clean up partial project directory on --offline scaffold failure
(same behavior as the GitHub-download failure path)
* fix(tests): use shutil.which for bash discovery, add ps parity tests
- _find_bash() now tries shutil.which('bash') first so non-standard
install locations (Nix, custom CI images) are found
- Parametrize parity test over both 'sh' and 'ps' script types to
ensure PowerShell variant stays byte-for-byte identical to release
script output (353 scaffold tests, 810 total)
* fix(tests): parse pyproject.toml with tomllib, remove unused fixture
- Use tomllib to parse force-include keys from the actual TOML table
instead of raw substring search (avoids false positives)
- Remove unused source_template_stems fixture from
test_scaffold_command_dir_location
* fix: guard GENRELEASES_DIR against unsafe values, update docstring
- Add safety check in create-release-packages.sh: reject empty, '/',
'.', '..' values for GENRELEASES_DIR before rm -rf
- Strip trailing slash to avoid path surprises
- Update scaffold_from_core_pack() docstring to accurately describe
all failure modes (not just 'assets not found')
* fix: harden GENRELEASES_DIR guard, cache parity tests, safe iterdir
- Reject '..' path segments in GENRELEASES_DIR to prevent traversal
- Session-cache both scaffold and release-script results in parity
tests — runtime drops from ~74s to ~45s (40% faster)
- Guard cmd_dir.iterdir() in assertion message against missing dirs
* fix(tests): exclude YAML frontmatter source metadata from path rewrite check
The codex and kimi SKILL.md files have 'source: templates/commands/...'
in their YAML frontmatter — this is provenance metadata, not a runtime
path that needs rewriting. Strip frontmatter before checking for bare
scripts/ and templates/ paths.
* fix(offline): surface scaffold failure detail in error output
When --offline scaffold fails, look up the tracker's 'scaffold' step
detail and print it alongside the generic error message so users see
the specific root cause (e.g. missing zip/pwsh, script stderr).
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* chore: bump version to 0.3.2
* fix: correct changelog generation — use tag sort instead of git describe, remove duplicate entries
- Replace git describe --tags --abbrev=0 with git tag --sort=-version:refname
to find the correct previous tag (git describe misses tags on unmerged
release branches)
- Change changelog section heading from '### Changed' to '### Changes'
- Remove duplicate entries from 0.3.2 that belonged to prior releases
- Clean up changelog preamble and stale entries
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: bump version to 0.3.0
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* 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
* 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>
* 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>
* 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>
* 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>
- 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.
- 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>
* 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>
* 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>