Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/reference/agentic-sdd.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Verify each stage works before moving to the next.

## `/speckit.converge`

Assesses the codebase against the feature's spec, plan, and tasks to confirm nothing was missed. It is **append-only**: it never edits or deletes code, and its only possible write is adding tasks to `tasks.md`. Run it only after `/speckit.implement` has run on the current `tasks.md`.
Assesses the codebase against the feature's spec, plan, and tasks to confirm nothing was missed. It is **append-only**: it never edits or deletes code, and its only possible write is adding tasks to `tasks.md`. Run it only after `/speckit.implement` has completed every task in `tasks.md`: if any task is still unchecked, converge stops before assessing anything, lists the unchecked tasks, and tells you to run `/speckit.implement` first, leaving `tasks.md` unchanged.

```text
/speckit.converge
Expand All @@ -120,4 +120,4 @@ Assesses the codebase against the feature's spec, plan, and tasks to confirm not
It first prints a severity-graded findings summary, then resolves to one of two outcomes:

- **Converged** — no gaps found. `tasks.md` is left byte-for-byte unchanged and you'll see a clean result like `✅ Converged — the implementation satisfies the spec, plan, and tasks.` You're done; proceed to review or open a PR.
- **Tasks appended** — gaps found. Converge appends them as new tasks under a Convergence section in `tasks.md` and tells you how many. Run `/speckit.implement` again to complete them, then `/speckit.converge` once more. Each pass finds fewer items; repeat until it reports converged.
- **Tasks appended** — gaps found. Converge appends them as new tasks under a Convergence section in `tasks.md` and tells you how many. Run `/speckit.implement` again to complete them, then `/speckit.converge` once more (until they are checked off, converge stops at its prerequisite check rather than appending them a second time). Each pass finds fewer items; repeat until it reports converged.
25 changes: 21 additions & 4 deletions templates/commands/converge.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ state of the code, determine which requirements, acceptance criteria, plan decis
existing tasks are unmet, incomplete, or only partially satisfied, and **append each piece
of remaining work as a new, traceable task** at the bottom of `tasks.md` so that
`__SPECKIT_COMMAND_IMPLEMENT__` can complete it. This command MUST run only after
`__SPECKIT_COMMAND_IMPLEMENT__` has run on the current `tasks.md`, and after `__SPECKIT_COMMAND_TASKS__` has produced a complete `tasks.md`.
`__SPECKIT_COMMAND_IMPLEMENT__` has completed every task in the current `tasks.md` (Step 1 checks, and stops if any task is unchecked), and after `__SPECKIT_COMMAND_TASKS__` has produced a complete `tasks.md`.

This is **not** a diff tool and does **not** track changes. It assesses the present state
of the code relative to the feature's artifacts — no git, no branch comparison, no history.
Expand Down Expand Up @@ -100,6 +100,17 @@ Run `{SCRIPT}` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_
If `spec.md`, `plan.md`, or `tasks.md` is missing, STOP with a clear, actionable message naming the
prerequisite command to run (`__SPECKIT_COMMAND_SPECIFY__` for a missing spec, `__SPECKIT_COMMAND_PLAN__` for a missing plan,
`__SPECKIT_COMMAND_TASKS__` for missing tasks). Do not produce partial output.

**Enforce the implement prerequisite before assessing anything.** Scan `tasks.md` for
unchecked tasks — lines matching `- [ ]` outside code fences, the rule `__SPECKIT_COMMAND_IMPLEMENT__` counts by —
and if there are any, STOP: report how many are unchecked and list their task IDs, and tell
the user to run `__SPECKIT_COMMAND_IMPLEMENT__` to complete them before converging. Leave
`tasks.md` byte-for-byte unchanged, report no findings, and do not continue to Step 2. This
is neither outcome of Step 7: an unchecked task is work that is already tracked but not yet
built, so assessing the code while one is open would report that same work again as a new
gap, and a run that reached `converged` would claim the implementation is complete while
tracked work remains. Converge assesses a finished implementation; it does not re-plan an
unfinished one.
For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").

### 2. Load Artifacts (Progressive Disclosure)
Expand Down Expand Up @@ -137,6 +148,10 @@ Create an internal model (do not echo raw artifacts):
- **Requirements inventory**: one stable key per FR-### / SC-### / user-story acceptance
scenario (e.g. `US1/AC2`), plus the plan decisions and constitution principles that
impose buildable obligations.
- **Task inventory**: every task in `tasks.md` — all of them checked by this point — with
the work it describes and the file paths it names. A task marked done whose work is
absent from the code, or only partly there, is a finding like any other, traced to
that task's ID.
- **Code-scope map**: from the file paths named in `plan.md` and `tasks.md`, plus a keyword
search for the concepts each requirement describes, derive the set of source files and
components in scope for assessment. Bound the assessment to these — do **not** infer
Expand Down Expand Up @@ -210,7 +225,8 @@ Append to the **end** of `tasks.md`, per the append contract:
```

