Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
6137c3f046 chore: bump version to 0.8.14 2026-05-26 13:30:49 +00:00
26 changed files with 252 additions and 507 deletions

View File

@@ -2,20 +2,6 @@
<!-- insert new changelog below this comment -->
## [0.8.15] - 2026-05-27
### Changed
- Update Fiction Book Writing preset to v1.8.1 (#2714)
- chore: update memorylint and superb to 1.4.0 (#2690)
- fix: promote post-execution hook dispatch to H2 with directive language (#2713)
- Add Token Budget extension to community catalog (#2712)
- fix: create skills directory on demand during extension/preset install (#2711)
- fix: PS 5.1 compat — replace non-ASCII chars in shipped PowerShell scripts (#2709)
- docs: update security-governance preset to v0.3.0 (#2676)
- Update README.md (#2675)
- chore: release 0.8.14, begin 0.8.15.dev0 development (#2706)
## [0.8.14] - 2026-05-26
### Changed

View File

@@ -35,7 +35,7 @@
- [🔧 Prerequisites](#-prerequisites)
- [📖 Learn More](#-learn-more)
- [📋 Detailed Process](#-detailed-process)
- [💬 Support](#-support)
- [ Support](#-support)
- [🙏 Acknowledgements](#-acknowledgements)
- [📄 License](#-license)
@@ -581,7 +581,7 @@ Once the implementation is complete, test the application and resolve any runtim
---
## 💬 Support
## 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

@@ -116,7 +116,6 @@ The following community-contributed extensions are available in [`catalog.commun
| Team Assign | Assign tasks.md items to human engineers, split into subtasks, and generate a per-engineer workboard | `process` | Read+Write | [spec-kit-team-assign](https://github.com/tarunkumarbhati/spec-kit-team-assign) |
| Time Machine | Retroactively apply the full SDD workflow to existing codebases — analyse, spec, and ship feature-by-feature | `process` | Read+Write | [spec-kit-time-machine](https://github.com/teeyo/spec-kit-time-machine) |
| TinySpec | Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process | `process` | Read+Write | [spec-kit-tinyspec](https://github.com/Quratulain-bilal/spec-kit-tinyspec) |
| Token Budget | Reduces LLM token consumption in Spec Kit workflows: compact artifacts in-place, scope per-phase reading, suppress prose padding, and report token usage | `process` | Read+Write | [spec-kit-token-budget](https://github.com/tinesoft/spec-kit-token-budget) |
| Token Consumption Analyzer | Captures, analyzes, and compares token consumption across SDD workflows | `visibility` | Read-only | [spec-kit-token-analyzer](https://github.com/coderandhiker/spec-kit-token-analyzer) |
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |

View File

@@ -15,7 +15,7 @@ The following community-contributed presets customize how Spec Kit behaves — o
| Claude AskUserQuestion | Upgrades `/speckit.clarify` and `/speckit.checklist` on Claude Code from Markdown-table prompts to the native AskUserQuestion picker, with a recommended option and reasoning on every question | 2 commands | — | [spec-kit-preset-claude-ask-questions](https://github.com/0xrafasec/spec-kit-preset-claude-ask-questions) |
| Cross-Platform Governance | Adds Bash/PowerShell parity, dry-run/WhatIf parity, Unix man-page expectations, PowerShell comment-based help, and Verb-Noun Cmdlet discipline | 8 templates, 3 commands | — | [spec-kit-preset-cross-platform-governance](https://github.com/hindermath/spec-kit-preset-cross-platform-governance) |
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose principles. Supports interactive elements like brainstorming, interview, roleplay and extras like statistics, cover builder and bio command. Export with templates for KDP, D2D etc. | 25 templates, 33 commands, 2 scripts | — | [speckit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose. Supports interactive elements like brainstorming, interview, roleplay and extras like statistics, cover builder and bio command. Export with templates for KDP, D2D etc. | 22 templates, 27 commands, 2 scripts | — | [speckit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Game Narrative Writing | Spec-Driven Development for interactive game narrative pre-production for video games. Authors write in a portable generic format, Twine/Sugarcube (.twee) or Ink (.ink). Covers choice-IF, visual novels, and branching dialogue. Supports Tier 1 mechanic hooks (flag, counter, inventory, timer, trust, currency, npc_state, ending_condition), multi-ending design, series carry-over variable registry, and NPC-focused character architecture. | 22 templates, 36 commands, 2 scripts | — | [speckit-preset-game-narrative-writing](https://github.com/adaumann/speckit-preset-game-narrative-writing) |
| iSAQB Architecture Governance | Adds general iSAQB/CPSA-F and arc42 architecture governance: goals, context, building blocks, runtime and deployment views, quality scenarios, ADRs, risks, and technical debt | 13 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
| Jira Issue Tracking | Overrides `speckit.taskstoissues` to create Jira epics, stories, and tasks instead of GitHub Issues via Atlassian MCP tools | 1 command | — | [spec-kit-preset-jira](https://github.com/luno/spec-kit-preset-jira) |
@@ -23,7 +23,7 @@ The following community-contributed presets customize how Spec Kit behaves — o
| Multi-Repo Branching | Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases | 2 commands | — | [spec-kit-preset-multi-repo-branching](https://github.com/sakitA/spec-kit-preset-multi-repo-branching) |
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Screenwriting | Spec-Driven Development for screenwriting/scriptwriting/tutorials: feature films, television (pilot, episode, limited series), and stage plays. Adapts the Spec Kit workflow to screenplay craft — slug lines, action lines, act breaks, beat sheets, and industry-standard pitch documents. Supports three-act, Save the Cat, TV pilot, network episode, cable/streaming episode, and stage-play structural frameworks. Export to Fountain, FTX, PDF | 26 templates, 32 commands, 1 script | — | [speckit-preset-screenwriting](https://github.com/adaumann/speckit-preset-screenwriting) |
| Security Governance | Adds secure development governance: memory-safe-language preference, secure code generation, NIST SSDF, CWE Top 25, OWASP ASVS, SBOM/AI-SBOM, VEX/SLSA, OpenSSF Scorecard, G7/BSI AI-SBOM target evidence, and EU CRA applicability | 12 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
| Security Governance | Adds secure development governance: memory-safe-language preference, secure code generation, NIST SSDF, CWE Top 25, OWASP ASVS, SBOM/VEX/SLSA, OpenSSF Scorecard, and EU CRA applicability | 12 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure: spec → plan → tasks → implement → deploy | 5 templates, 8 commands | — | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |

View File

@@ -1646,8 +1646,8 @@
"id": "memorylint",
"description": "Agent memory governance tool: Automatically audits and fixes boundary conflicts between AGENTS.md and the constitution.",
"author": "RbBtSn0w",
"version": "1.4.0",
"download_url": "https://github.com/RbBtSn0w/spec-kit-extensions/releases/download/memorylint-v1.4.0/memorylint.zip",
"version": "1.3.0",
"download_url": "https://github.com/RbBtSn0w/spec-kit-extensions/releases/download/memorylint-v1.3.0/memorylint.zip",
"repository": "https://github.com/RbBtSn0w/spec-kit-extensions",
"homepage": "https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/memorylint",
"documentation": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/memorylint/README.md",
@@ -1657,8 +1657,8 @@
"speckit_version": ">=0.5.1"
},
"provides": {
"commands": 2,
"hooks": 3
"commands": 1,
"hooks": 1
},
"tags": [
"memory",
@@ -1671,7 +1671,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-04-09T00:00:00Z",
"updated_at": "2026-05-24T01:06:49Z"
"updated_at": "2026-04-16T13:10:26Z"
},
"multi-model-review": {
"name": "Multi-Model Review",
@@ -2894,8 +2894,8 @@
"id": "superb",
"description": "Orchestrates obra/superpowers skills within the spec-kit SDD workflow. Thin bridge commands delegate to superpowers' authoritative SKILL.md files at runtime (with graceful fallback), while bridge-original commands provide spec-kit-native value. Eight commands cover the full lifecycle: intent clarification, TDD enforcement, task review, verification, critique, systematic debugging, branch completion, and review response. Hook-bound commands fire automatically; standalone commands are invoked when needed.",
"author": "rbbtsn0w",
"version": "1.4.0",
"download_url": "https://github.com/RbBtSn0w/spec-kit-extensions/releases/download/superpowers-bridge-v1.4.0/superpowers-bridge.zip",
"version": "1.3.0",
"download_url": "https://github.com/RbBtSn0w/spec-kit-extensions/releases/download/superpowers-bridge-v1.3.0/superpowers-bridge.zip",
"repository": "https://github.com/RbBtSn0w/spec-kit-extensions",
"homepage": "https://github.com/RbBtSn0w/spec-kit-extensions",
"documentation": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/superpowers-bridge/README.md",
@@ -2913,7 +2913,7 @@
},
"provides": {
"commands": 8,
"hooks": 3
"hooks": 4
},
"tags": [
"methodology",
@@ -2930,7 +2930,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-03-30T00:00:00Z",
"updated_at": "2026-05-24T01:07:34Z"
"updated_at": "2026-04-16T14:08:23Z"
},
"superpowers-bridge": {
"name": "Superpowers Bridge",
@@ -3160,48 +3160,6 @@
"created_at": "2026-05-01T00:00:00Z",
"updated_at": "2026-05-01T00:00:00Z"
},
"token-budget": {
"name": "Token Budget",
"id": "token-budget",
"description": "Reduces LLM token consumption in Spec Kit workflows: compact artifacts in-place, scope per-phase reading, suppress prose padding, and report token usage.",
"author": "Tine Kondo",
"version": "1.0.1",
"download_url": "https://github.com/tinesoft/spec-kit-token-budget/archive/refs/tags/v1.0.1.zip",
"repository": "https://github.com/tinesoft/spec-kit-token-budget",
"homepage": "https://github.com/tinesoft/spec-kit-token-budget",
"documentation": "https://github.com/tinesoft/spec-kit-token-budget/blob/main/README.md",
"changelog": "https://github.com/tinesoft/spec-kit-token-budget/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "python3",
"required": false
},
{
"name": "rtk",
"required": false
}
]
},
"provides": {
"commands": 4,
"hooks": 6
},
"tags": [
"tokens",
"budget",
"context",
"efficiency",
"cost-optimization"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-05-26T00:00:00Z",
"updated_at": "2026-05-26T00:00:00Z"
},
"v-model": {
"name": "V-Model Extension Pack",
"id": "v-model",

View File

@@ -35,7 +35,7 @@ Replace the script to add project-specific Git initialization steps:
## Output
On success:
- `[OK] Git repository initialized`
- ` Git repository initialized`
## Graceful Degradation

View File

@@ -115,7 +115,7 @@ if (Test-Path $configFile) {
}
}
} else {
# No config file -- auto-commit disabled by default
# No config file auto-commit disabled by default
exit 0
}

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env pwsh
# Git-specific common functions for the git extension.
# Extracted from scripts/powershell/common.ps1 -- contains only git-specific
# Extracted from scripts/powershell/common.ps1 contains only git-specific
# branch validation and detection logic.
function Test-HasGit {

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env pwsh
# Git extension: initialize-repo.ps1
# Initialize a Git repository with an initial commit.
# Customizable -- replace this script to add .gitignore templates,
# Customizable replace this script to add .gitignore templates,
# default branch config, git-flow, LFS, signing, etc.
$ErrorActionPreference = 'Stop'
@@ -66,4 +66,4 @@ try {
exit 1
}
Write-Host "[OK] Git repository initialized"
Write-Host " Git repository initialized"

View File

@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-05-26T00:00:00Z",
"updated_at": "2026-05-05T10:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
"presets": {
"a11y-governance": {
@@ -222,11 +222,11 @@
"fiction-book-writing": {
"name": "Fiction Book Writing",
"id": "fiction-book-writing",
"version": "1.8.1",
"description": "Spec-Driven Development for novel and long-form fiction. 33 AI commands from idea to submission: story bible governance, 9 POV modes, all major plot structure frameworks, scene-by-scene drafting with quality gates, audiobook pipeline (SSML/ElevenLabs), cover design, sensitivity review, pacing and prose statistics, and pandoc-based export to DOCX/EPUB/LaTeX. Two style modes: author voice sample extraction or humanized-AI prose with 5 craft profiles. 12 languages supported. Support for offline semantic search.",
"version": "1.7.0",
"description": "Spec-Driven Development for novel and long-form fiction. 27 AI commands from idea to submission: story bible governance, 9 POV modes, all major plot structure frameworks, scene-by-scene drafting with quality gates, audiobook pipeline (SSML/ElevenLabs), cover design, sensitivity review, pacing and prose statistics, and pandoc-based export to DOCX/EPUB/LaTeX. Two style modes: author voice sample extraction or humanized-AI prose with 5 craft profiles. 12 languages supported. Support for offline semantic search.",
"author": "Andreas Daumann",
"repository": "https://github.com/adaumann/speckit-preset-fiction-book-writing",
"download_url": "https://github.com/adaumann/speckit-preset-fiction-book-writing/archive/refs/tags/v1.8.1.zip",
"download_url": "https://github.com/adaumann/speckit-preset-fiction-book-writing/archive/refs/tags/v1.7.0.zip",
"homepage": "https://github.com/adaumann/speckit-preset-fiction-book-writing",
"documentation": "https://github.com/adaumann/speckit-preset-fiction-book-writing/blob/main/fiction-book-writing/README.md",
"license": "MIT",
@@ -234,8 +234,8 @@
"speckit_version": ">=0.5.0"
},
"provides": {
"templates": 25,
"commands": 33,
"templates": 22,
"commands": 27,
"scripts": 2
},
"tags": [
@@ -254,7 +254,7 @@
"language-support"
],
"created_at": "2026-04-09T08:00:00Z",
"updated_at": "2026-05-24T08:00:00Z"
"updated_at": "2026-04-27T08:00:00Z"
},
"game-narrative-writing": {
"name": "Game Narrative Writing",
@@ -472,11 +472,11 @@
"security-governance": {
"name": "Security Governance",
"id": "security-governance",
"version": "0.3.0",
"description": "Adds memory-safe-language preference, secure code generation, ASVS verification, SBOM/AI-SBOM supply-chain transparency, and EU Cyber Resilience Act awareness.",
"version": "0.2.0",
"description": "Adds secure development governance, MSL preference, ASVS verification, supply-chain transparency, and EU CRA awareness.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-security-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-security-governance/archive/refs/tags/v0.3.0.zip",
"download_url": "https://github.com/hindermath/spec-kit-preset-security-governance/archive/refs/tags/v0.2.0.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-security-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-security-governance/blob/main/README.md",
"license": "MIT",
@@ -491,20 +491,11 @@
"security",
"governance",
"msl",
"ssdf",
"asvs",
"supply-chain",
"sbom",
"ai-sbom",
"vex",
"slsa",
"cwe-top-25",
"g7",
"bsi",
"cra"
"supply-chain"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-05-22T00:00:00Z"
"updated_at": "2026-04-27T00:00:00Z"
},
"spec2cloud": {
"name": "Spec2Cloud",

View File

@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.8.15"
version = "0.8.14"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [

View File

@@ -336,10 +336,10 @@ function Get-FeaturePathsEnv {
function Test-FileExists {
param([string]$Path, [string]$Description)
if (Test-Path -Path $Path -PathType Leaf) {
Write-Output " [OK] $Description"
Write-Output " $Description"
return $true
} else {
Write-Output " [FAIL] $Description"
Write-Output " $Description"
return $false
}
}
@@ -347,10 +347,10 @@ function Test-FileExists {
function Test-DirHasFiles {
param([string]$Path, [string]$Description)
if ((Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer } | Select-Object -First 1)) {
Write-Output " [OK] $Description"
Write-Output " $Description"
return $true
} else {
Write-Output " [FAIL] $Description"
Write-Output " $Description"
return $false
}
}
@@ -591,7 +591,7 @@ except Exception:
if ($layerPaths.Count -eq 0) { return $null }
# If the top (highest-priority) layer is replace, it wins entirely --
# If the top (highest-priority) layer is replace, it wins entirely
# lower layers are irrelevant regardless of their strategies.
if ($layerStrategies[0] -eq 'replace') {
return (Get-Content $layerPaths[0] -Raw)

View File

@@ -312,7 +312,7 @@ if (-not $DryRun) {
if ($AllowExistingBranch) {
# If we're already on the branch, continue without another checkout.
if ($currentBranch -eq $branchName) {
# Already on the target branch -- nothing to do
# Already on the target branch nothing to do
} else {
# Otherwise switch to the existing branch instead of failing.
$switchBranchError = git checkout -q $branchName 2>&1 | Out-String

View File

@@ -317,57 +317,6 @@ def _get_skills_dir(project_path: Path, selected_ai: str) -> Path:
return project_path / ".agents" / "skills"
def resolve_active_skills_dir(project_root: Path) -> Path | None:
"""Return the active skills directory, creating it on demand when enabled.
Reads ``.specify/init-options.json`` to determine whether skills are
enabled and which agent was selected. When ``ai_skills`` is true the
directory is created safely (symlink/containment checks); when false
only Kimi's native-skills fallback is honoured (directory must already
exist).
Returns:
The skills directory ``Path``, or ``None`` if skills are not active.
Raises:
ValueError: If the resolved skills path escapes the project root,
a parent component is a symlink, or a path component exists
but is not a directory.
OSError: If the directory cannot be created (e.g. permission denied).
"""
from .shared_infra import _ensure_safe_shared_directory
opts = load_init_options(project_root)
if not isinstance(opts, dict):
opts = {}
agent = opts.get("ai")
if not isinstance(agent, str) or not agent:
return None
ai_skills_enabled = bool(opts.get("ai_skills"))
if not ai_skills_enabled and agent != "kimi":
return None
skills_dir = _get_skills_dir(project_root, agent)
if not ai_skills_enabled:
# Kimi native-skills fallback: use the directory only if it exists.
if not skills_dir.is_dir():
return None
_ensure_safe_shared_directory(
project_root, skills_dir,
create=False, context="agent skills directory",
)
return skills_dir
# ai_skills is explicitly enabled — create the directory safely.
_ensure_safe_shared_directory(
project_root, skills_dir, context="agent skills directory",
)
return skills_dir
def _cli_error_detail(exc: BaseException) -> str:
"""Return a compact one-line exception detail for CLI output."""
detail = str(exc).replace("\n", " ").strip()

View File

@@ -801,24 +801,38 @@ class ExtensionManager:
def _get_skills_dir(self) -> Optional[Path]:
"""Return the active skills directory for extension skill registration.
Delegates to :func:`resolve_active_skills_dir` which reads
init-options, applies the Kimi native-skills fallback, and
safely creates the directory when ``ai_skills`` is enabled.
Reads ``.specify/init-options.json`` to determine whether skills
are enabled and which agent was selected, then delegates to
the module-level ``_get_skills_dir()`` helper for the concrete path.
Returns ``None`` (instead of raising) when the directory cannot
be created due to symlink, containment, or permission issues so
that callers can fall back gracefully.
Kimi is treated as a native-skills agent: if ``ai == "kimi"`` and
``.kimi/skills`` exists, extension installs should still propagate
command skills even when ``ai_skills`` is false.
Returns:
The skills directory ``Path``, or ``None`` if skills were not
enabled and no native-skills fallback applies.
"""
from . import resolve_active_skills_dir, _print_cli_warning
try:
return resolve_active_skills_dir(self.project_root)
except (ValueError, OSError) as exc:
_print_cli_warning(
"resolve", "skills directory", None, exc,
continuing="Continuing without skill registration.",
)
from . import load_init_options, _get_skills_dir as resolve_skills_dir
opts = load_init_options(self.project_root)
if not isinstance(opts, dict):
opts = {}
agent = opts.get("ai")
if not isinstance(agent, str) or not agent:
return None
ai_skills_enabled = bool(opts.get("ai_skills"))
if not ai_skills_enabled and agent != "kimi":
return None
skills_dir = resolve_skills_dir(self.project_root, agent)
if not skills_dir.is_dir():
return None
return skills_dir
def _register_extension_skills(
self,
manifest: ExtensionManifest,

View File

@@ -1097,24 +1097,37 @@ class PresetManager:
def _get_skills_dir(self) -> Optional[Path]:
"""Return the active skills directory for preset skill overrides.
Delegates to :func:`resolve_active_skills_dir` which reads
init-options, applies the Kimi native-skills fallback, and
safely creates the directory when ``ai_skills`` is enabled.
Reads ``.specify/init-options.json`` to determine whether skills
are enabled and which agent was selected, then delegates to
the module-level ``_get_skills_dir()`` helper for the concrete path.
Returns ``None`` (instead of raising) when the directory cannot
be created due to symlink, containment, or permission issues so
that callers can fall back gracefully.
Kimi is treated as a native-skills agent: if ``ai == "kimi"`` and
``.kimi/skills`` exists, presets should still propagate command
overrides to skills even when ``ai_skills`` is false.
Returns:
The skills directory ``Path``, or ``None`` if skills were not
enabled and no native-skills fallback applies.
"""
from . import resolve_active_skills_dir, _print_cli_warning
try:
return resolve_active_skills_dir(self.project_root)
except (ValueError, OSError) as exc:
_print_cli_warning(
"resolve", "skills directory", None, exc,
continuing="Continuing without skill registration.",
)
from . import load_init_options, _get_skills_dir
opts = load_init_options(self.project_root)
if not isinstance(opts, dict):
opts = {}
agent = opts.get("ai")
if not isinstance(agent, str) or not agent:
return None
ai_skills_enabled = bool(opts.get("ai_skills"))
if not ai_skills_enabled and agent != "kimi":
return None
skills_dir = _get_skills_dir(self.project_root, agent)
if not skills_dir.is_dir():
return None
return skills_dir
@staticmethod
def _skill_names_for_command(cmd_name: str) -> tuple[str, str]:
"""Return the modern and legacy skill directory names for a command."""

View File

@@ -88,13 +88,7 @@ def _shared_relative_path(project_path: Path, dest: Path) -> Path:
return rel
def _ensure_safe_shared_directory(
project_path: Path,
directory: Path,
*,
create: bool = True,
context: str = "shared infrastructure directory",
) -> None:
def _ensure_safe_shared_directory(project_path: Path, directory: Path, *, create: bool = True) -> None:
"""Create a shared infra directory without following symlinked parents."""
root = project_path.resolve()
rel = _shared_relative_path(project_path, directory)
@@ -104,24 +98,24 @@ def _ensure_safe_shared_directory(
current = current / part
label = _shared_destination_label(project_path, current)
if current.is_symlink():
raise SymlinkedSharedPathError(f"Refusing to use symlinked {context}: {label}")
raise SymlinkedSharedPathError(f"Refusing to use symlinked shared infrastructure directory: {label}")
if current.exists():
if not current.is_dir():
raise ValueError(f"{context.capitalize()} path is not a directory: {label}")
raise ValueError(f"Shared infrastructure directory path is not a directory: {label}")
try:
current.resolve().relative_to(root)
except (OSError, ValueError):
raise ValueError(f"{context.capitalize()} escapes project root: {label}") from None
raise ValueError(f"Shared infrastructure directory escapes project root: {label}") from None
continue
if not create:
raise ValueError(f"{context.capitalize()} does not exist: {label}")
raise ValueError(f"Shared infrastructure directory does not exist: {label}")
current.mkdir()
if current.is_symlink():
raise SymlinkedSharedPathError(f"Refusing to use symlinked {context}: {label}")
raise SymlinkedSharedPathError(f"Refusing to use symlinked shared infrastructure directory: {label}")
try:
current.resolve().relative_to(root)
except (OSError, ValueError):
raise ValueError(f"{context.capitalize()} escapes project root: {label}") from None
raise ValueError(f"Shared infrastructure directory escapes project root: {label}") from None
def _validate_safe_shared_directory(project_path: Path, directory: Path) -> None:

View File

@@ -197,6 +197,14 @@ Execution steps:
7. Write the updated spec back to `FEATURE_SPEC`.
8. Report completion (after questioning loop ends or early termination):
- Number of questions asked & answered.
- Path to updated spec.
- Sections touched (list names).
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
- If any Outstanding or Deferred remain, recommend whether to proceed to `__SPECKIT_COMMAND_PLAN__` or run `__SPECKIT_COMMAND_CLARIFY__` again later post-plan.
- Suggested next command.
Behavior rules:
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
@@ -209,27 +217,17 @@ Behavior rules:
Context for prioritization: {ARGS}
## Mandatory Post-Execution Hooks
**You MUST complete this section before reporting completion to the user.**
## Post-Execution Checks
**Check for extension hooks (after clarification)**:
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_clarify`, skip to the Completion Report.
- If it exists, read it and look for entries under the `hooks.after_clarify` key.
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report.
- If it exists, read it and look for entries under the `hooks.after_clarify` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- **Optional hook** (`optional: true`):
```
## Extension Hooks
@@ -241,19 +239,12 @@ Check if `.specify/extensions.yml` exists in the project root.
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
## Completion Report
Report completion (after questioning loop ends or early termination):
- Number of questions asked & answered.
- Path to updated spec.
- Sections touched (list names).
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
- If any Outstanding or Deferred remain, recommend whether to proceed to `__SPECKIT_COMMAND_PLAN__` or run `__SPECKIT_COMMAND_CLARIFY__` again later post-plan.
- Suggested next command.
## Done When
- [ ] Spec ambiguities identified and clarifications integrated into spec file
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with questions answered, sections touched, and coverage summary
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently

View File

@@ -168,49 +168,35 @@ You **MUST** consider the user input before proceeding (if not empty).
- Check that implemented features match the original specification
- Validate that tests pass and coverage meets requirements
- Confirm the implementation follows the technical plan
- 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_COMMAND_TASKS__` first to regenerate the task list.
## Mandatory Post-Execution Hooks
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 out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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
**You MUST complete this section before reporting completion to the user.**
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_implement`, skip to the Completion Report.
- 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 to the Completion Report.
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion Report
Report final status with summary of completed work.
## Done When
- [ ] All tasks in tasks.md completed and marked `[X]`
- [ ] Implementation validated against specification, plan, and test coverage
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with summary of completed work
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently

View File

@@ -70,42 +70,36 @@ You **MUST** consider the user input before proceeding (if not empty).
- Phase 1: Update agent context by running the agent script
- Re-evaluate Constitution Check post-design
## Mandatory Post-Execution Hooks
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
**You MUST complete this section before reporting completion to the user.**
5. **Check for extension hooks**: After reporting, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_plan` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_plan`, skip to the Completion Report.
- If it exists, read it and look for entries under the `hooks.after_plan` key.
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report.
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- **Optional hook** (`optional: true`):
```
## Extension Hooks
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion Report
Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Phases
@@ -156,9 +150,3 @@ Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generate
- Use absolute paths for filesystem operations; use project-relative paths for references in documentation and agent context files
- ERROR on gate failures or unresolved clarifications
## Done When
- [ ] Plan workflow executed and design artifacts generated
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with branch, plan path, and generated artifacts

View File

@@ -183,7 +183,7 @@ Given that feature description, do this:
c. **Handle Validation Results**:
- **If all items pass**: Mark checklist complete and proceed to the Mandatory Post-Execution Hooks section
- **If all items pass**: Mark checklist complete and proceed to step 8
- **If items fail (excluding [NEEDS CLARIFICATION])**:
1. List the failing items and specific issues
@@ -228,46 +228,40 @@ Given that feature description, do this:
d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status
## Mandatory Post-Execution Hooks
8. **Report completion** to the user with:
- `SPECIFY_FEATURE_DIRECTORY` — the feature directory path
- `SPEC_FILE` — the spec file path
- Checklist results summary
- Readiness for the next phase (`__SPECKIT_COMMAND_CLARIFY__` or `__SPECKIT_COMMAND_PLAN__`)
**You MUST complete this section before reporting completion to the user.**
9. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_specify` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_specify`, skip to the Completion Report.
- If it exists, read it and look for entries under the `hooks.after_specify` key.
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report.
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- **Optional hook** (`optional: true`):
```
## Extension Hooks
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion Report
Report completion to the user with:
- `SPECIFY_FEATURE_DIRECTORY` — the feature directory path
- `SPEC_FILE` — the spec file path
- Checklist results summary
- Readiness for the next phase (`__SPECKIT_COMMAND_CLARIFY__` or `__SPECKIT_COMMAND_PLAN__`)
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
**NOTE:** Branch creation is handled by the `before_specify` hook (git extension). Spec directory and file creation are always handled by this core command.
@@ -331,9 +325,3 @@ Success criteria must be:
- "Database can handle 1000 TPS" (implementation detail, use user-facing metric)
- "React components render efficiently" (framework-specific)
- "Redis cache hit rate above 80%" (technology-specific)
## Done When
- [ ] Specification written to `SPEC_FILE` and validated against quality checklist
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with feature directory, spec file path, and checklist results

View File

@@ -89,48 +89,42 @@ You **MUST** consider the user input before proceeding (if not empty).
- Parallel execution examples per story
- Implementation strategy section (MVP first, incremental delivery)
## Mandatory Post-Execution Hooks
5. **Report**: Output path to generated tasks.md and summary:
- Total task count
- Task count per user story
- Parallel opportunities identified
- Independent test criteria for each story
- Suggested MVP scope (typically just User Story 1)
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
**You MUST complete this section before reporting completion to the user.**
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 out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_tasks`, skip to the Completion Report.
- 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 to the Completion Report.
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- **Optional hook** (`optional: true`):
```
## Extension Hooks
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion Report
Output path to generated tasks.md and summary:
- Total task count
- Task count per user story
- Parallel opportunities identified
- Independent test criteria for each story
- Suggested MVP scope (typically just User Story 1)
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
**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}
@@ -207,9 +201,3 @@ Every task MUST strictly follow this format:
- Within each story: Tests (if requested) → Models → Services → Endpoints → Integration
- Each phase should be a complete, independently testable increment
- **Final Phase**: Polish & Cross-Cutting Concerns
## Done When
- [ ] tasks.md generated with all phases, task IDs, and file paths
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with task count, story breakdown, and MVP scope

View File

@@ -335,11 +335,10 @@ class TestInitIntegrationFlag:
_install_shared_infra(project, "sh", force=False)
captured = capsys.readouterr()
plain = strip_ansi(captured.out)
assert "already exist and were not updated" in plain
assert "specify init --here --force" in plain
assert "already exist and were not updated" in captured.out
assert "specify init --here --force" in captured.out
# Rich may wrap long lines; normalize whitespace for the second command
normalized = " ".join(plain.split())
normalized = " ".join(captured.out.split())
assert "specify integration upgrade --force" in normalized
def test_shared_infra_warns_when_manifest_cannot_be_loaded(self, tmp_path, capsys):

View File

@@ -7,7 +7,6 @@ import pytest
from typer.testing import CliRunner
from specify_cli import app
from tests.conftest import strip_ansi
runner = CliRunner()
@@ -50,8 +49,7 @@ def _write_invalid_manifest(project, key):
def _integration_list_row_cells(output: str, key: str) -> list[str]:
plain = strip_ansi(output)
row = next(line for line in plain.splitlines() if line.startswith(f"{key}"))
row = next(line for line in output.splitlines() if line.startswith(f"{key}"))
return [cell.strip() for cell in row.split("")[1:-1]]
@@ -162,9 +160,8 @@ class TestIntegrationInstall:
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
plain = strip_ansi(result.output)
assert "already installed" in plain
normalized = " ".join(plain.split())
assert "already installed" in result.output
normalized = " ".join(result.output.split())
assert "specify integration upgrade copilot" in normalized
assert "already the default integration" in normalized
assert "No files were changed" in normalized
@@ -200,10 +197,9 @@ class TestIntegrationInstall:
finally:
os.chdir(old_cwd)
assert result.exit_code != 0
plain = strip_ansi(result.output)
assert "Installed integrations: copilot" in plain
assert "Default integration: copilot" in plain
normalized = " ".join(plain.split())
assert "Installed integrations: copilot" in result.output
assert "Default integration: copilot" in result.output
normalized = " ".join(result.output.split())
assert "To replace the default integration" in normalized
assert "specify integration switch claude" in normalized
assert "To install 'claude' alongside" in normalized
@@ -313,10 +309,9 @@ class TestIntegrationInstall:
finally:
os.chdir(old_cwd)
assert result.exit_code != 0
plain = strip_ansi(result.output)
assert "Installed integrations: copilot" in plain
assert "multi-install safe" in plain
normalized = " ".join(plain.split())
assert "Installed integrations: copilot" in result.output
assert "multi-install safe" in result.output
normalized = " ".join(result.output.split())
assert "To replace the default integration" in normalized
assert "specify integration switch claude" in normalized
assert "To install 'claude' alongside" in normalized

View File

@@ -173,32 +173,24 @@ class TestExtensionManagerGetSkillsDir:
assert result == skills_dir
def test_returns_none_when_no_ai_skills(self, no_skills_project):
"""Should return None when ai_skills is false and not create the dir."""
"""Should return None when ai_skills is false."""
manager = ExtensionManager(no_skills_project)
result = manager._get_skills_dir()
assert result is None
# Ensure the directory was NOT created on disk
from specify_cli import _get_skills_dir as resolve_skills_dir
skills_path = resolve_skills_dir(no_skills_project, "claude")
assert not skills_path.exists()
def test_returns_none_when_no_init_options(self, project_dir):
"""Should return None when init-options.json is missing and not create any dir."""
"""Should return None when init-options.json is missing."""
manager = ExtensionManager(project_dir)
result = manager._get_skills_dir()
assert result is None
# No agent skills directory should have been created
assert not (project_dir / ".claude" / "skills").exists()
assert not (project_dir / ".agents" / "skills").exists()
def test_creates_skills_dir_on_demand(self, project_dir):
"""Should create skills dir when ai_skills is enabled but dir is missing."""
def test_returns_none_when_skills_dir_missing(self, project_dir):
"""Should return None when skills dir doesn't exist on disk."""
_create_init_options(project_dir, ai="claude", ai_skills=True)
# Don't create the skills directory — _get_skills_dir should do it
# Don't create the skills directory
manager = ExtensionManager(project_dir)
result = manager._get_skills_dir()
assert result is not None
assert result.is_dir()
assert result is None
def test_returns_kimi_skills_dir_when_ai_skills_disabled(self, project_dir):
"""Kimi should still use its native skills dir when ai_skills is false."""
@@ -468,38 +460,6 @@ class TestExtensionSkillRegistration:
assert "speckit-missing-cmd-ext-exists" in metadata["registered_skills"]
assert "speckit-missing-cmd-ext-ghost" not in metadata["registered_skills"]
@pytest.mark.parametrize("ai", ["claude", "codex"])
def test_skills_registered_when_dir_missing(self, project_dir, temp_dir, ai):
"""Extension add should create skills dir on demand and register skills.
Regression test for https://github.com/github/spec-kit/issues/2682:
when an extension is installed before the agent skills directory exists,
skills must still be materialized (the directory is created on demand).
"""
_create_init_options(project_dir, ai=ai, ai_skills=True)
# Deliberately do NOT create the skills directory
ext_dir = _create_extension_dir(temp_dir, ext_id="early-ext")
manager = ExtensionManager(project_dir)
manifest = manager.install_from_directory(
ext_dir, "0.1.0", register_commands=False
)
# Skills dir should have been created automatically
from specify_cli import _get_skills_dir as resolve_skills_dir
skills_dir = resolve_skills_dir(project_dir, ai)
assert skills_dir.is_dir()
# SKILL.md files should exist
assert (skills_dir / "speckit-early-ext-hello" / "SKILL.md").exists()
assert (skills_dir / "speckit-early-ext-world" / "SKILL.md").exists()
# Registry should record them
metadata = manager.registry.get(manifest.id)
assert len(metadata["registered_skills"]) == 2
assert "speckit-early-ext-hello" in metadata["registered_skills"]
assert "speckit-early-ext-world" in metadata["registered_skills"]
# ===== Extension Skill Unregistration Tests =====

View File

@@ -1,54 +0,0 @@
"""Regression tests for PowerShell 5.1 compatibility (GitHub issue #2680).
PowerShell 5.1 (built-in on Windows) defaults to the system's legacy encoding
when reading .ps1 files. Non-ASCII characters in UTF-8-encoded scripts cause
parse errors because multi-byte sequences are misinterpreted as individual bytes.
These tests ensure that all shipped .ps1 files remain ASCII-only so they work
on both PowerShell 5.1 and 7+.
"""
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
# All directories that contain shipped PowerShell scripts.
_PS1_DIRS = [
REPO_ROOT / "scripts" / "powershell",
REPO_ROOT / "extensions" / "git" / "scripts" / "powershell",
]
def _collect_ps1_files():
"""Yield all .ps1 files under the known script directories."""
for d in _PS1_DIRS:
if d.is_dir():
yield from sorted(d.rglob("*.ps1"))
_PS1_FILES = list(_collect_ps1_files())
@pytest.mark.parametrize("ps1_file", _PS1_FILES, ids=lambda p: str(p.relative_to(REPO_ROOT)))
def test_ps1_file_is_ascii_only(ps1_file: Path):
"""Every .ps1 file must contain only ASCII characters (PS 5.1 compat)."""
content = ps1_file.read_bytes()
non_ascii = [
(i + 1, byte)
for i, byte in enumerate(content)
if byte > 127
]
assert not non_ascii, (
f"{ps1_file.relative_to(REPO_ROOT)} contains non-ASCII bytes "
f"(PowerShell 5.1 incompatible): "
f"first at byte offset {non_ascii[0][0]} (0x{non_ascii[0][1]:02x})"
)
def test_ps1_files_discovered():
"""Sanity check: at least the known script files are found."""
names = {p.name for p in _PS1_FILES}
assert "common.ps1" in names
assert "initialize-repo.ps1" in names