From 9b812464c451eefc9630f976861449f9cc82171a Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Tue, 25 Aug 2026 19:25:19 +0700 Subject: [PATCH] fix(implement): count checkbox markers outside code fences only The checklist gate counted every `- [ ]` / `- [x]` line in every checklist file, fenced blocks included. A checklist that documents the checkbox format with an example fence therefore reported unchecked items nobody can ever tick, and /speckit-implement stops on a non-zero unchecked count -- so writing down the format blocked implementation. /speckit-clarify already scopes its scan to markers outside code fences, so this was also the two commands disagreeing about what a checklist item is. They now state the same rule. Closes #4272 --- templates/commands/implement.md | 7 +-- tests/unit/test_checklist_scan_contract.py | 58 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_checklist_scan_contract.py diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 742c45e185..c61b1e6ee4 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -58,10 +58,11 @@ You **MUST** consider the user input before proceeding (if not empty). - `checklists/requirements.md` is the built-in spec-quality checklist maintained by `__SPECKIT_COMMAND_SPECIFY__` and `__SPECKIT_COMMAND_CLARIFY__`; custom checklists generated by `__SPECKIT_COMMAND_CHECKLIST__` are reviewer-owned requirements-quality review artifacts - For custom checklists, `[x]` means the reviewer determined the requirements-quality criterion is satisfied; it does NOT mean implementation work is complete - Scan all checklist files in the checklists/ directory + - Count only checkbox lines **outside of code fences**, the same rule `__SPECKIT_COMMAND_CLARIFY__` applies. A checklist that documents the checkbox format inside a fence is showing an example, not tracking work, and counting those examples blocks implementation on items nobody can ever tick - For each checklist, count: - - Total items: All lines matching `- [ ]` or `- [X]` or `- [x]` - - Checked items: Lines matching `- [X]` or `- [x]` - - Unchecked items: Lines matching `- [ ]` + - Total items: All lines matching `- [ ]` or `- [X]` or `- [x]` outside code fences + - Checked items: Lines matching `- [X]` or `- [x]` outside code fences + - Unchecked items: Lines matching `- [ ]` outside code fences - Create a status table: ```text diff --git a/tests/unit/test_checklist_scan_contract.py b/tests/unit/test_checklist_scan_contract.py new file mode 100644 index 0000000000..3e961162df --- /dev/null +++ b/tests/unit/test_checklist_scan_contract.py @@ -0,0 +1,58 @@ +"""Every command that scans checkbox markers must say it skips code fences. + +A checklist is free to *document* the checkbox format inside a fenced block. Counting +those example markers reports items nobody can tick, and `/speckit-implement` treats a +non-zero unchecked count as a reason to stop — so an example fence blocks implementation +(#4272). `/speckit-clarify` already scoped its scan to markers outside code fences; this +keeps the two commands from drifting apart again, and holds any future command that +starts counting markers to the same rule. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +COMMAND_DIRS = [ + PROJECT_ROOT / "templates" / "commands", + *sorted((PROJECT_ROOT / "presets").glob("*/commands")), +] + +# The instruction that tells the agent which lines are checkbox markers. Written to catch +# the phrasing both commands use rather than one exact sentence. +SCAN_INSTRUCTION = re.compile(r"lines matching\s+`- \[ \]`", re.IGNORECASE) +FENCE_EXCLUSION = re.compile(r"outside\s+(?:of\s+)?code\s+fences", re.IGNORECASE) + + +def scan_instructions() -> list[tuple[Path, int, str]]: + """Every line in a command template that defines what counts as a checkbox marker.""" + found: list[tuple[Path, int, str]] = [] + for directory in COMMAND_DIRS: + if not directory.is_dir(): + continue + for path in sorted(directory.glob("*.md")): + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + if SCAN_INSTRUCTION.search(line): + found.append((path, number, line)) + return found + + +def test_the_contract_is_actually_stated_somewhere() -> None: + """Guard against the regex silently matching nothing and the test passing vacuously.""" + assert scan_instructions(), "no command template defines a checkbox-marker scan any more" + + +@pytest.mark.parametrize( + ("path", "number", "line"), + scan_instructions(), + ids=lambda value: value.name if isinstance(value, Path) else str(value), +) +def test_marker_scans_exclude_code_fences(path: Path, number: int, line: str) -> None: + assert FENCE_EXCLUSION.search(line), ( + f"{path.relative_to(PROJECT_ROOT)}:{number} tells the agent to match checkbox " + f"markers without excluding fenced code blocks, so an example fence is counted " + f"as real work:\n {line.strip()}" + )