`<source-ref>` traces the task to its origin: e.g. `FR-003`, `SC-002`,
`US1/AC2`, `plan: storage decision`, `Constitution II`.
`US1/AC2`, `plan: storage decision`, `Constitution II`, or a task ID such as `T017`
when a task marked done is not reflected in the code.

`<gap-type>` is one of `missing`, `partial`, `contradicts`, `unrequested`.

Expand All @@ -228,8 +244,9 @@ Append to the **end** of `tasks.md`, per the append contract:
### 8. Provide Next Actions (Handoff)

- On `tasks_appended`: state how many tasks were appended under which phase, and recommend
running `__SPECKIT_COMMAND_IMPLEMENT__` to complete them; note that a follow-up converge
run will find fewer or no remaining items.
running `__SPECKIT_COMMAND_IMPLEMENT__` to complete them; note that converge will stop at
its prerequisite check until those tasks are checked off, and re-assess everything once
they are.
- On `converged`: recommend proceeding to review / opening a PR. No further implement pass
is needed for this feature's specified scope.

Expand Down
7 changes: 4 additions & 3 deletions templates/commands/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions templates/commands/taskstoissues.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ git config --get remote.origin.url
> [!CAUTION]
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL

1. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by **at least** three digits, e.g. `T001` — `__SPECKIT_COMMAND_CONVERGE__` assigns new IDs with `T{M+1:03d}`, which is a floor rather than a cap, so once a file has more than 999 tasks the IDs are four digits or longer). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3,}\b` (the `{3,}` accepts four-digit and longer IDs — with `\d{3}` a title containing `T1000` would not match at all, because the trailing `\b` cannot fall between two digits, so that task would be silently neither deduplicated nor created; word boundaries still stop a token like `ST001` from matching, and force the whole digit run to be consumed so `T100` can never match inside `T1000`; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked.
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote. Task lines in `tasks.md` start with a markdown checkbox, so first strip the leading `- [ ]` (and any `[P]` / `[US#]` markers) to recover the task ID and its description. Create the issue with a single canonical title of the form `T001: <description>`, with the ID written once followed by the task description (for example, the line `- [ ] T001 Create project structure` becomes the title `T001: Create project structure`).
- **Skip** any task whose ID is already present in the set of existing issues from the previous step, and report it (for example, `T001 already has an issue, skipping`).
1. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by **at least** three digits, e.g. `T001` — `__SPECKIT_COMMAND_CONVERGE__` assigns new IDs with `T{M+1:03d}`, which is a floor rather than a cap, so once a file has more than 999 tasks the IDs are four digits or longer). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3,}\b` (the `{3,}` accepts four-digit and longer IDs — with `\d{3}` a title containing `T1000` would not match at all, because the trailing `\b` cannot fall between two digits, so that task would be silently neither deduplicated nor created; word boundaries still stop a token like `ST001` from matching, and force the whole digit run to be consumed so `T100` can never match inside `T1000`; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue **only if the title also carries this feature's identifier** (see below). Task IDs restart at `T001` in every feature's `tasks.md`, so an unscoped match means the first feature to reach the tracker permanently suppresses `T001` for every later feature -- a silent gap in exactly the multi-feature repos this command is for. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked.
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote. Task lines in `tasks.md` start with a markdown checkbox, so first strip the leading `- [ ]` (and any `[P]` / `[US#]` markers) to recover the task ID and its description. Create the issue with a single canonical title of the form `[<feature>] T001: <description>`, where `<feature>` is the basename of FEATURE_DIR parsed in step 1 (the `NNN-name` spec directory, e.g. `002-billing`), followed by the ID written once and then the task description (for example, the line `- [ ] T001 Create project structure` in feature `002-billing` becomes the title `[002-billing] T001: Create project structure`). The ID keeps its own word boundaries, so the `\bT\d{3,}\b` matching above is unchanged by the prefix.
- **Skip** a task only when an existing issue matches **both** this feature's identifier and the task ID, and report it (for example, `[002-billing] T001 already has an issue, skipping`). A `T001` belonging to another feature is a different task and must not suppress this one.
- Issues created before this scoping exists carry a bare `T001: ...` title, and a bare title carries no feature identity: it cannot show which feature's `T001` it tracks, and the absence of a scoped title for that ID does not show it either. So never decide a bare match on your own, in either direction -- skipping silently drops this feature's task when the issue belongs to another feature, and creating duplicates it when the issue is this feature's. Before creating anything, list every task ID that matched only a bare title, each with the issue number, the issue title and this feature's description for that task, and ask the user which of those issues already track this feature's tasks. Skip the ones the user confirms and create the rest with the scoped title. Then offer to retitle each confirmed issue to the scoped form (`[<feature>] T001: ...`) so later runs match it without asking, and retitle only the issues the user agrees to.
- Only create issues for tasks that do not yet have a matching issue.

> [!CAUTION]
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/test_checklist_scan_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""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. The first marker may be
# checked or unchecked: `/speckit-implement` defines its checked count on `- [X]` alone,
# and that definition needs the same exclusion as the other two.
SCAN_INSTRUCTION = re.compile(r"lines matching\s+`- \[[ xX]\]`", 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()}"
)
Loading