Skip to content

Commit 9b81246

Browse files
committed
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
1 parent f5d0422 commit 9b81246

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

‎templates/commands/implement.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,11 @@ You **MUST** consider the user input before proceeding (if not empty).
5858
- `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
5959
- For custom checklists, `[x]` means the reviewer determined the requirements-quality criterion is satisfied; it does NOT mean implementation work is complete
6060
- Scan all checklist files in the checklists/ directory
61+
- 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
6162
- For each checklist, count:
62-
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
63-
- Checked items: Lines matching `- [X]` or `- [x]`
64-
- Unchecked items: Lines matching `- [ ]`
63+
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]` outside code fences
64+
- Checked items: Lines matching `- [X]` or `- [x]` outside code fences
65+
- Unchecked items: Lines matching `- [ ]` outside code fences
6566
- Create a status table:
6667
6768
```text
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Every command that scans checkbox markers must say it skips code fences.
2+
3+
A checklist is free to *document* the checkbox format inside a fenced block. Counting
4+
those example markers reports items nobody can tick, and `/speckit-implement` treats a
5+
non-zero unchecked count as a reason to stop — so an example fence blocks implementation
6+
(#4272). `/speckit-clarify` already scoped its scan to markers outside code fences; this
7+
keeps the two commands from drifting apart again, and holds any future command that
8+
starts counting markers to the same rule.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import re
14+
from pathlib import Path
15+
16+
import pytest
17+
18+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
19+
COMMAND_DIRS = [
20+
PROJECT_ROOT / "templates" / "commands",
21+
*sorted((PROJECT_ROOT / "presets").glob("*/commands")),
22+
]
23+
24+
# The instruction that tells the agent which lines are checkbox markers. Written to catch
25+
# the phrasing both commands use rather than one exact sentence.
26+
SCAN_INSTRUCTION = re.compile(r"lines matching\s+`- \[ \]`", re.IGNORECASE)
27+
FENCE_EXCLUSION = re.compile(r"outside\s+(?:of\s+)?code\s+fences", re.IGNORECASE)
28+
29+
30+
def scan_instructions() -> list[tuple[Path, int, str]]:
31+
"""Every line in a command template that defines what counts as a checkbox marker."""
32+
found: list[tuple[Path, int, str]] = []
33+
for directory in COMMAND_DIRS:
34+
if not directory.is_dir():
35+
continue
36+
for path in sorted(directory.glob("*.md")):
37+
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
38+
if SCAN_INSTRUCTION.search(line):
39+
found.append((path, number, line))
40+
return found
41+
42+
43+
def test_the_contract_is_actually_stated_somewhere() -> None:
44+
"""Guard against the regex silently matching nothing and the test passing vacuously."""
45+
assert scan_instructions(), "no command template defines a checkbox-marker scan any more"
46+
47+
48+
@pytest.mark.parametrize(
49+
("path", "number", "line"),
50+
scan_instructions(),
51+
ids=lambda value: value.name if isinstance(value, Path) else str(value),
52+
)
53+
def test_marker_scans_exclude_code_fences(path: Path, number: int, line: str) -> None:
54+
assert FENCE_EXCLUSION.search(line), (
55+
f"{path.relative_to(PROJECT_ROOT)}:{number} tells the agent to match checkbox "
56+
f"markers without excluding fenced code blocks, so an example fence is counted "
57+
f"as real work:\n {line.strip()}"
58+
)

0 commit comments

Comments
 (0)