Compare commits

...

6 Commits

Author SHA1 Message Date
github-actions[bot]
2c9bf5a542 Add Quality Gates (Enforcement Layer) extension to community catalog
Add gates extension submitted by @schwichtgit to:
- extensions/catalog.community.json (alphabetical order, between fx-to-dotnet and github-issues)
- docs/community/extensions.md community extensions table

Closes #3414

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 18:20:14 +00:00
Manfred Riem
d075b27360 test: pin interpreter probe so py-template render test passes on Windows (#3428)
test_template_renders_python_invocation monkeypatches shutil.which to
return /usr/bin/python3, but on Windows resolve_python_interpreter guards
the which() result with a real _interpreter_runs subprocess probe (#3304).
The mocked /usr/bin/python3 path does not exist on a Windows runner, so the
probe fails, the resolver falls back to sys.executable (a ...python.exe
path), and the python3-anchored regex assertion fails.

Patch _interpreter_runs to return True in the _pin_interpreter fixture so
the resolved interpreter token stays python3 across all platforms, keeping
the #3304 production guard intact while making the assertion deterministic.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 12:21:50 -05:00
Marsel Safin
eedf73f714 feat(workflows): make shell step timeout configurable (#3404)
* feat(workflows): make shell step timeout configurable

The shell step hardcoded a 300s subprocess timeout, killing any
legitimate long-running QA command. Read an optional timeout field
(seconds, positive integer, default 300) and validate it.

Fixes #3327

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

* style: multi-line timeout validation, assert status in default test

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

* Guard against unvalidated timeout in ShellStep.execute()

The engine does not auto-validate step config, so a string or null
timeout would reach subprocess.run() and crash the run with a
TypeError. Fall back to the 300s default for malformed values,
mirroring how the engine treats unvalidated continue_on_error.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 11:06:45 -05:00
Marsel Safin
292eaa6c98 fix: find plans in nested spec directories (#3405)
* fix: find plans in nested spec directories

The agent-context mtime fallback used a one-level specs/*/plan.md
glob, so scoped layouts (specs/<scope>/<feature>/plan.md via
SPECIFY_FEATURE_DIRECTORY) never matched and the SPECKIT block was
written without a plan path. Recurse in both script variants and
update the command doc wording.

Fixes #3024

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

* refactor: pick newest plan with max(), align doc wording

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 08:53:54 -05:00
Marsel Safin
55da30c66d feat(templates): add py: lines to command templates' scripts frontmatter (#3403)
* feat(templates): add py: lines to command templates' scripts frontmatter

Every templates/commands/*.md with a scripts: block now declares a py:
variant so --script py renders a Python invocation via the existing
interpreter-prefixing in process_template.

Fixes #3283

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

* fix: install scripts/python for the py script type

install_shared_infra mapped every non-sh script type to powershell, so
--script py rendered invocations pointing at files that were never
installed. Map py to the python variant dir and skip __pycache__
artifacts during the copy.

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

* test: read templates with explicit utf-8 encoding

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

* fix: keep py: lines standalone, enforce script existence in tests

Drop the plan/tasks py: lines that referenced scripts shipping in the
core port (#3280); they move to that PR. Tests now assert every py:
line points at a script the repo ships, so a dangling reference can
never merge green.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 08:51:20 -05:00
Manfred Riem
062418093d chore: release 0.12.9, begin 0.12.10.dev0 development (#3426)
* chore: bump version to 0.12.9

* chore: begin 0.12.10.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-09 08:30:20 -05:00
19 changed files with 561 additions and 165 deletions

View File

@@ -2,6 +2,21 @@
<!-- insert new changelog below this comment -->
## [0.12.9] - 2026-07-09
### Changed
- fix(integrations): skip Windows Store python3 alias stub in resolve_python_interpreter (#3385)
- fix(integrations): escape control characters in SKILL.md frontmatter (#3399)
- fix(workflows): apply chained expression filters left-to-right (#3339)
- fix(scripts): resolve invoke_separator by parse success, not python3 availability (#3304) (#3320)
- fix(shared-infra): refresh_shared_templates preserves recovered user files (#3378)
- fix(agents): resolve skill placeholders in Goose (yaml) command output (#3374)
- fix(bundler): enforce version pin on bundled preset/extension installs (#3377)
- Update Golden Demo extension to v0.3.0 (#3394)
- test: isolate integration test home (#3144)
- chore: release 0.12.8, begin 0.12.9.dev0 development (#3410)
## [0.12.8] - 2026-07-08
### Changed

View File

@@ -97,6 +97,7 @@ The following community-contributed extensions are available in [`catalog.commun
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |
| QA Testing Extension | Systematic QA testing with browser-driven or CLI-based validation of acceptance criteria from spec | `code` | Read-only | [spec-kit-qa](https://github.com/arunt14/spec-kit-qa) |
| Quality Gates (Enforcement Layer) | Deterministic quality enforcement for Spec Kit projects at three boundaries — agent hooks, git pre-commit, CI — with one policy file and provable enforcement (attestations, canaries, verified parity). | `process` | Read+Write | [spec-gates](https://github.com/schwichtgit/spec-gates) |
| RAG Azure Builder | Spec Kit extension for onboarding and operating an Azure RAG stack with guided workflows. | `process` | Read+Write | [spec-kit-extension-rag-azure-builder](https://github.com/Sertxito/spec-kit-extension-rag-azure-builder) |
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss-Projects/spec-kit-ralph) |
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |

View File

@@ -15,7 +15,7 @@ The script reads the agent-context extension config at
- `context_files` — optional project-relative paths for multiple coding agent context files. When non-empty, the script updates each listed file and the list takes precedence over `context_file`.
- `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing.
It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/<feature>/plan.md`).
It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/**/plan.md`, any depth).
If `context_files` and `context_file` are empty, the command reports nothing to do and exits successfully. Context file paths must stay project-relative; absolute paths, Windows drive paths, backslash separators, and `..` path segments are rejected.
@@ -24,4 +24,4 @@ If `context_files` and `context_file` are empty, the command reports nothing to
- **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]`
- **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]`
When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`.
When `plan_path` is omitted, the script auto-detects the most recently modified `specs/**/plan.md` (any depth, so scoped layouts like `specs/<scope>/<feature>/plan.md` are found).

View File

@@ -12,7 +12,7 @@
#
# When `plan_path` is omitted, the script derives it from `.specify/feature.json`
# (written by /speckit-specify). Falls back to the most recently modified
# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet.
# `specs/**/plan.md` only when feature.json is absent or its plan does not exist yet.
set -euo pipefail
@@ -307,14 +307,14 @@ import sys
from pathlib import Path
root = Path(sys.argv[1]).resolve()
specs = root / "specs"
plans = sorted(
specs.glob("*/plan.md"),
plan = max(
specs.glob("**/plan.md"),
key=lambda p: p.stat().st_mtime,
reverse=True,
default=None,
)
if plans:
if plan:
try:
print(plans[0].relative_to(root).as_posix())
print(plan.relative_to(root).as_posix())
except ValueError:
print("")
else:

View File

@@ -12,7 +12,7 @@
#
# When `plan_path` is omitted, the script derives it from `.specify/feature.json`
# (written by /speckit-specify). Falls back to the most recently modified
# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet.
# `specs/**/plan.md` only when feature.json is absent or its plan does not exist yet.
[CmdletBinding()]
param(
@@ -426,9 +426,7 @@ if (-not $PlanPath) {
if (-not $PlanPath) {
try {
$specsDir = Join-Path $ProjectRoot 'specs'
$candidate = Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue |
ForEach-Object { Get-Item -LiteralPath (Join-Path $_.FullName 'plan.md') -ErrorAction SilentlyContinue } |
Where-Object { $_ } |
$candidate = Get-ChildItem -Path $specsDir -Recurse -File -Filter 'plan.md' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($candidate) {

View File

@@ -1,43 +1,8 @@
{
"schema_version": "1.0",
"updated_at": "2026-07-07T00:00:00Z",
"updated_at": "2026-07-09T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"aide": {
"name": "AI-Driven Engineering (AIDE)",
"id": "aide",
"description": "A structured 7-step workflow for building new projects from scratch with AI assistants — from vision through implementation.",
"author": "mnriem",
"version": "1.0.0",
"download_url": "https://github.com/mnriem/spec-kit-extensions/releases/download/aide-v1.0.0/aide.zip",
"repository": "https://github.com/mnriem/spec-kit-extensions",
"homepage": "https://github.com/mnriem/spec-kit-extensions",
"documentation": "https://github.com/mnriem/spec-kit-extensions/blob/main/aide/README.md",
"changelog": "https://github.com/mnriem/spec-kit-extensions/blob/main/aide/CHANGELOG.md",
"license": "MIT",
"category": "process",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.2.0"
},
"provides": {
"commands": 7,
"hooks": 0
},
"tags": [
"workflow",
"project-management",
"ai-driven",
"new-project",
"planning",
"experimental"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-18T00:00:00Z",
"updated_at": "2026-03-18T00:00:00Z"
},
"agent-assign": {
"name": "Agent Assign",
"id": "agent-assign",
@@ -145,6 +110,41 @@
"created_at": "2026-05-04T00:00:00Z",
"updated_at": "2026-05-04T00:00:00Z"
},
"aide": {
"name": "AI-Driven Engineering (AIDE)",
"id": "aide",
"description": "A structured 7-step workflow for building new projects from scratch with AI assistants — from vision through implementation.",
"author": "mnriem",
"version": "1.0.0",
"download_url": "https://github.com/mnriem/spec-kit-extensions/releases/download/aide-v1.0.0/aide.zip",
"repository": "https://github.com/mnriem/spec-kit-extensions",
"homepage": "https://github.com/mnriem/spec-kit-extensions",
"documentation": "https://github.com/mnriem/spec-kit-extensions/blob/main/aide/README.md",
"changelog": "https://github.com/mnriem/spec-kit-extensions/blob/main/aide/CHANGELOG.md",
"license": "MIT",
"category": "process",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.2.0"
},
"provides": {
"commands": 7,
"hooks": 0
},
"tags": [
"workflow",
"project-management",
"ai-driven",
"new-project",
"planning",
"experimental"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-18T00:00:00Z",
"updated_at": "2026-03-18T00:00:00Z"
},
"analytics": {
"name": "Analytics",
"id": "analytics",
@@ -704,6 +704,37 @@
"created_at": "2026-07-06T00:00:00Z",
"updated_at": "2026-07-06T00:00:00Z"
},
"checkpoint": {
"name": "Checkpoint Extension",
"id": "checkpoint",
"description": "An extension to commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end.",
"author": "aaronrsun",
"version": "1.0.0",
"download_url": "https://github.com/aaronrsun/spec-kit-checkpoint/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/aaronrsun/spec-kit-checkpoint",
"homepage": "https://github.com/aaronrsun/spec-kit-checkpoint",
"documentation": "https://github.com/aaronrsun/spec-kit-checkpoint/blob/main/README.md",
"changelog": "https://github.com/aaronrsun/spec-kit-checkpoint/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "code",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 0
},
"tags": [
"checkpoint",
"commit"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-22T00:00:00Z",
"updated_at": "2026-03-22T00:00:00Z"
},
"ci-guard": {
"name": "CI Guard",
"id": "ci-guard",
@@ -739,37 +770,6 @@
"created_at": "2026-04-10T17:00:00Z",
"updated_at": "2026-04-10T17:00:00Z"
},
"checkpoint": {
"name": "Checkpoint Extension",
"id": "checkpoint",
"description": "An extension to commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end.",
"author": "aaronrsun",
"version": "1.0.0",
"download_url": "https://github.com/aaronrsun/spec-kit-checkpoint/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/aaronrsun/spec-kit-checkpoint",
"homepage": "https://github.com/aaronrsun/spec-kit-checkpoint",
"documentation": "https://github.com/aaronrsun/spec-kit-checkpoint/blob/main/README.md",
"changelog": "https://github.com/aaronrsun/spec-kit-checkpoint/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "code",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 0
},
"tags": [
"checkpoint",
"commit"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-22T00:00:00Z",
"updated_at": "2026-03-22T00:00:00Z"
},
"cleanup": {
"name": "Cleanup Extension",
"id": "cleanup",
@@ -854,7 +854,10 @@
"requires": {
"speckit_version": ">=0.9.5",
"tools": [
{ "name": "python3", "required": false }
{
"name": "python3",
"required": false
}
]
},
"provides": {
@@ -907,38 +910,6 @@
"created_at": "2026-03-19T12:08:20Z",
"updated_at": "2026-04-03T12:35:01Z"
},
"critique": {
"name": "Spec Critique Extension",
"id": "critique",
"description": "Dual-lens critical review of spec and plan from product strategy and engineering risk perspectives.",
"author": "arunt14",
"version": "1.0.0",
"download_url": "https://github.com/arunt14/spec-kit-critique/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/arunt14/spec-kit-critique",
"homepage": "https://github.com/arunt14/spec-kit-critique",
"documentation": "https://github.com/arunt14/spec-kit-critique/blob/main/README.md",
"changelog": "https://github.com/arunt14/spec-kit-critique/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "docs",
"effect": "read-only",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"docs",
"review",
"planning"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-01T00:00:00Z",
"updated_at": "2026-04-01T00:00:00Z"
},
"confluence": {
"name": "Confluence Extension",
"id": "confluence",
@@ -1003,6 +974,38 @@
"created_at": "2026-05-03T00:00:00Z",
"updated_at": "2026-05-05T00:00:00Z"
},
"critique": {
"name": "Spec Critique Extension",
"id": "critique",
"description": "Dual-lens critical review of spec and plan from product strategy and engineering risk perspectives.",
"author": "arunt14",
"version": "1.0.0",
"download_url": "https://github.com/arunt14/spec-kit-critique/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/arunt14/spec-kit-critique",
"homepage": "https://github.com/arunt14/spec-kit-critique",
"documentation": "https://github.com/arunt14/spec-kit-critique/blob/main/README.md",
"changelog": "https://github.com/arunt14/spec-kit-critique/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "docs",
"effect": "read-only",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"docs",
"review",
"planning"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-01T00:00:00Z",
"updated_at": "2026-04-01T00:00:00Z"
},
"data-model-diagram": {
"name": "Data Model Diagram",
"id": "data-model-diagram",
@@ -1353,6 +1356,50 @@
"created_at": "2026-05-06T00:00:00Z",
"updated_at": "2026-05-06T00:00:00Z"
},
"gates": {
"name": "Quality Gates (Enforcement Layer)",
"id": "gates",
"description": "Deterministic quality enforcement for Spec Kit projects at three boundaries — agent hooks, git pre-commit, CI — with one policy file and provable enforcement (attestations, canaries, verified parity).",
"author": "schwichtgit",
"version": "0.1.0",
"download_url": "https://github.com/schwichtgit/spec-gates/releases/download/v0.1.0/gates-0.1.0.zip",
"repository": "https://github.com/schwichtgit/spec-gates",
"homepage": "https://github.com/schwichtgit/spec-gates",
"documentation": "https://github.com/schwichtgit/spec-gates/blob/main/docs/how-it-works.md",
"changelog": "",
"license": "MIT",
"category": "process",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.12.0",
"tools": [
{
"name": "jq",
"required": true
},
{
"name": "git",
"required": false
}
]
},
"provides": {
"commands": 5,
"hooks": 1
},
"tags": [
"quality",
"enforcement",
"hooks",
"ci",
"governance"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-07-09T00:00:00Z",
"updated_at": "2026-07-09T00:00:00Z"
},
"github-issues": {
"name": "GitHub Issues Integration 1",
"id": "github-issues",
@@ -1664,12 +1711,31 @@
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{ "name": "bash", "version": ">=4.4", "required": true },
{ "name": "git", "required": true },
{ "name": "curl", "required": true },
{ "name": "jq", "required": true },
{ "name": "gitleaks", "required": false },
{ "name": "trufflehog", "required": false }
{
"name": "bash",
"version": ">=4.4",
"required": true
},
{
"name": "git",
"required": true
},
{
"name": "curl",
"required": true
},
{
"name": "jq",
"required": true
},
{
"name": "gitleaks",
"required": false
},
{
"name": "trufflehog",
"required": false
}
]
},
"provides": {
@@ -3864,8 +3930,14 @@
"requires": {
"speckit_version": ">=0.2.0",
"tools": [
{ "name": "gh", "required": true },
{ "name": "python3", "required": true }
{
"name": "gh",
"required": true
},
{
"name": "python3",
"required": true
}
]
},
"provides": {
@@ -3918,6 +3990,40 @@
"created_at": "2026-05-20T00:00:00Z",
"updated_at": "2026-05-20T00:00:00Z"
},
"threatmodel": {
"name": "OWASP LLM Threat Model",
"id": "threatmodel",
"description": "OWASP Top 10 for LLM Applications 2025 threat analysis on agent artifacts",
"author": "NaviaSamal",
"version": "1.0.0",
"download_url": "https://github.com/NaviaSamal/spec-kit-threatmodel/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/NaviaSamal/spec-kit-threatmodel",
"homepage": "https://github.com/NaviaSamal/spec-kit-threatmodel",
"documentation": "https://github.com/NaviaSamal/spec-kit-threatmodel/blob/main/README.md",
"changelog": "https://github.com/NaviaSamal/spec-kit-threatmodel/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "code",
"effect": "read-only",
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"security",
"owasp",
"threat-model",
"llm",
"analysis"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-25T00:00:00Z",
"updated_at": "2026-04-25T00:00:00Z"
},
"time-machine": {
"name": "Time Machine",
"id": "time-machine",
@@ -3991,40 +4097,6 @@
"created_at": "2026-04-10T00:00:00Z",
"updated_at": "2026-04-10T00:00:00Z"
},
"threatmodel": {
"name": "OWASP LLM Threat Model",
"id": "threatmodel",
"description": "OWASP Top 10 for LLM Applications 2025 threat analysis on agent artifacts",
"author": "NaviaSamal",
"version": "1.0.0",
"download_url": "https://github.com/NaviaSamal/spec-kit-threatmodel/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/NaviaSamal/spec-kit-threatmodel",
"homepage": "https://github.com/NaviaSamal/spec-kit-threatmodel",
"documentation": "https://github.com/NaviaSamal/spec-kit-threatmodel/blob/main/README.md",
"changelog": "https://github.com/NaviaSamal/spec-kit-threatmodel/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "code",
"effect": "read-only",
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"security",
"owasp",
"threat-model",
"llm",
"analysis"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-25T00:00:00Z",
"updated_at": "2026-04-25T00:00:00Z"
},
"tldr": {
"name": "Spec Kit TLDR",
"id": "tldr",
@@ -4159,11 +4231,27 @@
"requires": {
"speckit_version": ">=0.10.0",
"tools": [
{ "name": "rtk", "required": false },
{ "name": "headroom", "required": false },
{ "name": "token-router", "required": false },
{ "name": "ollama", "required": false },
{ "name": "python", "version": ">=3.10", "required": false }
{
"name": "rtk",
"required": false
},
{
"name": "headroom",
"required": false
},
{
"name": "token-router",
"required": false
},
{
"name": "ollama",
"required": false
},
{
"name": "python",
"version": ">=3.10",
"required": false
}
]
},
"provides": {

View File

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

View File

@@ -141,8 +141,9 @@ def _install_shared_infra(
Copies ``.specify/scripts/<variant>/`` and ``.specify/templates/`` from
the bundled core_pack or source checkout, where ``<variant>`` is
``bash`` when *script_type* is ``"sh"`` and ``powershell`` when it is
``"ps"``. Tracks all installed files in ``speckit.manifest.json``.
``bash`` when *script_type* is ``"sh"``, ``python`` when it is ``"py"``,
and ``powershell`` when it is ``"ps"``. Tracks all installed files in
``speckit.manifest.json``.
Shared scripts and page templates are processed to resolve
``__SPECKIT_COMMAND_<NAME>__`` placeholders using *invoke_separator*

View File

@@ -403,7 +403,7 @@ def install_shared_infra(
# manifest entries the core no longer ships (stale-script cleanup, #3076).
seen_rels: set[str] = set()
scripts_scanned = False
variant_dir = "bash" if script_type == "sh" else "powershell"
variant_dir = {"sh": "bash", "py": "python"}.get(script_type, "powershell")
def _decide_overwrite(rel: str, dst: Path) -> tuple[bool, str | None]:
"""Return (write, bucket) where bucket is 'skip', 'preserved', or None."""
@@ -465,6 +465,10 @@ def install_shared_infra(
for src_path in variant_src.rglob("*"):
if not src_path.is_file():
continue
# Python bytecode caches are local artifacts, not
# workflow scripts — never install them.
if "__pycache__" in src_path.parts:
continue
# Mark scanned only once a real source file is seen. An
# empty (or symlink-skipped) variant keeps this False, so
# stale-cleanup is skipped — otherwise it would treat every

View File

@@ -25,6 +25,14 @@ class ShellStep(StepBase):
run_cmd = str(run_cmd)
cwd = context.project_root or "."
# Defensive: the engine does not auto-validate step config, so an
# invalid ``timeout`` (string, None, ...) would otherwise raise a
# TypeError from subprocess.run() and crash the whole run. Mirror
# the engine's handling of unvalidated ``continue_on_error`` by
# only honoring well-formed values and falling back to the default.
timeout = config.get("timeout", 300)
if isinstance(timeout, bool) or not isinstance(timeout, int) or timeout <= 0:
timeout = 300
# NOTE: shell=True is required to support pipes, redirects, and
# multi-command expressions in workflow YAML. Workflow authors
@@ -37,7 +45,7 @@ class ShellStep(StepBase):
capture_output=True,
text=True,
cwd=cwd,
timeout=300,
timeout=timeout,
)
output = {
"exit_code": proc.returncode,
@@ -74,7 +82,7 @@ class ShellStep(StepBase):
except subprocess.TimeoutExpired:
return StepResult(
status=StepStatus.FAILED,
error="Shell command timed out after 300 seconds.",
error=f"Shell command timed out after {timeout} seconds.",
output={"exit_code": -1, "stdout": "", "stderr": "timeout"},
)
except OSError as exc:
@@ -106,4 +114,16 @@ class ShellStep(StepBase):
f"Shell step {config.get('id', '?')!r}: 'output_format' must "
f"be 'json' when present, got {output_format!r}."
)
if "timeout" in config:
timeout = config["timeout"]
# bool is an int subclass, so reject it explicitly.
if (
isinstance(timeout, bool)
or not isinstance(timeout, int)
or timeout <= 0
):
errors.append(
f"Shell step {config.get('id', '?')!r}: 'timeout' must be a "
f"positive integer (seconds) when present, got {timeout!r}."
)
return errors

View File

@@ -3,6 +3,7 @@ description: Perform a non-destructive cross-artifact consistency and quality an
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
---
## User Input

View File

@@ -3,6 +3,7 @@ description: Generate a custom checklist for the current feature based on user r
scripts:
sh: scripts/bash/check-prerequisites.sh --json
ps: scripts/powershell/check-prerequisites.ps1 -Json
py: scripts/python/check_prerequisites.py --json
---
## Checklist Purpose: "Unit Tests for English"

View File

@@ -7,6 +7,7 @@ handoffs:
scripts:
sh: scripts/bash/check-prerequisites.sh --json --paths-only
ps: scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly
py: scripts/python/check_prerequisites.py --json --paths-only
---
## User Input

View File

@@ -3,6 +3,7 @@ description: Assess the current codebase against the feature's spec, plan, and t
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
---
## User Input

View File

@@ -3,6 +3,7 @@ description: Execute the implementation plan by processing and executing all tas
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
---
## User Input

View File

@@ -4,6 +4,7 @@ tools: ['github/github-mcp-server/list_issues', 'github/github-mcp-server/issue_
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
---
## User Input

View File

@@ -688,6 +688,62 @@ class TestExtensionSelfSeed:
_MDC_CONTEXT_FILE = ".cursor/rules/specify-rules.mdc"
class TestPlanDiscovery:
"""Mtime fallback must find plans in nested spec layouts (#3024).
Repos using SPECIFY_FEATURE_DIRECTORY place plans at
``specs/<scope>/<feature>/plan.md``; a one-level ``specs/*/plan.md``
glob never matches those.
"""
@staticmethod
def _make_plans(project: Path) -> Path:
# Older flat plan plus a newer nested plan: recursive discovery
# must pick the nested one by mtime.
flat = project / "specs" / "old-feature" / "plan.md"
flat.parent.mkdir(parents=True)
flat.write_text("flat plan\n", encoding="utf-8")
os.utime(flat, (1_000_000_000, 1_000_000_000))
nested = project / "specs" / "scope" / "new-feature" / "plan.md"
nested.parent.mkdir(parents=True)
nested.write_text("nested plan\n", encoding="utf-8")
return nested
@requires_bash
def test_bash_script_finds_nested_plan(self, tmp_path):
project = tmp_path / "project"
project.mkdir()
_install_agent_context_config(
project,
context_file="AGENTS.md",
context_files=["AGENTS.md"],
)
self._make_plans(project)
result = _run_bash_agent_context_script(project)
assert result.returncode == 0, result.stderr + result.stdout
content = (project / "AGENTS.md").read_text(encoding="utf-8")
assert "specs/scope/new-feature/plan.md" in content
@pytest.mark.skipif(POWERSHELL is None, reason="PowerShell not available")
def test_powershell_script_finds_nested_plan(self, tmp_path):
project = tmp_path / "project"
project.mkdir()
_install_agent_context_config(
project,
context_file="AGENTS.md",
context_files=["AGENTS.md"],
)
self._make_plans(project)
result = _run_powershell_agent_context_script(project)
assert result.returncode == 0, result.stderr + result.stdout
content = (project / "AGENTS.md").read_text(encoding="utf-8")
assert "specs/scope/new-feature/plan.md" in content
class TestMdcFrontmatter:
"""Cursor-style ``.mdc`` targets must carry ``alwaysApply: true`` frontmatter
so the rule file is auto-loaded; non-``.mdc`` targets must not gain any."""

View File

@@ -0,0 +1,107 @@
"""Command templates with a py: script line must render for --script py.
Covers #3283: ``py:`` lines in the ``scripts:`` frontmatter of
``templates/commands/*.md`` reference Python scripts that exist in the repo,
and ``process_template`` turns them into a valid Python invocation
(interpreter-prefixed, path rewritten to the ``.specify`` tree).
``plan.md`` and ``tasks.md`` gain their ``py:`` lines together with
``setup_plan.py``/``setup_tasks.py`` in the core-scripts port (#3280); the
existence check below enforces that ordering.
"""
import re
from pathlib import Path
import pytest
from specify_cli.integrations.base import IntegrationBase
REPO_ROOT = Path(__file__).parent.parent
TEMPLATES_DIR = REPO_ROOT / "templates" / "commands"
_PY_LINE = re.compile(r"^\s*py: (scripts/python/\S+\.py)", re.MULTILINE)
def _py_script(name: str) -> str | None:
match = _PY_LINE.search((TEMPLATES_DIR / name).read_text(encoding="utf-8"))
return match.group(1) if match else None
PY_TEMPLATES = sorted(
p.name for p in TEMPLATES_DIR.glob("*.md") if _py_script(p.name)
)
@pytest.fixture(autouse=True)
def _pin_interpreter(monkeypatch):
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: "/usr/bin/python3" if name == "python3" else None,
)
# On Windows, ``resolve_python_interpreter`` guards the ``which`` result
# with a real ``_interpreter_runs`` subprocess probe (#3304). The mocked
# ``/usr/bin/python3`` path does not exist on a Windows runner, so the
# probe would fail and the resolver would fall back to ``sys.executable``
# (a ``...python.exe`` path), breaking the ``python3``-anchored assertion.
# Pin the probe to True so the interpreter token stays ``python3`` on all
# platforms.
monkeypatch.setattr(
"specify_cli.integrations.base.IntegrationBase._interpreter_runs",
staticmethod(lambda path: True),
)
def test_py_templates_discovered():
# Guard: the glob must find the known py-scripted templates, otherwise
# the parametrized tests below would silently pass on an empty set.
assert "implement.md" in PY_TEMPLATES
assert "clarify.md" in PY_TEMPLATES
@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_referenced_python_script_exists(name: str):
# A py: line must never point at a script the repo does not ship —
# rendering would produce a broken invocation at runtime.
script = _py_script(name)
assert (REPO_ROOT / script).is_file(), f"{name} references missing {script}"
@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_template_renders_python_invocation(name: str):
content = (TEMPLATES_DIR / name).read_text(encoding="utf-8")
result = IntegrationBase.process_template(content, "agent", "py")
assert "{SCRIPT}" not in result
assert re.search(
r"python3 \.specify/scripts/python/\w+\.py(?: --[\w-]+)*", result
), f"{name} did not render a Python invocation"
@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_sh_rendering_unchanged(name: str):
# Negative: adding py: lines must not leak into sh rendering.
content = (TEMPLATES_DIR / name).read_text(encoding="utf-8")
result = IntegrationBase.process_template(content, "agent", "sh")
assert "{SCRIPT}" not in result
assert "scripts/python" not in result
def test_install_shared_infra_copies_python_scripts(tmp_path):
# --script py must install scripts/python/ into .specify/scripts/python/
# so the rendered invocations point at files that exist.
from rich.console import Console
from specify_cli.shared_infra import install_shared_infra
install_shared_infra(
tmp_path,
"py",
version="0.0.0",
core_pack=None,
repo_root=REPO_ROOT,
console=Console(quiet=True),
force=False,
)
dest = tmp_path / ".specify" / "scripts" / "python"
assert (dest / "check_prerequisites.py").is_file()
assert not (tmp_path / ".specify" / "scripts" / "powershell").exists()

View File

@@ -1353,6 +1353,106 @@ class TestShellStep:
assert step.validate({"id": "s", "run": "echo hi"}) == []
assert step.validate({"id": "s", "run": "{{ steps.x.output }}"}) == []
def test_timeout_is_configurable(self, monkeypatch):
"""A 'timeout' field overrides the 300s default (#3327)."""
import subprocess as sp
from specify_cli.workflows.steps.shell import ShellStep
from specify_cli.workflows.base import StepContext, StepStatus
seen = {}
real_run = sp.run
def spy_run(*args, **kwargs):
seen["timeout"] = kwargs.get("timeout")
return real_run(*args, **kwargs)
monkeypatch.setattr(
"specify_cli.workflows.steps.shell.subprocess.run", spy_run
)
step = ShellStep()
result = step.execute(
{"id": "t", "run": "echo hi", "timeout": 1800}, StepContext()
)
assert result.status == StepStatus.COMPLETED
assert seen["timeout"] == 1800
def test_timeout_defaults_to_300(self, monkeypatch):
import subprocess as sp
from specify_cli.workflows.steps.shell import ShellStep
from specify_cli.workflows.base import StepContext, StepStatus
seen = {}
real_run = sp.run
def spy_run(*args, **kwargs):
seen["timeout"] = kwargs.get("timeout")
return real_run(*args, **kwargs)
monkeypatch.setattr(
"specify_cli.workflows.steps.shell.subprocess.run", spy_run
)
result = ShellStep().execute({"id": "t", "run": "echo hi"}, StepContext())
assert result.status == StepStatus.COMPLETED
assert seen["timeout"] == 300
def test_timeout_error_reports_configured_value(self, monkeypatch):
import subprocess as sp
from specify_cli.workflows.steps.shell import ShellStep
from specify_cli.workflows.base import StepContext, StepStatus
def raise_timeout(*args, **kwargs):
raise sp.TimeoutExpired(cmd="x", timeout=kwargs.get("timeout"))
monkeypatch.setattr(
"specify_cli.workflows.steps.shell.subprocess.run", raise_timeout
)
result = ShellStep().execute(
{"id": "t", "run": "sleep 999", "timeout": 7}, StepContext()
)
assert result.status == StepStatus.FAILED
assert "7 seconds" in result.error
@pytest.mark.parametrize("bad", [0, -5, "600", 1.5, None, True])
def test_execute_ignores_unvalidated_bad_timeout(self, bad, monkeypatch):
"""execute() falls back to 300 when config skipped validation (#3327)."""
import subprocess as sp
from specify_cli.workflows.steps.shell import ShellStep
from specify_cli.workflows.base import StepContext, StepStatus
seen = {}
real_run = sp.run
def spy_run(*args, **kwargs):
seen["timeout"] = kwargs.get("timeout")
return real_run(*args, **kwargs)
monkeypatch.setattr(
"specify_cli.workflows.steps.shell.subprocess.run", spy_run
)
result = ShellStep().execute(
{"id": "t", "run": "echo hi", "timeout": bad}, StepContext()
)
assert result.status == StepStatus.COMPLETED
assert seen["timeout"] == 300
@pytest.mark.parametrize("bad", [0, -5, "600", 1.5, None, True])
def test_validate_rejects_bad_timeout(self, bad):
from specify_cli.workflows.steps.shell import ShellStep
errors = ShellStep().validate({"id": "s", "run": "echo hi", "timeout": bad})
assert any("'timeout'" in e for e in errors)
def test_validate_accepts_positive_int_timeout(self):
from specify_cli.workflows.steps.shell import ShellStep
assert (
ShellStep().validate({"id": "s", "run": "echo hi", "timeout": 1800}) == []
)
def test_output_format_json_exposes_data(self, tmp_path):
from specify_cli.workflows.steps.shell import ShellStep