-
Notifications
You must be signed in to change notification settings - Fork 8k
feat: Add SPECIFY_SPECS_DIR for centralized specs directory and worktree support #1579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
4f6b64b
f1292f5
fdf0510
8d99e69
71aa427
b2e9c95
296517a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,23 @@ | ||
| #!/usr/bin/env bash | ||
| # Common functions and variables for all scripts | ||
|
|
||
| # Escape a string for safe inclusion in JSON (handles all JSON-required escapes) | ||
| json_escape() { | ||
| local str="$1" | ||
| # Escape backslashes first, then quotes, then control characters | ||
| str="${str//\\/\\\\}" | ||
| str="${str//\"/\\\"}" | ||
| str="${str//$'\n'/\\n}" | ||
| str="${str//$'\r'/\\r}" | ||
| str="${str//$'\t'/\\t}" | ||
| 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 | ||
| get_repo_root() { | ||
| if git rev-parse --show-toplevel >/dev/null 2>&1; then | ||
|
|
@@ -12,6 +29,24 @@ 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)}" | ||
| local specs_dir | ||
|
|
||
| if [[ -n "${SPECIFY_SPECS_DIR:-}" ]]; then | ||
| specs_dir="$SPECIFY_SPECS_DIR" | ||
| # Resolve relative paths against repo root | ||
| if [[ "$specs_dir" != /* ]]; then | ||
| specs_dir="$repo_root/$specs_dir" | ||
| fi | ||
| else | ||
| specs_dir="$repo_root/specs" | ||
| fi | ||
|
|
||
| echo "$specs_dir" | ||
| } | ||
|
Comment on lines
+33
to
+48
|
||
|
|
||
| # Get current branch, with fallback for non-git repositories | ||
| get_current_branch() { | ||
| # First check if SPECIFY_FEATURE environment variable is set | ||
|
|
@@ -28,7 +63,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 +116,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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,21 @@ 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) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $specsDir = $env:SPECIFY_SPECS_DIR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Resolve relative paths against repo root | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $specsDir = Join-Path $RepoRoot $specsDir | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $specsDir | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Join-Path $RepoRoot "specs" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
alanmeadows marked this conversation as resolved.
Comment on lines
+22
to
+30
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($env:SPECIFY_SPECS_DIR) { | |
| $specsDir = $env:SPECIFY_SPECS_DIR | |
| # Resolve relative paths against repo root | |
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | |
| $specsDir = Join-Path $RepoRoot $specsDir | |
| } | |
| return $specsDir | |
| } | |
| return Join-Path $RepoRoot "specs" | |
| $specsDir = $null | |
| if ($env:SPECIFY_SPECS_DIR) { | |
| $specsDir = $env:SPECIFY_SPECS_DIR | |
| # Resolve relative paths against repo root | |
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | |
| $specsDir = Join-Path $RepoRoot $specsDir | |
| } | |
| } else { | |
| $specsDir = Join-Path $RepoRoot "specs" | |
| } | |
| # Validate that, if the path exists, it is a directory | |
| if (Test-Path $specsDir) { | |
| if (-not (Test-Path $specsDir -PathType Container)) { | |
| Write-Error "Invalid specs directory path '$specsDir': path exists but is not a directory." | |
| return $null | |
| } | |
| } | |
| return $specsDir |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -149,7 +149,11 @@ try { | |||||||||||||||||
|
|
||||||||||||||||||
| Set-Location $repoRoot | ||||||||||||||||||
|
|
||||||||||||||||||
| $specsDir = 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 | ||||||||||||||||||
| } | ||||||||||||||||||
|
alanmeadows marked this conversation as resolved.
Comment on lines
+161
to
+164
|
||||||||||||||||||
| if (-not $specsDir) { | |
| Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red | |
| exit 1 | |
| } |
Copilot
AI
Feb 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The if (-not $specsDir) guard is currently unreachable because Get-SpecsDir (in common.ps1) always returns a non-empty string (either SPECIFY_SPECS_DIR resolved or the default Join-Path $RepoRoot 'specs'). Either remove this check, or add real validation to Get-SpecsDir (e.g., return $null when the path is invalid/uncreatable) so the error message can actually trigger.
| if (-not $specsDir) { | |
| Write-Host "`n[specify] ERROR: Invalid SPECIFY_SPECS_DIR configuration. Aborting." -ForegroundColor Red | |
| exit 1 | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Comment on lines
+70
to
+72
|
||
|
|
||
| ### 3. Build Semantic Models | ||
|
|
||
| Create internal representations (do not include raw artifacts in output): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
||
|
Comment on lines
81
to
86
|
||
| **Context Loading Strategy**: | ||
| - Load only necessary portions relevant to active focus areas (avoid full-file dumping) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. 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. | ||
|
|
||
|
Comment on lines
+28
to
+38
|
||
| 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,17 +157,17 @@ 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. | ||
| - No contradictory earlier statement remains (scan for now-invalid alternative choices removed). | ||
| - 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). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Comment on lines
55
to
+58
|
||
|
|
||
| 4. **Project Setup Verification**: | ||
| - **REQUIRED**: Create/verify ignore files based on actual project setup: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Comment on lines
29
to
+31
|
||
|
|
||
| 3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to: | ||
| - Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Comment on lines
27
to
33
|
||
|
|
||
| 3. **Execute task generation workflow**: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SPECIFY_SPECS_DIRis user-controlled and can now influence values emitted byget_feature_paths(which are later consumed viaeval $(get_feature_paths)in multiple scripts). Becauseget_feature_pathswraps values in single quotes, aSPECIFY_SPECS_DIRcontaining a'can break out of quoting and lead to command injection. Consider switching away fromeval-based exports, or ensure values are safely shell-escaped (e.g., escape single quotes) before being embedded in theget_feature_pathsoutput.