From 2307704d7c161b36d74899ef94bb3a2fcf2bd63b Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Sat, 29 Aug 2026 00:01:23 +0500 Subject: [PATCH 1/3] 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" From 09bf3f15478c86558c92d5218b0549e0b2ae92ac Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Sun, 30 Aug 2026 21:50:21 +0500 Subject: [PATCH 2/3] pass require spec in the py script line too analyze and converge read spec.md so the py runner needs the same guard as sh and ps. the python script already had the flag but no caller passed it so the python variant kept the old late failure. --- templates/commands/analyze.md | 2 +- templates/commands/converge.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/commands/analyze.md b/templates/commands/analyze.md index 1049933eb2..2e13af58ae 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -3,7 +3,7 @@ description: Perform a non-destructive cross-artifact consistency and quality an scripts: 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 + py: scripts/python/check_prerequisites.py --json --require-spec --require-tasks --include-tasks --- ## User Input diff --git a/templates/commands/converge.md b/templates/commands/converge.md index 93fe5cfb51..a177c31371 100644 --- a/templates/commands/converge.md +++ b/templates/commands/converge.md @@ -3,7 +3,7 @@ description: Assess the current codebase against the feature's spec, plan, and t scripts: 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 + py: scripts/python/check_prerequisites.py --json --require-spec --require-tasks --include-tasks --- ## User Input From 3c608ed3def975494376425e28e94eb6ae601a6c Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Tue, 1 Sep 2026 11:10:31 +0500 Subject: [PATCH 3/3] restore the options comments in the powershell script the require spec line got duplicated over the require tasks line and the second copy lost its leading hash, so line 14 was executable powershell sitting above CmdletBinding and the whole script stopped parsing. every test_ps_ test failed on ci because of it. parser says 2 errors before and none after: line 20 unexpected attribute CmdletBinding line 21 unexpected token param also add the powershell half of the require spec parity test, which is what would have caught this. --- scripts/powershell/check-prerequisites.ps1 | 3 +- .../test_check_prerequisites_python_parity.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/scripts/powershell/check-prerequisites.ps1 b/scripts/powershell/check-prerequisites.ps1 index 1ad245e98c..0e9434d063 100644 --- a/scripts/powershell/check-prerequisites.ps1 +++ b/scripts/powershell/check-prerequisites.ps1 @@ -10,8 +10,7 @@ # OPTIONS: # -Json Output in JSON format # -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) +# -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 diff --git a/tests/test_check_prerequisites_python_parity.py b/tests/test_check_prerequisites_python_parity.py index b2333a0b48..69372d3f17 100644 --- a/tests/test_check_prerequisites_python_parity.py +++ b/tests/test_check_prerequisites_python_parity.py @@ -283,6 +283,42 @@ def test_python_require_spec_matches_bash(prereq_repo: Path) -> None: assert _json_stdout(py_present) == _json_stdout(bash_present) +@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available") +def test_powershell_require_spec_matches_python(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 + ps_without = _run(_ps_cmd(prereq_repo, "-Json", "-RequireTasks"), prereq_repo) + py_without = _run(_py_cmd(prereq_repo, "--json", "--require-tasks"), prereq_repo) + assert ps_without.returncode == py_without.returncode == 0 + + # with the flag both variants fail the same way and name the same file + ps_missing = _run( + _ps_cmd(prereq_repo, "-Json", "-RequireSpec", "-RequireTasks"), prereq_repo + ) + py_missing = _run( + _py_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo + ) + assert ps_missing.returncode == py_missing.returncode == 1 + assert "spec.md not found" in ps_missing.stderr + assert "spec.md not found" in py_missing.stderr + + # and once the spec exists the flag is satisfied and the payloads agree + (feat / "spec.md").write_text("# spec\n", encoding="utf-8") + ps_present = _run( + _ps_cmd(prereq_repo, "-Json", "-RequireSpec", "-RequireTasks"), prereq_repo + ) + py_present = _run( + _py_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo + ) + assert ps_present.returncode == py_present.returncode == 0 + assert _json_stdout(ps_present) == _json_stdout(py_present) + + @requires_bash def test_python_text_output_matches_bash(prereq_repo: Path) -> None: feat = prereq_repo / "specs" / "001-my-feature"