From 2307704d7c161b36d74899ef94bb3a2fcf2bd63b Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Sat, 29 Aug 2026 00:01:23 +0500 Subject: [PATCH] add --require-spec to check-prerequisites the script resolves FEATURE_SPEC and reports it but never checks that the file is there, so analyze and converge pass the prerequisite check and then fail later reading a spec that does not exist, without the run specify first guidance the script gives for every other artifact the flag is opt in so nothing changes for callers that do not read the spec. analyze and converge pass it because they do added to all three script variants because the parity tests compare their help text and error output against each other --- scripts/bash/check-prerequisites.sh | 13 +++++++ scripts/powershell/check-prerequisites.ps1 | 14 +++++++- scripts/python/check_prerequisites.py | 14 ++++++++ templates/commands/analyze.md | 4 +-- templates/commands/converge.md | 4 +-- .../test_check_prerequisites_python_parity.py | 36 +++++++++++++++++++ 6 files changed, 80 insertions(+), 5 deletions(-) diff --git a/scripts/bash/check-prerequisites.sh b/scripts/bash/check-prerequisites.sh index c21edc41f0..7d6dba1353 100644 --- a/scripts/bash/check-prerequisites.sh +++ b/scripts/bash/check-prerequisites.sh @@ -9,6 +9,7 @@ # # OPTIONS: # --json Output in JSON format +# --require-spec Require spec.md to exist (for analysis phase) # --require-tasks Require tasks.md to exist (for implementation phase) # --include-tasks Include tasks.md in AVAILABLE_DOCS list # --paths-only Only output path variables (no validation) @@ -24,6 +25,7 @@ set -e # Parse command line arguments JSON_MODE=false +REQUIRE_SPEC=false REQUIRE_TASKS=false INCLUDE_TASKS=false PATHS_ONLY=false @@ -34,6 +36,9 @@ while [[ $# -gt 0 ]]; do --json) JSON_MODE=true ;; + --require-spec) + REQUIRE_SPEC=true + ;; --require-tasks) REQUIRE_TASKS=true ;; @@ -59,6 +64,7 @@ Consolidated prerequisite checking for Spec-Driven Development workflow. OPTIONS: --json Output in JSON format + --require-spec Require spec.md to exist (for analysis phase) --require-tasks Require tasks.md to exist (for implementation phase) --include-tasks Include tasks.md in AVAILABLE_DOCS list --paths-only Only output path variables (no prerequisite validation) @@ -142,6 +148,13 @@ if [[ ! -f "$IMPL_PLAN" ]]; then exit 1 fi +# Check for spec.md if required +if $REQUIRE_SPEC && [[ ! -f "$FEATURE_SPEC" ]]; then + echo "ERROR: spec.md not found in $FEATURE_DIR" >&2 + echo "Run $(format_speckit_command specify "$REPO_ROOT") first to create the feature specification." >&2 + exit 1 +fi + # Check for tasks.md if required if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 diff --git a/scripts/powershell/check-prerequisites.ps1 b/scripts/powershell/check-prerequisites.ps1 index 27c87d6c69..1ad245e98c 100644 --- a/scripts/powershell/check-prerequisites.ps1 +++ b/scripts/powershell/check-prerequisites.ps1 @@ -9,7 +9,9 @@ # # OPTIONS: # -Json Output in JSON format -# -RequireTasks Require tasks.md to exist (for implementation phase) +# -RequireSpec Require spec.md to exist (for analysis phase) +# -RequireSpec Require spec.md to exist (for analysis phase) + -RequireTasks Require tasks.md to exist (for implementation phase) # -IncludeTasks Include tasks.md in AVAILABLE_DOCS list # -PathsOnly Only output path variables (no validation) # -Template NAME Include composed template content in JSON output @@ -18,6 +20,7 @@ [CmdletBinding()] param( [switch]$Json, + [switch]$RequireSpec, [switch]$RequireTasks, [switch]$IncludeTasks, [switch]$PathsOnly, @@ -36,6 +39,7 @@ Consolidated prerequisite checking for Spec-Driven Development workflow. OPTIONS: -Json Output in JSON format + -RequireSpec Require spec.md to exist (for analysis phase) -RequireTasks Require tasks.md to exist (for implementation phase) -IncludeTasks Include tasks.md in AVAILABLE_DOCS list -PathsOnly Only output path variables (no prerequisite validation) @@ -105,6 +109,14 @@ if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) { exit 1 } +# Check for spec.md if required +if ($RequireSpec -and -not (Test-Path $paths.FEATURE_SPEC -PathType Leaf)) { + [Console]::Error.WriteLine("ERROR: spec.md not found in $($paths.FEATURE_DIR)") + $specifyCommand = Format-SpecKitCommand -CommandName 'specify' -RepoRoot $paths.REPO_ROOT + [Console]::Error.WriteLine("Run $specifyCommand first to create the feature specification.") + exit 1 +} + # Check for tasks.md if required if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) { [Console]::Error.WriteLine("ERROR: tasks.md not found in $($paths.FEATURE_DIR)") diff --git a/scripts/python/check_prerequisites.py b/scripts/python/check_prerequisites.py index a5dc3e7e39..e025b4d672 100644 --- a/scripts/python/check_prerequisites.py +++ b/scripts/python/check_prerequisites.py @@ -37,6 +37,7 @@ def _json_line(payload: object) -> str: OPTIONS: --json Output in JSON format + --require-spec Require spec.md to exist (for analysis phase) --require-tasks Require tasks.md to exist (for implementation phase) --include-tasks Include tasks.md in AVAILABLE_DOCS list --paths-only Only output path variables (no prerequisite validation) @@ -59,6 +60,7 @@ def _json_line(payload: object) -> str: @dataclass(frozen=True) class Args: json_mode: bool = False + require_spec: bool = False require_tasks: bool = False include_tasks: bool = False paths_only: bool = False @@ -67,6 +69,7 @@ class Args: def _parse_args(argv: list[str]) -> Args: json_mode = False + require_spec = False require_tasks = False include_tasks = False paths_only = False @@ -77,6 +80,8 @@ def _parse_args(argv: list[str]) -> Args: arg = argv[index] if arg == "--json": json_mode = True + elif arg == "--require-spec": + require_spec = True elif arg == "--require-tasks": require_tasks = True elif arg == "--include-tasks": @@ -105,6 +110,7 @@ def _parse_args(argv: list[str]) -> Args: return Args( json_mode=json_mode, + require_spec=require_spec, require_tasks=require_tasks, include_tasks=include_tasks, paths_only=paths_only, @@ -230,6 +236,14 @@ def main(argv: list[str] | None = None) -> int: ) return 1 + if args.require_spec and not paths.feature_spec.is_file(): + print(f"ERROR: spec.md not found in {paths.feature_dir}", file=sys.stderr) + print( + f"Run {format_speckit_command('specify', paths.repo_root)} first to create the feature specification.", + file=sys.stderr, + ) + return 1 + if args.require_tasks and not paths.tasks.is_file(): print(f"ERROR: tasks.md not found in {paths.feature_dir}", file=sys.stderr) print( diff --git a/templates/commands/analyze.md b/templates/commands/analyze.md index 2cd83bd7c0..1049933eb2 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -1,8 +1,8 @@ --- description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation. scripts: - sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks - ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks + sh: scripts/bash/check-prerequisites.sh --json --require-spec --require-tasks --include-tasks + ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireSpec -RequireTasks -IncludeTasks py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks --- diff --git a/templates/commands/converge.md b/templates/commands/converge.md index eadb96ee58..93fe5cfb51 100644 --- a/templates/commands/converge.md +++ b/templates/commands/converge.md @@ -1,8 +1,8 @@ --- description: Assess the current codebase against the feature's spec, plan, and tasks, then append any remaining unbuilt work as new tasks to tasks.md so implement can complete it. scripts: - sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks - ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks + sh: scripts/bash/check-prerequisites.sh --json --require-spec --require-tasks --include-tasks + ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireSpec -RequireTasks -IncludeTasks py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks --- diff --git a/tests/test_check_prerequisites_python_parity.py b/tests/test_check_prerequisites_python_parity.py index b0e74217c0..b2333a0b48 100644 --- a/tests/test_check_prerequisites_python_parity.py +++ b/tests/test_check_prerequisites_python_parity.py @@ -247,6 +247,42 @@ def test_python_json_output_matches_bash(prereq_repo: Path, args: tuple[str, ... assert _json_stdout(py) == _json_stdout(bash) +@requires_bash +def test_python_require_spec_matches_bash(prereq_repo: Path) -> None: + feat = prereq_repo / "specs" / "001-my-feature" + feat.mkdir(parents=True) + (feat / "plan.md").write_text("# plan\n", encoding="utf-8") + (feat / "tasks.md").write_text("# tasks\n", encoding="utf-8") + _write_feature_json(prereq_repo) + + # spec.md is missing, and without the flag that stays the caller's problem + bash_without = _run(_bash_cmd(prereq_repo, "--json", "--require-tasks"), prereq_repo) + py_without = _run(_py_cmd(prereq_repo, "--json", "--require-tasks"), prereq_repo) + assert py_without.returncode == bash_without.returncode == 0 + + # with the flag both variants fail the same way and name the same command + bash_missing = _run( + _bash_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo + ) + py_missing = _run( + _py_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo + ) + assert py_missing.returncode == bash_missing.returncode == 1 + assert py_missing.stderr == bash_missing.stderr + assert "spec.md not found" in bash_missing.stderr + + # and once the spec exists the flag is satisfied + (feat / "spec.md").write_text("# spec\n", encoding="utf-8") + bash_present = _run( + _bash_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo + ) + py_present = _run( + _py_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo + ) + assert py_present.returncode == bash_present.returncode == 0 + assert _json_stdout(py_present) == _json_stdout(bash_present) + + @requires_bash def test_python_text_output_matches_bash(prereq_repo: Path) -> None: feat = prereq_repo / "specs" / "001-my-feature"