Skip to content

Commit ac55a7a

Browse files
add --require-spec to check-prerequisites (#4367)
* 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 * 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. * 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.
1 parent 40e0b4c commit ac55a7a

6 files changed

Lines changed: 116 additions & 6 deletions

File tree

scripts/bash/check-prerequisites.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#
1010
# OPTIONS:
1111
# --json Output in JSON format
12+
# --require-spec Require spec.md to exist (for analysis phase)
1213
# --require-tasks Require tasks.md to exist (for implementation phase)
1314
# --include-tasks Include tasks.md in AVAILABLE_DOCS list
1415
# --paths-only Only output path variables (no validation)
@@ -24,6 +25,7 @@ set -e
2425

2526
# Parse command line arguments
2627
JSON_MODE=false
28+
REQUIRE_SPEC=false
2729
REQUIRE_TASKS=false
2830
INCLUDE_TASKS=false
2931
PATHS_ONLY=false
@@ -34,6 +36,9 @@ while [[ $# -gt 0 ]]; do
3436
--json)
3537
JSON_MODE=true
3638
;;
39+
--require-spec)
40+
REQUIRE_SPEC=true
41+
;;
3742
--require-tasks)
3843
REQUIRE_TASKS=true
3944
;;
@@ -59,6 +64,7 @@ Consolidated prerequisite checking for Spec-Driven Development workflow.
5964
6065
OPTIONS:
6166
--json Output in JSON format
67+
--require-spec Require spec.md to exist (for analysis phase)
6268
--require-tasks Require tasks.md to exist (for implementation phase)
6369
--include-tasks Include tasks.md in AVAILABLE_DOCS list
6470
--paths-only Only output path variables (no prerequisite validation)
@@ -142,6 +148,13 @@ if [[ ! -f "$IMPL_PLAN" ]]; then
142148
exit 1
143149
fi
144150

151+
# Check for spec.md if required
152+
if $REQUIRE_SPEC && [[ ! -f "$FEATURE_SPEC" ]]; then
153+
echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
154+
echo "Run $(format_speckit_command specify "$REPO_ROOT") first to create the feature specification." >&2
155+
exit 1
156+
fi
157+
145158
# Check for tasks.md if required
146159
if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
147160
echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2

scripts/powershell/check-prerequisites.ps1

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#
1010
# OPTIONS:
1111
# -Json Output in JSON format
12+
# -RequireSpec Require spec.md to exist (for analysis phase)
1213
# -RequireTasks Require tasks.md to exist (for implementation phase)
1314
# -IncludeTasks Include tasks.md in AVAILABLE_DOCS list
1415
# -PathsOnly Only output path variables (no validation)
@@ -18,6 +19,7 @@
1819
[CmdletBinding()]
1920
param(
2021
[switch]$Json,
22+
[switch]$RequireSpec,
2123
[switch]$RequireTasks,
2224
[switch]$IncludeTasks,
2325
[switch]$PathsOnly,
@@ -36,6 +38,7 @@ Consolidated prerequisite checking for Spec-Driven Development workflow.
3638
3739
OPTIONS:
3840
-Json Output in JSON format
41+
-RequireSpec Require spec.md to exist (for analysis phase)
3942
-RequireTasks Require tasks.md to exist (for implementation phase)
4043
-IncludeTasks Include tasks.md in AVAILABLE_DOCS list
4144
-PathsOnly Only output path variables (no prerequisite validation)
@@ -105,6 +108,14 @@ if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
105108
exit 1
106109
}
107110

