From 4f6b64b079720c756114c06472ed563358b35b4d Mon Sep 17 00:00:00 2001 From: Alan Meadows Date: Thu, 5 Feb 2026 12:52:28 -0800 Subject: [PATCH 1/7] feat: Add SPECIFY_SPECS_DIR env var for centralized specs directory Enable specs directory to be located outside the repository via the SPECIFY_SPECS_DIR environment variable. This enables: - Worktree workflows where specs are shared across worktrees - Spec-first development (create specs before branches) - Cross-feature visibility when working on multiple features - Project-wide shared context via _shared/ subdirectory Changes: - Add get_specs_dir()/Get-SpecsDir functions to common scripts - Update all hardcoded specs path references - Add SPECS_DIR to JSON output from check-prerequisites - Update all command templates to load _shared/ context 100% backward compatible - when SPECIFY_SPECS_DIR is not set, behavior is identical to current. --- scripts/bash/check-prerequisites.sh | 9 ++++++--- scripts/bash/common.sh | 12 +++++++++--- scripts/bash/create-new-feature.sh | 2 +- scripts/powershell/check-prerequisites.ps1 | 7 ++++++- scripts/powershell/common.ps1 | 13 +++++++++++-- scripts/powershell/create-new-feature.ps1 | 2 +- templates/commands/analyze.md | 4 ++++ templates/commands/checklist.md | 1 + templates/commands/clarify.md | 2 +- templates/commands/implement.md | 1 + templates/commands/plan.md | 2 +- templates/commands/specify.md | 10 ++++++---- templates/commands/tasks.md | 1 + 13 files changed, 49 insertions(+), 17 deletions(-) diff --git a/scripts/bash/check-prerequisites.sh b/scripts/bash/check-prerequisites.sh index 98e387c271..fa6ac80e28 100644 --- a/scripts/bash/check-prerequisites.sh +++ b/scripts/bash/check-prerequisites.sh @@ -84,10 +84,11 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 # If paths-only mode, output paths and exit (support JSON + paths-only combined) if $PATHS_ONLY; then + SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" if $JSON_MODE; then # Minimal JSON paths payload (no validation performed) - printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ - "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" + printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s","SPECS_DIR":"%s"}\n' \ + "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" "$SPECS_DIR" else echo "REPO_ROOT: $REPO_ROOT" echo "BRANCH: $CURRENT_BRANCH" @@ -95,6 +96,7 @@ if $PATHS_ONLY; then echo "FEATURE_SPEC: $FEATURE_SPEC" echo "IMPL_PLAN: $IMPL_PLAN" echo "TASKS: $TASKS" + echo "SPECS_DIR: $SPECS_DIR" fi exit 0 fi @@ -148,7 +150,8 @@ if $JSON_MODE; then json_docs="[${json_docs%,}]" fi - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" + SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"SPECS_DIR":"%s"}\n' "$FEATURE_DIR" "$json_docs" "$SPECS_DIR" else # Text output echo "FEATURE_DIR:$FEATURE_DIR" diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 2c3165e41d..ab88b1c71b 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -12,6 +12,12 @@ get_repo_root() { fi } +# Get specs directory, with support for external location via SPECIFY_SPECS_DIR +get_specs_dir() { + local repo_root="${1:-$(get_repo_root)}" + echo "${SPECIFY_SPECS_DIR:-$repo_root/specs}" +} + # Get current branch, with fallback for non-git repositories get_current_branch() { # First check if SPECIFY_FEATURE environment variable is set @@ -28,7 +34,7 @@ get_current_branch() { # For non-git repos, try to find the latest feature directory local repo_root=$(get_repo_root) - local specs_dir="$repo_root/specs" + local specs_dir="$(get_specs_dir "$repo_root")" if [[ -d "$specs_dir" ]]; then local latest_feature="" @@ -81,14 +87,14 @@ check_feature_branch() { return 0 } -get_feature_dir() { echo "$1/specs/$2"; } +get_feature_dir() { echo "$(get_specs_dir "$1")/$2"; } # Find feature directory by numeric prefix instead of exact branch match # This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) find_feature_dir_by_prefix() { local repo_root="$1" local branch_name="$2" - local specs_dir="$repo_root/specs" + local specs_dir="$(get_specs_dir "$repo_root")" # Extract numeric prefix from branch (e.g., "004" from "004-whatever") if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index c40cfd77f0..68c003ba1b 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -174,7 +174,7 @@ fi cd "$REPO_ROOT" -SPECS_DIR="$REPO_ROOT/specs" +SPECS_DIR="${SPECIFY_SPECS_DIR:-$REPO_ROOT/specs}" mkdir -p "$SPECS_DIR" # Function to generate branch name with stop word filtering and length filtering diff --git a/scripts/powershell/check-prerequisites.ps1 b/scripts/powershell/check-prerequisites.ps1 index 91667e9ef1..bcfe10cb3e 100644 --- a/scripts/powershell/check-prerequisites.ps1 +++ b/scripts/powershell/check-prerequisites.ps1 @@ -65,6 +65,7 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GI # If paths-only mode, output paths and exit (support combined -Json -PathsOnly) if ($PathsOnly) { + $specsDir = Get-SpecsDir -RepoRoot $paths.REPO_ROOT if ($Json) { [PSCustomObject]@{ REPO_ROOT = $paths.REPO_ROOT @@ -73,6 +74,7 @@ if ($PathsOnly) { FEATURE_SPEC = $paths.FEATURE_SPEC IMPL_PLAN = $paths.IMPL_PLAN TASKS = $paths.TASKS + SPECS_DIR = $specsDir } | ConvertTo-Json -Compress } else { Write-Output "REPO_ROOT: $($paths.REPO_ROOT)" @@ -81,6 +83,7 @@ if ($PathsOnly) { Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)" Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)" Write-Output "TASKS: $($paths.TASKS)" + Write-Output "SPECS_DIR: $specsDir" } exit 0 } @@ -127,9 +130,11 @@ if ($IncludeTasks -and (Test-Path $paths.TASKS)) { # Output results if ($Json) { # JSON output + $specsDir = Get-SpecsDir -RepoRoot $paths.REPO_ROOT [PSCustomObject]@{ FEATURE_DIR = $paths.FEATURE_DIR - AVAILABLE_DOCS = $docs + AVAILABLE_DOCS = $docs + SPECS_DIR = $specsDir } | ConvertTo-Json -Compress } else { # Text output diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index b0be273545..70602ec76e 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -15,6 +15,15 @@ function Get-RepoRoot { return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path } +# Get specs directory, with support for external location via SPECIFY_SPECS_DIR +function Get-SpecsDir { + param([string]$RepoRoot = (Get-RepoRoot)) + if ($env:SPECIFY_SPECS_DIR) { + return $env:SPECIFY_SPECS_DIR + } + return Join-Path $RepoRoot "specs" +} + function Get-CurrentBranch { # First check if SPECIFY_FEATURE environment variable is set if ($env:SPECIFY_FEATURE) { @@ -33,7 +42,7 @@ function Get-CurrentBranch { # For non-git repos, try to find the latest feature directory $repoRoot = Get-RepoRoot - $specsDir = Join-Path $repoRoot "specs" + $specsDir = Get-SpecsDir -RepoRoot $repoRoot if (Test-Path $specsDir) { $latestFeature = "" @@ -89,7 +98,7 @@ function Test-FeatureBranch { function Get-FeatureDir { param([string]$RepoRoot, [string]$Branch) - Join-Path $RepoRoot "specs/$Branch" + Join-Path (Get-SpecsDir -RepoRoot $RepoRoot) $Branch } function Get-FeaturePathsEnv { diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 2f0172e35d..25b4fb2a13 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -149,7 +149,7 @@ try { Set-Location $repoRoot -$specsDir = Join-Path $repoRoot 'specs' +$specsDir = if ($env:SPECIFY_SPECS_DIR) { $env:SPECIFY_SPECS_DIR } else { Join-Path $repoRoot 'specs' } New-Item -ItemType Directory -Path $specsDir -Force | Out-Null # Function to generate branch name with stop word filtering and length filtering diff --git a/templates/commands/analyze.md b/templates/commands/analyze.md index 827d4e4caf..1f1cf6ee0e 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -67,6 +67,10 @@ Load only the minimal necessary context from each artifact: - Load `/memory/constitution.md` for principle validation +**From shared context (if available):** + +- **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files for project-wide standards (architecture decisions, coding conventions, security requirements). Use these as additional validation criteria alongside the constitution. + ### 3. Build Semantic Models Create internal representations (do not include raw artifacts in output): diff --git a/templates/commands/checklist.md b/templates/commands/checklist.md index e32a2c843b..651181eae9 100644 --- a/templates/commands/checklist.md +++ b/templates/commands/checklist.md @@ -82,6 +82,7 @@ You **MUST** consider the user input before proceeding (if not empty). - spec.md: Feature requirements and scope - plan.md (if exists): Technical details, dependencies - tasks.md (if exists): Implementation tasks + - **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files for project-wide standards (security requirements, accessibility standards, coding conventions). Incorporate these into checklist generation to ensure project-wide requirements are validated. **Context Loading Strategy**: - Load only necessary portions relevant to active focus areas (avoid full-file dumping) diff --git a/templates/commands/clarify.md b/templates/commands/clarify.md index 4de842aa60..fbc7366f3d 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -32,7 +32,7 @@ Execution steps: - If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment. - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). -2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). +2. Load the current spec file. **IF `SPECS_DIR/_shared/` exists** (SPECS_DIR is in the JSON output): Read all `.md` files from it for project-wide context (architecture decisions, conventions, standards). Use this to validate spec alignment with project standards and inform ambiguity detection. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). Functional Scope & Behavior: - Core user goals & success criteria diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 39abb1e6c8..d5f0397256 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -55,6 +55,7 @@ You **MUST** consider the user input before proceeding (if not empty). - **IF EXISTS**: Read contracts/ for API specifications and test requirements - **IF EXISTS**: Read research.md for technical decisions and constraints - **IF EXISTS**: Read quickstart.md for integration scenarios + - **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files for project-wide context (coding standards, API conventions, security requirements). Use this to guide implementation decisions and ensure alignment with established patterns. 4. **Project Setup Verification**: - **REQUIRED**: Create/verify ignore files based on actual project setup: diff --git a/templates/commands/plan.md b/templates/commands/plan.md index 147da0afa0..5da02a084e 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -28,7 +28,7 @@ You **MUST** consider the user input before proceeding (if not empty). 1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). -2. **Load context**: Read FEATURE_SPEC and `/memory/constitution.md`. Load IMPL_PLAN template (already copied). +2. **Load context**: Read FEATURE_SPEC and `/memory/constitution.md`. Load IMPL_PLAN template (already copied). **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files from it for project-wide context (architecture decisions, API conventions, coding standards). Use this to inform technical decisions and ensure alignment with established patterns. 3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to: - Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION") diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 3c952d683e..ddd6e3ed33 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -73,7 +73,9 @@ Given that feature description, do this: 3. Load `templates/spec-template.md` to understand required sections. -4. Follow this execution flow: +4. **Load shared context (if available)**: Check if `SPECS_DIR/_shared/` directory exists (SPECS_DIR is returned in the script JSON output). If it exists, read all `.md` files from it. These contain project-wide standards (architecture decisions, API conventions, coding standards, security requirements). Use this context to inform spec structure and ensure alignment with established patterns. Do not modify shared files. + +5. Follow this execution flow: 1. Parse user description from Input If empty: ERROR "No feature description provided" @@ -99,9 +101,9 @@ Given that feature description, do this: 7. Identify Key Entities (if data involved) 8. Return: SUCCESS (spec ready for planning) -5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. +6. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. -6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: +7. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items: @@ -193,7 +195,7 @@ Given that feature description, do this: d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status -7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`). +8. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`). **NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index d69d43763e..da598a4ee5 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -29,6 +29,7 @@ You **MUST** consider the user input before proceeding (if not empty). 2. **Load design documents**: Read from FEATURE_DIR: - **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities) - **Optional**: data-model.md (entities), contracts/ (API endpoints), research.md (decisions), quickstart.md (test scenarios) + - **IF `SPECS_DIR/_shared/` exists**: Read all `.md` files for project-wide context (coding standards, conventions). Use this to inform task structure and ensure alignment with established patterns. - Note: Not all projects have all documents. Generate tasks based on what's available. 3. **Execute task generation workflow**: From f1292f5bcf5a8fc96ba92c4184db24dce4a70d02 Mon Sep 17 00:00:00 2001 From: Alan Meadows Date: Thu, 5 Feb 2026 13:16:16 -0800 Subject: [PATCH 2/7] fix: Address PR review feedback for centralized specs - Add path validation for SPECIFY_SPECS_DIR (must be absolute, no ..) - Add json_escape helper function for safe JSON string encoding - Add SPECS_DIR to create-new-feature.sh/ps1 JSON output - Update specify.md template wording for clarity - Apply json_escape to all printf JSON outputs --- scripts/bash/check-prerequisites.sh | 5 ++-- scripts/bash/common.sh | 32 ++++++++++++++++++++++- scripts/bash/create-new-feature.sh | 3 ++- scripts/powershell/common.ps1 | 14 +++++++++- scripts/powershell/create-new-feature.ps1 | 1 + templates/commands/specify.md | 2 +- 6 files changed, 51 insertions(+), 6 deletions(-) diff --git a/scripts/bash/check-prerequisites.sh b/scripts/bash/check-prerequisites.sh index fa6ac80e28..91fb9d684b 100644 --- a/scripts/bash/check-prerequisites.sh +++ b/scripts/bash/check-prerequisites.sh @@ -88,7 +88,8 @@ if $PATHS_ONLY; then if $JSON_MODE; then # Minimal JSON paths payload (no validation performed) printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s","SPECS_DIR":"%s"}\n' \ - "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" "$SPECS_DIR" + "$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" \ + "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")" "$(json_escape "$SPECS_DIR")" else echo "REPO_ROOT: $REPO_ROOT" echo "BRANCH: $CURRENT_BRANCH" @@ -151,7 +152,7 @@ if $JSON_MODE; then fi SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"SPECS_DIR":"%s"}\n' "$FEATURE_DIR" "$json_docs" "$SPECS_DIR" + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"SPECS_DIR":"%s"}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "$SPECS_DIR")" else # Text output echo "FEATURE_DIR:$FEATURE_DIR" diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index ab88b1c71b..569470d05d 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -1,6 +1,18 @@ #!/usr/bin/env bash # Common functions and variables for all scripts +# Escape a string for safe inclusion in JSON +json_escape() { + local str="$1" + # Escape backslashes first, then quotes, then other special chars + str="${str//\\/\\\\}" + str="${str//\"/\\\"}" + str="${str//$'\n'/\\n}" + str="${str//$'\r'/\\r}" + str="${str//$'\t'/\\t}" + echo "$str" +} + # Get repository root, with fallback for non-git repositories get_repo_root() { if git rev-parse --show-toplevel >/dev/null 2>&1; then @@ -15,7 +27,25 @@ get_repo_root() { # Get specs directory, with support for external location via SPECIFY_SPECS_DIR get_specs_dir() { local repo_root="${1:-$(get_repo_root)}" - echo "${SPECIFY_SPECS_DIR:-$repo_root/specs}" + local specs_dir + + if [[ -n "${SPECIFY_SPECS_DIR:-}" ]]; then + specs_dir="$SPECIFY_SPECS_DIR" + # Require absolute path + if [[ "$specs_dir" != /* ]]; then + echo "[specify] ERROR: SPECIFY_SPECS_DIR must be an absolute path (got: '$specs_dir')" >&2 + return 1 + fi + # Block path traversal + if [[ "$specs_dir" == *".."* ]]; then + echo "[specify] ERROR: SPECIFY_SPECS_DIR must not contain '..' (got: '$specs_dir')" >&2 + return 1 + fi + else + specs_dir="$repo_root/specs" + fi + + echo "$specs_dir" } # Get current branch, with fallback for non-git repositories diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 68c003ba1b..3782979f27 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -288,7 +288,8 @@ if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE" export SPECIFY_FEATURE="$BRANCH_NAME" if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","SPECS_DIR":"%s"}\n' \ + "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" "$(json_escape "$SPECS_DIR")" else echo "BRANCH_NAME: $BRANCH_NAME" echo "SPEC_FILE: $SPEC_FILE" diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 70602ec76e..a316f9998e 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -18,8 +18,20 @@ function Get-RepoRoot { # Get specs directory, with support for external location via SPECIFY_SPECS_DIR function Get-SpecsDir { param([string]$RepoRoot = (Get-RepoRoot)) + if ($env:SPECIFY_SPECS_DIR) { - return $env:SPECIFY_SPECS_DIR + $specsDir = $env:SPECIFY_SPECS_DIR + # Require absolute path + if (-not [System.IO.Path]::IsPathRooted($specsDir)) { + Write-Error "[specify] ERROR: SPECIFY_SPECS_DIR must be an absolute path (got: '$specsDir')" + throw "SPECIFY_SPECS_DIR must be an absolute path" + } + # Block path traversal + if ($specsDir -match '\.\.') { + Write-Error "[specify] ERROR: SPECIFY_SPECS_DIR must not contain '..' (got: '$specsDir')" + throw "SPECIFY_SPECS_DIR must not contain '..'" + } + return $specsDir } return Join-Path $RepoRoot "specs" } diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 25b4fb2a13..7ec31377e8 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -271,6 +271,7 @@ if ($Json) { SPEC_FILE = $specFile FEATURE_NUM = $featureNum HAS_GIT = $hasGit + SPECS_DIR = $specsDir } $obj | ConvertTo-Json -Compress } else { diff --git a/templates/commands/specify.md b/templates/commands/specify.md index ddd6e3ed33..f6c10f675d 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -73,7 +73,7 @@ Given that feature description, do this: 3. Load `templates/spec-template.md` to understand required sections. -4. **Load shared context (if available)**: Check if `SPECS_DIR/_shared/` directory exists (SPECS_DIR is returned in the script JSON output). If it exists, read all `.md` files from it. These contain project-wide standards (architecture decisions, API conventions, coding standards, security requirements). Use this context to inform spec structure and ensure alignment with established patterns. Do not modify shared files. +4. **Load shared context (if available)**: Check if `SPECS_DIR/_shared/` directory exists (SPECS_DIR is provided in the JSON output from the setup script). If it exists, read all `.md` files from it. These contain project-wide standards (architecture decisions, API conventions, coding standards, security requirements). Use this context to inform spec structure and ensure alignment with established patterns. Do not modify shared files. 5. Follow this execution flow: From fdf051012f079363be63f9d0cecaa12a59b8552b Mon Sep 17 00:00:00 2001 From: Alan Meadows Date: Thu, 5 Feb 2026 13:45:09 -0800 Subject: [PATCH 3/7] fix: Harden SPECIFY_SPECS_DIR handling and improve clarify workflow - Use get_specs_dir/Get-SpecsDir consistently instead of raw env var access - Support relative paths by resolving against repo root - Add exit-on-failure checks after all get_specs_dir calls - Improve json_escape to handle all JSON control characters - Add explanatory comment for pre-formatted JSON array usage - Separate shared context loading into dedicated step in clarify.md --- scripts/bash/check-prerequisites.sh | 5 +++-- scripts/bash/common.sh | 21 ++++++++++----------- scripts/bash/create-new-feature.sh | 2 +- scripts/powershell/check-prerequisites.ps1 | 2 ++ scripts/powershell/common.ps1 | 10 ++-------- scripts/powershell/create-new-feature.ps1 | 6 +++++- templates/commands/clarify.md | 18 +++++++++++------- 7 files changed, 34 insertions(+), 30 deletions(-) diff --git a/scripts/bash/check-prerequisites.sh b/scripts/bash/check-prerequisites.sh index 91fb9d684b..fed6a0a8f4 100644 --- a/scripts/bash/check-prerequisites.sh +++ b/scripts/bash/check-prerequisites.sh @@ -84,7 +84,7 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 # If paths-only mode, output paths and exit (support JSON + paths-only combined) if $PATHS_ONLY; then - SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" + SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" || exit 1 if $JSON_MODE; then # Minimal JSON paths payload (no validation performed) printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s","SPECS_DIR":"%s"}\n' \ @@ -151,7 +151,8 @@ if $JSON_MODE; then json_docs="[${json_docs%,}]" fi - SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" + SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" || exit 1 + # Note: $json_docs is not escaped because it is already a pre-formatted JSON array printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"SPECS_DIR":"%s"}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "$SPECS_DIR")" else # Text output diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 569470d05d..bed3b70fc2 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -1,16 +1,21 @@ #!/usr/bin/env bash # Common functions and variables for all scripts -# Escape a string for safe inclusion in JSON +# Escape a string for safe inclusion in JSON (handles all JSON-required escapes) json_escape() { local str="$1" - # Escape backslashes first, then quotes, then other special chars + # Escape backslashes first, then quotes, then control characters str="${str//\\/\\\\}" str="${str//\"/\\\"}" str="${str//$'\n'/\\n}" str="${str//$'\r'/\\r}" str="${str//$'\t'/\\t}" - echo "$str" + str="${str//$'\b'/\\b}" + str="${str//$'\f'/\\f}" + # Remove any remaining control characters (U+0000-U+001F) that are + # not covered above, since they are invalid unescaped in JSON strings. + str="$(printf '%s' "$str" | tr -d '\000-\006\016-\037')" + printf '%s' "$str" } # Get repository root, with fallback for non-git repositories @@ -31,15 +36,9 @@ get_specs_dir() { if [[ -n "${SPECIFY_SPECS_DIR:-}" ]]; then specs_dir="$SPECIFY_SPECS_DIR" - # Require absolute path + # Resolve relative paths against repo root if [[ "$specs_dir" != /* ]]; then - echo "[specify] ERROR: SPECIFY_SPECS_DIR must be an absolute path (got: '$specs_dir')" >&2 - return 1 - fi - # Block path traversal - if [[ "$specs_dir" == *".."* ]]; then - echo "[specify] ERROR: SPECIFY_SPECS_DIR must not contain '..' (got: '$specs_dir')" >&2 - return 1 + specs_dir="$repo_root/$specs_dir" fi else specs_dir="$repo_root/specs" diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 3782979f27..905b526595 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -174,7 +174,7 @@ fi cd "$REPO_ROOT" -SPECS_DIR="${SPECIFY_SPECS_DIR:-$REPO_ROOT/specs}" +SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" || exit 1 mkdir -p "$SPECS_DIR" # Function to generate branch name with stop word filtering and length filtering diff --git a/scripts/powershell/check-prerequisites.ps1 b/scripts/powershell/check-prerequisites.ps1 index bcfe10cb3e..dab4120fdc 100644 --- a/scripts/powershell/check-prerequisites.ps1 +++ b/scripts/powershell/check-prerequisites.ps1 @@ -66,6 +66,7 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GI # If paths-only mode, output paths and exit (support combined -Json -PathsOnly) if ($PathsOnly) { $specsDir = Get-SpecsDir -RepoRoot $paths.REPO_ROOT + if (-not $specsDir) { exit 1 } if ($Json) { [PSCustomObject]@{ REPO_ROOT = $paths.REPO_ROOT @@ -131,6 +132,7 @@ if ($IncludeTasks -and (Test-Path $paths.TASKS)) { if ($Json) { # JSON output $specsDir = Get-SpecsDir -RepoRoot $paths.REPO_ROOT + if (-not $specsDir) { exit 1 } [PSCustomObject]@{ FEATURE_DIR = $paths.FEATURE_DIR AVAILABLE_DOCS = $docs diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index a316f9998e..57c0081038 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -21,15 +21,9 @@ function Get-SpecsDir { if ($env:SPECIFY_SPECS_DIR) { $specsDir = $env:SPECIFY_SPECS_DIR - # Require absolute path + # Resolve relative paths against repo root if (-not [System.IO.Path]::IsPathRooted($specsDir)) { - Write-Error "[specify] ERROR: SPECIFY_SPECS_DIR must be an absolute path (got: '$specsDir')" - throw "SPECIFY_SPECS_DIR must be an absolute path" - } - # Block path traversal - if ($specsDir -match '\.\.') { - Write-Error "[specify] ERROR: SPECIFY_SPECS_DIR must not contain '..' (got: '$specsDir')" - throw "SPECIFY_SPECS_DIR must not contain '..'" + $specsDir = Join-Path $RepoRoot $specsDir } return $specsDir } diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 7ec31377e8..ed23a0f2cb 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -149,7 +149,11 @@ try { Set-Location $repoRoot -$specsDir = if ($env:SPECIFY_SPECS_DIR) { $env:SPECIFY_SPECS_DIR } else { Join-Path $repoRoot 'specs' } +$specsDir = Get-SpecsDir -RepoRoot $repoRoot +if (-not $specsDir) { + Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red + exit 1 +} New-Item -ItemType Directory -Path $specsDir -Force | Out-Null # Function to generate branch name with stop word filtering and length filtering diff --git a/templates/commands/clarify.md b/templates/commands/clarify.md index fbc7366f3d..b29b315c52 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -32,7 +32,11 @@ Execution steps: - If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment. - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). -2. Load the current spec file. **IF `SPECS_DIR/_shared/` exists** (SPECS_DIR is in the JSON output): Read all `.md` files from it for project-wide context (architecture decisions, conventions, standards). Use this to validate spec alignment with project standards and inform ambiguity detection. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). +2. Load the current spec file from `FEATURE_SPEC`. + +3. Load shared project context. **IF `SPECS_DIR/_shared/` exists** (SPECS_DIR is in the JSON output): Read all `.md` files from it for project-wide context (architecture decisions, conventions, standards). Use this shared context to validate spec alignment with project standards and inform ambiguity detection in the next step. If the directory does not exist, proceed without shared context. + +4. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). Functional Scope & Behavior: - Core user goals & success criteria @@ -88,7 +92,7 @@ Execution steps: - Clarification would not materially change implementation or validation strategy - Information is better deferred to planning phase (note internally) -3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: +5. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: - Maximum of 10 total questions across the whole session. - Each question must be answerable with EITHER: - A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR @@ -99,7 +103,7 @@ Execution steps: - Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests. - If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic. -4. Sequential questioning loop (interactive): +6. Sequential questioning loop (interactive): - Present EXACTLY ONE question at a time. - For multiple‑choice questions: - **Analyze all options** and determine the **most suitable option** based on: @@ -135,7 +139,7 @@ Execution steps: - Never reveal future queued questions in advance. - If no valid questions exist at start, immediately report no critical ambiguities. -5. Integration after EACH accepted answer (incremental update approach): +7. Integration after EACH accepted answer (incremental update approach): - Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents. - For the first integrated answer in this session: - Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing). @@ -153,7 +157,7 @@ Execution steps: - Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact. - Keep each inserted clarification minimal and testable (avoid narrative drift). -6. Validation (performed after EACH write plus final pass): +8. Validation (performed after EACH write plus final pass): - Clarifications session contains exactly one bullet per accepted answer (no duplicates). - Total asked (accepted) questions ≤ 5. - Updated sections contain no lingering vague placeholders the new answer was meant to resolve. @@ -161,9 +165,9 @@ Execution steps: - Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`. - Terminology consistency: same canonical term used across all updated sections. -7. Write the updated spec back to `FEATURE_SPEC`. +9. Write the updated spec back to `FEATURE_SPEC`. -8. Report completion (after questioning loop ends or early termination): +10. Report completion (after questioning loop ends or early termination): - Number of questions asked & answered. - Path to updated spec. - Sections touched (list names). From 8d99e694c8a3f339bb119a5bc12bc43472bf5911 Mon Sep 17 00:00:00 2001 From: Alan Meadows Date: Thu, 5 Feb 2026 14:22:19 -0800 Subject: [PATCH 4/7] docs: Scaffold _shared directory with README on first feature creation Both create-new-feature scripts now auto-create SPECS_DIR/_shared/ with a README.md (from .specify/templates/_shared/README.md) when the shared directory does not yet exist. The README documents what files to place there, which commands consume them, and provides usage examples. --- scripts/bash/create-new-feature.sh | 10 ++++++ scripts/powershell/create-new-feature.ps1 | 10 ++++++ templates/_shared/README.md | 39 +++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 templates/_shared/README.md diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 905b526595..b57f42de54 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -177,6 +177,16 @@ cd "$REPO_ROOT" SPECS_DIR="$(get_specs_dir "$REPO_ROOT")" || exit 1 mkdir -p "$SPECS_DIR" +# Scaffold _shared directory with README if it doesn't exist yet +SHARED_DIR="$SPECS_DIR/_shared" +if [ ! -d "$SHARED_DIR" ]; then + mkdir -p "$SHARED_DIR" + SHARED_README="$REPO_ROOT/.specify/templates/_shared/README.md" + if [ -f "$SHARED_README" ]; then + cp "$SHARED_README" "$SHARED_DIR/README.md" + fi +fi + # Function to generate branch name with stop word filtering and length filtering generate_branch_name() { local description="$1" diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index ed23a0f2cb..89a7163a2d 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -156,6 +156,16 @@ if (-not $specsDir) { } New-Item -ItemType Directory -Path $specsDir -Force | Out-Null +# Scaffold _shared directory with README if it doesn't exist yet +$sharedDir = Join-Path $specsDir '_shared' +if (-not (Test-Path $sharedDir)) { + New-Item -ItemType Directory -Path $sharedDir -Force | Out-Null + $sharedReadme = Join-Path $repoRoot '.specify/templates/_shared/README.md' + if (Test-Path $sharedReadme) { + Copy-Item $sharedReadme (Join-Path $sharedDir 'README.md') -Force + } +} + # Function to generate branch name with stop word filtering and length filtering function Get-BranchName { param([string]$Description) diff --git a/templates/_shared/README.md b/templates/_shared/README.md new file mode 100644 index 0000000000..5581db23bb --- /dev/null +++ b/templates/_shared/README.md @@ -0,0 +1,39 @@ +# Shared Context + +This directory holds project-wide standards and conventions that apply across +all feature specs. Every `.md` file placed here is automatically loaded as +read-only context by the following Spec Kit commands: + +- **specify** -- informs spec structure and alignment with project standards +- **clarify** -- validates spec alignment and informs ambiguity detection +- **plan** -- guides technical decisions in implementation plans +- **tasks** -- informs task structure and sequencing +- **implement** -- guides implementation decisions and coding patterns +- **checklist** -- incorporates project-wide requirements into validation +- **analyze** -- uses standards as additional validation criteria + +## What to put here + +Create `.md` files for any project-wide standards you want consistently applied: + +- **Architecture decisions** -- system boundaries, data flow, service topology +- **API conventions** -- endpoint naming, versioning, request/response patterns +- **Coding standards** -- naming, formatting, error handling, logging patterns +- **Security requirements** -- authentication, authorization, input validation +- **Accessibility standards** -- WCAG compliance, ARIA patterns +- **Testing conventions** -- coverage expectations, mocking strategies, test naming + +## Example + +``` +specs/_shared/ + api-conventions.md + coding-standards.md + security-requirements.md +``` + +## Notes + +- Files are **read-only** -- Spec Kit commands never modify shared context. +- Only `.md` files are loaded; other file types are ignored. +- This directory is optional. If absent, commands proceed without shared context. From 71aa427adc62a83bc8476b801bc608d553793b54 Mon Sep 17 00:00:00 2001 From: Alan Meadows Date: Fri, 6 Feb 2026 11:56:23 -0800 Subject: [PATCH 5/7] fix: Add worktree mode to skip branch operations when SPECIFY_SPECS_DIR is set - Source common.sh in create-new-feature.sh (fixes missing get_specs_dir/json_escape) - Skip git checkout -b and git fetch --all --prune when SPECIFY_SPECS_DIR is set - Fall back to local directory scan for feature numbering in worktree mode - Skip branch naming validation in check_feature_branch/Test-FeatureBranch - Add WORKTREE_MODE field to JSON output for LLM template awareness - Update specify.md to conditionally skip branch scanning steps 2a-2c --- scripts/bash/common.sh | 6 ++++++ scripts/bash/create-new-feature.sh | 25 ++++++++++++++++++----- scripts/powershell/common.ps1 | 6 ++++++ scripts/powershell/create-new-feature.ps1 | 15 +++++++++++--- templates/commands/specify.md | 7 +++++-- 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index bed3b70fc2..e7b5d50902 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -101,6 +101,12 @@ check_feature_branch() { local branch="$1" local has_git_repo="$2" + # When SPECIFY_SPECS_DIR is set (e.g., worktree mode), skip branch naming + # validation since the branch/worktree may not follow the NNN- convention. + if [[ -n "${SPECIFY_SPECS_DIR:-}" ]]; then + return 0 + fi + # For non-git repos, we can't enforce branch naming but still provide output if [[ "$has_git_repo" != "true" ]]; then echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index b57f42de54..ce6c2b9d62 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -160,6 +160,10 @@ clean_branch_name() { # were initialised with --no-git. SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Source shared functions (get_specs_dir, json_escape, etc.) +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + if git rev-parse --show-toplevel >/dev/null 2>&1; then REPO_ROOT=$(git rev-parse --show-toplevel) HAS_GIT=true @@ -244,13 +248,22 @@ else BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") fi +# When SPECIFY_SPECS_DIR is set, we assume an external specs directory (e.g., a +# git worktree dedicated to this feature). Skip branch creation and git fetch +# since the caller controls the branch lifecycle. +WORKTREE_MODE=false +if [ -n "${SPECIFY_SPECS_DIR:-}" ]; then + WORKTREE_MODE=true +fi + # Determine branch number if [ -z "$BRANCH_NUMBER" ]; then - if [ "$HAS_GIT" = true ]; then + if [ "$HAS_GIT" = true ] && [ "$WORKTREE_MODE" = false ]; then # Check existing branches on remotes BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") else - # Fall back to local directory check + # Fall back to local directory check (also used in worktree mode to + # avoid running git fetch --all --prune against the parent repo) HIGHEST=$(get_highest_from_specs "$SPECS_DIR") BRANCH_NUMBER=$((HIGHEST + 1)) fi @@ -281,7 +294,9 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" fi -if [ "$HAS_GIT" = true ]; then +if [ "$WORKTREE_MODE" = true ]; then + >&2 echo "[specify] Worktree mode: skipping branch creation (SPECIFY_SPECS_DIR is set)" +elif [ "$HAS_GIT" = true ]; then git checkout -b "$BRANCH_NAME" else >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" @@ -298,8 +313,8 @@ if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE" export SPECIFY_FEATURE="$BRANCH_NAME" if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","SPECS_DIR":"%s"}\n' \ - "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" "$(json_escape "$SPECS_DIR")" + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","SPECS_DIR":"%s","WORKTREE_MODE":%s}\n' \ + "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" "$(json_escape "$SPECS_DIR")" "$WORKTREE_MODE" else echo "BRANCH_NAME: $BRANCH_NAME" echo "SPEC_FILE: $SPEC_FILE" diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 57c0081038..05d6e767b0 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -88,6 +88,12 @@ function Test-FeatureBranch { [bool]$HasGit = $true ) + # When SPECIFY_SPECS_DIR is set (e.g., worktree mode), skip branch naming + # validation since the branch/worktree may not follow the NNN- convention. + if ($env:SPECIFY_SPECS_DIR) { + return $true + } + # For non-git repos, we can't enforce branch naming but still provide output if (-not $HasGit) { Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation" diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 89a7163a2d..609d5fb4b7 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -149,6 +149,11 @@ try { Set-Location $repoRoot +# When SPECIFY_SPECS_DIR is set, we assume an external specs directory (e.g., a +# git worktree dedicated to this feature). Skip branch creation and git fetch +# since the caller controls the branch lifecycle. +$worktreeMode = [bool]$env:SPECIFY_SPECS_DIR + $specsDir = Get-SpecsDir -RepoRoot $repoRoot if (-not $specsDir) { Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red @@ -222,11 +227,12 @@ if ($ShortName) { # Determine branch number if ($Number -eq 0) { - if ($hasGit) { + if ($hasGit -and -not $worktreeMode) { # Check existing branches on remotes $Number = Get-NextBranchNumber -SpecsDir $specsDir } else { - # Fall back to local directory check + # Fall back to local directory check (also used in worktree mode to + # avoid running git fetch --all --prune against the parent repo) $Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1 } } @@ -255,7 +261,9 @@ if ($branchName.Length -gt $maxBranchLength) { Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)" } -if ($hasGit) { +if ($worktreeMode) { + Write-Warning "[specify] Worktree mode: skipping branch creation (SPECIFY_SPECS_DIR is set)" +} elseif ($hasGit) { try { git checkout -b $branchName | Out-Null } catch { @@ -286,6 +294,7 @@ if ($Json) { FEATURE_NUM = $featureNum HAS_GIT = $hasGit SPECS_DIR = $specsDir + WORKTREE_MODE = $worktreeMode } $obj | ConvertTo-Json -Compress } else { diff --git a/templates/commands/specify.md b/templates/commands/specify.md index f6c10f675d..ffd4e24d34 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -41,6 +41,8 @@ Given that feature description, do this: 2. **Check for existing branches before creating new one**: + **IF the environment variable `SPECIFY_SPECS_DIR` is set** (worktree mode), skip steps 2a-2c entirely and go directly to 2d. The script will detect worktree mode automatically and skip branch creation/checkout. The feature directory and spec file will still be created under the custom specs directory. + a. First, fetch all remote branches to ensure we have the latest information: ```bash @@ -50,7 +52,7 @@ Given that feature description, do this: b. Find the highest feature number across all sources for the short-name: - Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-$'` - Local branches: `git branch | grep -E '^[* ]*[0-9]+-$'` - - Specs directories: Check for directories matching `specs/[0-9]+-` + - Specs directories: Check for directories matching the specs directory for `[0-9]+-` c. Determine the next available number: - Extract all numbers from all three sources @@ -69,6 +71,7 @@ Given that feature description, do this: - You must only ever run this script once per feature - The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for - The JSON output will contain BRANCH_NAME and SPEC_FILE paths + - If WORKTREE_MODE is true in the JSON output, the script skipped branch creation/checkout (the current branch is used as-is) - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot") 3. Load `templates/spec-template.md` to understand required sections. @@ -197,7 +200,7 @@ Given that feature description, do this: 8. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`). -**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. +**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. In worktree mode (WORKTREE_MODE is true in JSON output), the script skips branch creation/checkout since the worktree already represents the feature branch. ## General Guidelines From b2e9c9522f84c5826f7a6cd59826816341abac31 Mon Sep 17 00:00:00 2001 From: Alan Meadows Date: Fri, 6 Feb 2026 12:41:18 -0800 Subject: [PATCH 6/7] fix: Use WORKTREE_MODE from JSON output instead of env var check in specify template The LLM executing the template cannot access environment variables like SPECIFY_SPECS_DIR. Remove the broken env var conditional and rely on the WORKTREE_MODE field in the script's JSON output for reporting guidance. --- templates/commands/specify.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/templates/commands/specify.md b/templates/commands/specify.md index ffd4e24d34..951fbd45b6 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -41,8 +41,6 @@ Given that feature description, do this: 2. **Check for existing branches before creating new one**: - **IF the environment variable `SPECIFY_SPECS_DIR` is set** (worktree mode), skip steps 2a-2c entirely and go directly to 2d. The script will detect worktree mode automatically and skip branch creation/checkout. The feature directory and spec file will still be created under the custom specs directory. - a. First, fetch all remote branches to ensure we have the latest information: ```bash @@ -200,7 +198,7 @@ Given that feature description, do this: 8. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`). -**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. In worktree mode (WORKTREE_MODE is true in JSON output), the script skips branch creation/checkout since the worktree already represents the feature branch. +**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. If `WORKTREE_MODE` is `true` in the JSON output, the script skipped branch creation/checkout because the user has configured a custom specs directory -- the current branch is used as-is. When reporting completion in worktree mode, note that the branch was not created by the script. ## General Guidelines From 296517a55fe2cae2b9724fa49a40436ff2c537c3 Mon Sep 17 00:00:00 2001 From: Alan Meadows Date: Fri, 6 Feb 2026 14:16:10 -0800 Subject: [PATCH 7/7] refactor: Replace WORKTREE_MODE with --no-branch flag for skipping branch operations - Add --no-branch CLI flag to create-new-feature.sh and .ps1 - Auto-enable --no-branch when SPECIFY_SPECS_DIR env var is set - Output NO_BRANCH field in JSON (replaces WORKTREE_MODE) - Update specify.md to parse --no-branch from user arguments and skip branch scanning steps (git fetch, branch/dir lookups) - Users invoke with: /speckit.specify --no-branch "feature description" --- scripts/bash/create-new-feature.sh | 29 +++++++++++++---------- scripts/powershell/create-new-feature.ps1 | 23 ++++++++++-------- templates/commands/specify.md | 17 +++++++++---- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index ce6c2b9d62..7ebb9b2c26 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -5,6 +5,7 @@ set -e JSON_MODE=false SHORT_NAME="" BRANCH_NUMBER="" +NO_BRANCH=false ARGS=() i=1 while [ $i -le $# ]; do @@ -13,6 +14,9 @@ while [ $i -le $# ]; do --json) JSON_MODE=true ;; + --no-branch) + NO_BRANCH=true + ;; --short-name) if [ $((i + 1)) -gt $# ]; then echo 'Error: --short-name requires a value' >&2 @@ -41,17 +45,19 @@ while [ $i -le $# ]; do BRANCH_NUMBER="$next_arg" ;; --help|-h) - echo "Usage: $0 [--json] [--short-name ] [--number N] " + echo "Usage: $0 [--json] [--short-name ] [--number N] [--no-branch] " echo "" echo "Options:" echo " --json Output in JSON format" echo " --short-name Provide a custom short name (2-4 words) for the branch" echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --no-branch Skip branch creation/checkout and git fetch" echo " --help, -h Show this help message" echo "" echo "Examples:" echo " $0 'Add user authentication system' --short-name 'user-auth'" echo " $0 'Implement OAuth2 integration for API' --number 5" + echo " $0 --no-branch 'Fix payment timeout' --short-name 'fix-payment'" exit 0 ;; *) @@ -248,21 +254,20 @@ else BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") fi -# When SPECIFY_SPECS_DIR is set, we assume an external specs directory (e.g., a -# git worktree dedicated to this feature). Skip branch creation and git fetch -# since the caller controls the branch lifecycle. -WORKTREE_MODE=false +# When SPECIFY_SPECS_DIR is set, automatically enable --no-branch since the +# caller controls the branch lifecycle (e.g., a git worktree dedicated to this +# feature). if [ -n "${SPECIFY_SPECS_DIR:-}" ]; then - WORKTREE_MODE=true + NO_BRANCH=true fi # Determine branch number if [ -z "$BRANCH_NUMBER" ]; then - if [ "$HAS_GIT" = true ] && [ "$WORKTREE_MODE" = false ]; then + if [ "$HAS_GIT" = true ] && [ "$NO_BRANCH" = false ]; then # Check existing branches on remotes BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") else - # Fall back to local directory check (also used in worktree mode to + # Fall back to local directory check (also used in --no-branch mode to # avoid running git fetch --all --prune against the parent repo) HIGHEST=$(get_highest_from_specs "$SPECS_DIR") BRANCH_NUMBER=$((HIGHEST + 1)) @@ -294,8 +299,8 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" fi -if [ "$WORKTREE_MODE" = true ]; then - >&2 echo "[specify] Worktree mode: skipping branch creation (SPECIFY_SPECS_DIR is set)" +if [ "$NO_BRANCH" = true ]; then + >&2 echo "[specify] --no-branch: skipping branch creation" elif [ "$HAS_GIT" = true ]; then git checkout -b "$BRANCH_NAME" else @@ -313,8 +318,8 @@ if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE" export SPECIFY_FEATURE="$BRANCH_NAME" if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","SPECS_DIR":"%s","WORKTREE_MODE":%s}\n' \ - "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" "$(json_escape "$SPECS_DIR")" "$WORKTREE_MODE" + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","SPECS_DIR":"%s","NO_BRANCH":%s}\n' \ + "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" "$(json_escape "$SPECS_DIR")" "$NO_BRANCH" else echo "BRANCH_NAME: $BRANCH_NAME" echo "SPEC_FILE: $SPEC_FILE" diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 609d5fb4b7..59eaa1233b 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -5,6 +5,7 @@ param( [switch]$Json, [string]$ShortName, [int]$Number = 0, + [switch]$NoBranch, [switch]$Help, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$FeatureDescription @@ -19,6 +20,7 @@ if ($Help) { Write-Host " -Json Output in JSON format" Write-Host " -ShortName Provide a custom short name (2-4 words) for the branch" Write-Host " -Number N Specify branch number manually (overrides auto-detection)" + Write-Host " -NoBranch Skip branch creation (auto-enabled when SPECIFY_SPECS_DIR is set)" Write-Host " -Help Show this help message" Write-Host "" Write-Host "Examples:" @@ -149,10 +151,11 @@ try { Set-Location $repoRoot -# When SPECIFY_SPECS_DIR is set, we assume an external specs directory (e.g., a -# git worktree dedicated to this feature). Skip branch creation and git fetch -# since the caller controls the branch lifecycle. -$worktreeMode = [bool]$env:SPECIFY_SPECS_DIR +# Auto-enable --no-branch when SPECIFY_SPECS_DIR is set (e.g., worktree scenario +# where the caller controls the branch lifecycle). +if (-not $NoBranch -and $env:SPECIFY_SPECS_DIR) { + $NoBranch = $true +} $specsDir = Get-SpecsDir -RepoRoot $repoRoot if (-not $specsDir) { @@ -227,12 +230,12 @@ if ($ShortName) { # Determine branch number if ($Number -eq 0) { - if ($hasGit -and -not $worktreeMode) { + if ($hasGit -and -not $NoBranch) { # Check existing branches on remotes $Number = Get-NextBranchNumber -SpecsDir $specsDir } else { - # Fall back to local directory check (also used in worktree mode to - # avoid running git fetch --all --prune against the parent repo) + # Fall back to local directory check (also used in --no-branch mode + # to avoid running git fetch --all --prune against the parent repo) $Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1 } } @@ -261,8 +264,8 @@ if ($branchName.Length -gt $maxBranchLength) { Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)" } -if ($worktreeMode) { - Write-Warning "[specify] Worktree mode: skipping branch creation (SPECIFY_SPECS_DIR is set)" +if ($NoBranch) { + Write-Warning "[specify] --no-branch: skipping branch creation" } elseif ($hasGit) { try { git checkout -b $branchName | Out-Null @@ -294,7 +297,7 @@ if ($Json) { FEATURE_NUM = $featureNum HAS_GIT = $hasGit SPECS_DIR = $specsDir - WORKTREE_MODE = $worktreeMode + NO_BRANCH = $NoBranch } $obj | ConvertTo-Json -Compress } else { diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 951fbd45b6..172f1ac1c0 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -39,9 +39,11 @@ Given that feature description, do this: - "Create a dashboard for analytics" → "analytics-dashboard" - "Fix payment processing timeout bug" → "fix-payment-timeout" -2. **Check for existing branches before creating new one**: +2. **Set up feature branch and run the create-new-feature script**: - a. First, fetch all remote branches to ensure we have the latest information: + **IF `--no-branch` is NOT present in the user's input**, perform branch scanning first: + + a. Fetch all remote branches to ensure we have the latest information: ```bash git fetch --all --prune @@ -57,19 +59,24 @@ Given that feature description, do this: - Find the highest number N - Use N+1 for the new branch number + **IF `--no-branch` IS present in the user's input**, skip steps 2a-2c entirely. The script will determine the feature number from local directories only and will not create or switch git branches. Pass `--no-branch` through to the script. + d. Run the script `{SCRIPT}` with the calculated number and short-name: - Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description + - If `--no-branch` was specified, also pass `--no-branch` to the script - Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"` + - Bash example (no-branch): `{SCRIPT} --json --no-branch --short-name "user-auth" "Add user authentication"` - PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"` + - PowerShell example (no-branch): `{SCRIPT} -Json -NoBranch -ShortName "user-auth" "Add user authentication"` **IMPORTANT**: - - Check all three sources (remote branches, local branches, specs directories) to find the highest number + - When doing branch scanning (steps 2a-2c), check all three sources (remote branches, local branches, specs directories) to find the highest number - Only match branches/directories with the exact short-name pattern - If no existing branches/directories found with this short-name, start with number 1 - You must only ever run this script once per feature - The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for - The JSON output will contain BRANCH_NAME and SPEC_FILE paths - - If WORKTREE_MODE is true in the JSON output, the script skipped branch creation/checkout (the current branch is used as-is) + - If NO_BRANCH is true in the JSON output, the script skipped branch creation/checkout (the current branch is used as-is) - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot") 3. Load `templates/spec-template.md` to understand required sections. @@ -198,7 +205,7 @@ Given that feature description, do this: 8. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`). -**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. If `WORKTREE_MODE` is `true` in the JSON output, the script skipped branch creation/checkout because the user has configured a custom specs directory -- the current branch is used as-is. When reporting completion in worktree mode, note that the branch was not created by the script. +**NOTE:** By default, the script creates and checks out a new branch and initializes the spec file before writing. If `NO_BRANCH` is `true` in the JSON output (set by `--no-branch` flag or automatically when the user has configured a custom specs directory via `SPECIFY_SPECS_DIR`), the script skipped branch creation/checkout -- the current branch is used as-is. When reporting completion in no-branch mode, note that the branch was not created by the script. ## General Guidelines