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"