111+
# Check for spec.md if required
112+
if ($RequireSpec -and -not (Test-Path $paths.FEATURE_SPEC -PathType Leaf)) {
113+
[Console]::Error.WriteLine("ERROR: spec.md not found in $($paths.FEATURE_DIR)")
114+
$specifyCommand = Format-SpecKitCommand -CommandName 'specify' -RepoRoot $paths.REPO_ROOT
115+
[Console]::Error.WriteLine("Run $specifyCommand first to create the feature specification.")
116+
exit 1
117+
}
118+
108119
# Check for tasks.md if required
109120
if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) {
110121
[Console]::Error.WriteLine("ERROR: tasks.md not found in $($paths.FEATURE_DIR)")

scripts/python/check_prerequisites.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def _json_line(payload: object) -> str:
3737
3838
OPTIONS:
3939
--json Output in JSON format
40+
--require-spec Require spec.md to exist (for analysis phase)
4041
--require-tasks Require tasks.md to exist (for implementation phase)
4142
--include-tasks Include tasks.md in AVAILABLE_DOCS list
4243
--paths-only Only output path variables (no prerequisite validation)
@@ -59,6 +60,7 @@ def _json_line(payload: object) -> str:
5960
@dataclass(frozen=True)
6061
class Args:
6162
json_mode: bool = False
63+
require_spec: bool = False
6264
require_tasks: bool = False
6365
include_tasks: bool = False
6466
paths_only: bool = False
@@ -67,6 +69,7 @@ class Args:
6769

6870
def _parse_args(argv: list[str]) -> Args:
6971
json_mode = False
72+
require_spec = False
7073
require_tasks = False
7174
include_tasks = False
7275
paths_only = False
@@ -77,6 +80,8 @@ def _parse_args(argv: list[str]) -> Args:
7780
arg = argv[index]
7881
if arg == "--json":
7982
json_mode = True
83+
elif arg == "--require-spec":
84+
require_spec = True
8085
elif arg == "--require-tasks":
8186
require_tasks = True
8287
elif arg == "--include-tasks":
@@ -105,6 +110,7 @@ def _parse_args(argv: list[str]) -> Args:
105110

106111
return Args(
107112
json_mode=json_mode,
113+
require_spec=require_spec,
108114
require_tasks=require_tasks,
109115
include_tasks=include_tasks,
110116
paths_only=paths_only,
@@ -230,6 +236,14 @@ def main(argv: list[str] | None = None) -> int:
230236
)
231237
return 1
232238

239+
if args.require_spec and not paths.feature_spec.is_file():
240+
print(f"ERROR: spec.md not found in {paths.feature_dir}", file=sys.stderr)
241+
print(
242+
f"Run {format_speckit_command('specify', paths.repo_root)} first to create the feature specification.",
243+
file=sys.stderr,
244+
)
245+
return 1
246+
233247
if args.require_tasks and not paths.tasks.is_file():
234248
print(f"ERROR: tasks.md not found in {paths.feature_dir}", file=sys.stderr)
235249
print(

templates/commands/analyze.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
22
description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation.
33
scripts:
4-
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
5-
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
6-
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
4+
sh: scripts/bash/check-prerequisites.sh --json --require-spec --require-tasks --include-tasks
5+
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireSpec -RequireTasks -IncludeTasks
6+
py: scripts/python/check_prerequisites.py --json --require-spec --require-tasks --include-tasks
77
---
88

99
## User Input

templates/commands/converge.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
22
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.
33
scripts:
4-
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
5-
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
6-
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
4+
sh: scripts/bash/check-prerequisites.sh --json --require-spec --require-tasks --include-tasks
5+
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireSpec -RequireTasks -IncludeTasks
6+
py: scripts/python/check_prerequisites.py --json --require-spec --require-tasks --include-tasks
77
---
88

99
## User Input

tests/test_check_prerequisites_python_parity.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,78 @@ def test_python_json_output_matches_bash(prereq_repo: Path, args: tuple[str, ...
247247
assert _json_stdout(py) == _json_stdout(bash)
248248

249249

250+
@requires_bash
251+
def test_python_require_spec_matches_bash(prereq_repo: Path) -> None:
252+
feat = prereq_repo / "specs" / "001-my-feature"
253+
feat.mkdir(parents=True)
254+
(feat / "plan.md").write_text("# plan\n", encoding="utf-8")
255+
(feat / "tasks.md").write_text("# tasks\n", encoding="utf-8")
256+
_write_feature_json(prereq_repo)
257+
258+
# spec.md is missing, and without the flag that stays the caller's problem
259+
bash_without = _run(_bash_cmd(prereq_repo, "--json", "--require-tasks"), prereq_repo)
260+
py_without = _run(_py_cmd(prereq_repo, "--json", "--require-tasks"), prereq_repo)
261+
assert py_without.returncode == bash_without.returncode == 0
262+
263+
# with the flag both variants fail the same way and name the same command
264+
bash_missing = _run(
265+
_bash_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo
266+
)
267+
py_missing = _run(
268+
_py_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo
269+
)
270+
assert py_missing.returncode == bash_missing.returncode == 1
271+
assert py_missing.stderr == bash_missing.stderr
272+
assert "spec.md not found" in bash_missing.stderr
273+
274+
# and once the spec exists the flag is satisfied
275+
(feat / "spec.md").write_text("# spec\n", encoding="utf-8")
276+
bash_present = _run(
277+
_bash_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo
278+
)
279+
py_present = _run(
280+
_py_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo
281+
)
282+
assert py_present.returncode == bash_present.returncode == 0
283+
assert _json_stdout(py_present) == _json_stdout(bash_present)
284+
285+
286+
@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
287+
def test_powershell_require_spec_matches_python(prereq_repo: Path) -> None:
288+
feat = prereq_repo / "specs" / "001-my-feature"
289+
feat.mkdir(parents=True)
290+
(feat / "plan.md").write_text("# plan\n", encoding="utf-8")
291+
(feat / "tasks.md").write_text("# tasks\n", encoding="utf-8")
292+
_write_feature_json(prereq_repo)
293+
294+
# spec.md is missing, and without the flag that stays the caller's problem
295+
ps_without = _run(_ps_cmd(prereq_repo, "-Json", "-RequireTasks"), prereq_repo)
296+
py_without = _run(_py_cmd(prereq_repo, "--json", "--require-tasks"), prereq_repo)
297+
assert ps_without.returncode == py_without.returncode == 0
298+
299+
# with the flag both variants fail the same way and name the same file
300+
ps_missing = _run(
301+
_ps_cmd(prereq_repo, "-Json", "-RequireSpec", "-RequireTasks"), prereq_repo
302+
)
303+
py_missing = _run(
304+
_py_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo
305+
)
306+
assert ps_missing.returncode == py_missing.returncode == 1
307+
assert "spec.md not found" in ps_missing.stderr
308+
assert "spec.md not found" in py_missing.stderr
309+
310+
# and once the spec exists the flag is satisfied and the payloads agree
311+
(feat / "spec.md").write_text("# spec\n", encoding="utf-8")
312+
ps_present = _run(
313+
_ps_cmd(prereq_repo, "-Json", "-RequireSpec", "-RequireTasks"), prereq_repo
314+
)
315+
py_present = _run(
316+
_py_cmd(prereq_repo, "--json", "--require-spec", "--require-tasks"), prereq_repo
317+
)
318+
assert ps_present.returncode == py_present.returncode == 0
319+
assert _json_stdout(ps_present) == _json_stdout(py_present)
320+
321+
250322
@requires_bash
251323
def test_python_text_output_matches_bash(prereq_repo: Path) -> None:
252324
feat = prereq_repo / "specs" / "001-my-feature"

0 commit comments

Comments
 (0)