Skip to content

fix(commands): task bookkeeping in implement, taskstoissues and converge - #4520

Open
ntdatt812 wants to merge 6 commits into
github:mainfrom
ntdatt812:fix/task-bookkeeping-commands
Open

fix(commands): task bookkeeping in implement, taskstoissues and converge#4520
ntdatt812 wants to merge 6 commits into
github:mainfrom
ntdatt812:fix/task-bookkeeping-commands

Conversation

@ntdatt812

Copy link
Copy Markdown
Contributor

Consolidates #4313, #4314 and #4330 into one pull request, as asked on #4460 (author-over-cap). Those three will be closed in favour of this one. Each also had review feedback outstanding; that is addressed here, and noted per section below.

The three share one subject: how the command templates read task bookkeeping in tasks.md and in the issue tracker. They stay separable. Each fix is its own run of commits, and each run was cherry-picked alone onto main and passes its own tests there, so any one can be dropped without touching the others.

Closes #4269, closes #4271, closes #4272.

1. /speckit-implement counts checkbox markers inside code fences (#4272), from #4313

implement.md defined its total, checked and unchecked counts on every - [ ] / - [x] line, so a checklist that documents the checkbox format in a fenced example reported items nobody can tick. A non-zero unchecked count stops implementation. /speckit-clarify already scoped its scan to markers outside code fences; implement now does the same.

tests/unit/test_checklist_scan_contract.py asserts that every line in a command template that defines a checkbox-marker scan also excludes code fences.

Review feedback: Copilot pointed out that the test only recognised a definition whose first marker is unchecked, so the "Checked items: Lines matching - [X]" definition was never parametrised; removing its exclusion left the suite green. Measured before changing it: with the old pattern that removal passes 4/4. The pattern now accepts a checked or unchecked first marker, four definitions are guarded instead of three, and the same removal fails.

2. /speckit-taskstoissues dedup matches bare task IDs across features (#4271), from #4314

Task IDs restart at T001 in every feature's tasks.md, and dedup matched existing issues on the ID alone. So the first feature to reach the tracker permanently suppressed T001 for every later feature, and those tasks were silently never created. Issue titles now carry the feature ([002-billing] T001: ..., from the FEATURE_DIR basename), and a task is skipped only when an issue matches both the feature and the ID.

Review feedback, and backward compatibility: Copilot found that the upgrade rule brought the bug straight back. It treated a bare legacy T001: ... title as this feature's whenever no scoped title existed for that ID, and that is the state of every feature on its first run after upgrading: a bare T001 filed for 001-auth still suppressed 002-billing's T001.

A bare title names no feature, and neither does the absence of a scoped one, so nothing in the tracker can settle which feature it belongs to. Deciding either way breaks someone: skipping drops a task silently, and creating duplicates one an existing user already tracks. That second case is the compatibility concern raised on #4314. So the command now lists every ID that matched only a bare title, each with the issue and this feature's description for the task, and asks before creating anything. Confirmed issues are skipped. It then offers to retitle them to the scoped form, and does so only for the ones the user agrees to, so the question does not come back on later runs. Existing users therefore get no duplicates and no silent gaps; the cost is one question per legacy issue, once.

The U+0008 bytes Copilot flagged on the earlier commit were already fixed in that PR (\b written as two characters); tests/integrations/test_integration_goose.py, which caught them, passes.

3. /speckit-converge is not idempotent (#4269), from #4330

#4330 dropped findings that an unchecked task already covered. Review there identified the root cause: converge never enforced its prerequisite. Run while tasks.md still had open tasks, it assessed the code anyway, found that unbuilt work as new gaps and appended it again under fresh IDs. And a run whose findings were all already tracked ended in converged, whose report says the implementation satisfies the spec while tracked work is still open.

Reworked around the lifecycle instead of the dedup rule:

  • Step 1 enforces the prerequisite. If tasks.md has any unchecked task (outside code fences, the rule implement counts by), converge stops before assessing anything. It lists the open task IDs, points to /speckit-implement, and leaves tasks.md byte-for-byte unchanged. This is neither converged nor tasks_appended.
  • Every task is assessed once all are checked. Tasks join the intent inventory, so one marked done but not built is a finding traced to its ID.
  • Anything appended makes the next run stop until it is implemented, which is what makes a re-run safe. The dedup paragraph is removed, and converged is only reachable once the work is actually finished.
  • The handoff and docs/reference/agentic-sdd.md describe the gate.

tests/unit/test_converge_prerequisite.py pins the lifecycle: the gate sits in Step 1 and stops before Step 2, names implement, lists the open IDs, leaves tasks.md untouched, counts outside code fences, is not reported as converged, the inventory includes tasks, and the docs describe it. Copilot's other note on #4330, that the dedup test did not pin substance-based matching, no longer applies, since that rule is gone.

Evidence

on main on this branch
test_checklist_scan_contract.py 3 failed, 2 passed 5 passed
test_taskstoissues_feature_scope.py 5 failed 5 passed
test_converge_prerequisite.py 7 failed 7 passed
  • Each fix cherry-picked alone onto main: its own tests plus the goose integration tests pass (43, 43 and 45).
  • Mutations, each reverted afterwards:
    • implement: removing the checked-items exclusion fails 1.
    • taskstoissues: restoring the previous legacy rule fails 2; removing the question to the user fails 1.
    • converge: restoring fix(converge): do not re-append work an unchecked task already tracks (#4269) #4330's template fails 6; dropping the fence exclusion from the gate fails 2 (this one is also caught by the checkbox-scan contract from section 1); removing STOP fails 1; removing the task inventory fails 1.
  • uvx ruff@0.15.0 check src tests: clean. markdownlint reports the same number of findings on the four touched Markdown files as on main (all pre-existing, in untouched lines).
  • tests/unit and tests/integrations on Windows: the only failures are symlink-privilege errors (WinError 1314); the two whose names do not say so fail identically on a clean checkout of main.

AI disclosure

Per CONTRIBUTING: this pull request (code, tests, measurements and this description) was developed with Claude Code as a coding agent.

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 github#4272
The scan-instruction pattern only recognised a definition whose first
marker is unchecked, so implement.md's "Checked items: Lines matching
`- [X]`" line was never parametrised. Removing its code-fence exclusion
left the suite green. Match a checked or unchecked first marker: four
definitions are now guarded instead of three, and that removal fails.
Task IDs are local to a feature -- every tasks.md restarts at T001 -- but the
dedup matched existing issues on the bare ID. So once feature 001-auth had an
issue titled T001, running the command for 002-billing saw "T001 exists" and
skipped it. The task was never created and nothing said so, which is a silent
gap in exactly the multi-feature repos this command targets.

The canonical title now carries the feature directory basename, and a task is
skipped only when an existing issue matches both that identifier and the ID.
The ID keeps its own word boundaries inside the prefixed title, so the
\bT\d{3,}\b matching from github#2968 is unchanged.

Issues filed before the prefix existed carry a bare `T001: ...`; those are
still recognised for their own feature, so upgrading does not re-create work
that is already tracked.

Closes github#4271
The sentence added in the previous commit — "so the `\bT\d{3,}\b` matching
above is unchanged by the prefix" — reached the file with two literal U+0008
BACKSPACE bytes where `\b` was meant. My editing pipeline interpreted the
escape rather than passing it through.

A control character cannot appear in a YAML block scalar, so the generator
fell back to a double-quoted flow scalar for the whole prompt, and
`test_yaml_has_prompt` failed on `speckit.taskstoissues.yaml`:

    AssertionError: speckit.taskstoissues.yaml missing prompt block scalar

Reproduced and pinned locally: with the two bytes present the test fails with
that exact message, and with them written as `\b` the goose suite is 38/38.
The rendered sentence is unchanged — it was always meant to read `\b`.
The upgrade rule treated a bare `T001: ...` title as this feature's
whenever no scoped title existed for that ID. That is exactly the state
of every feature on its first run after upgrading, so a bare T001 filed
for 001-auth still suppressed 002-billing's T001: the github#4271 skip, back.

A bare title names no feature, and neither does the absence of a scoped
one, so no inference from the tracker can settle it. Skipping drops a
task silently; creating duplicates one an existing user already tracks.
The command now lists every ID that matched only a bare title, with the
issue and this feature's description for the task, and asks before
creating anything. Confirmed issues are skipped, and retitled to the
scoped form only if the user agrees, so the question does not recur.
…append them

Converge is meant to assess a finished implementation, and nothing
enforced that. Run while tasks.md still had open work, it assessed the
code anyway, found that unbuilt work as new gaps and appended it again
under fresh IDs, so every re-run duplicated its own remediation tasks
(github#4269). And a run that found nothing new reported `converged`, whose
report says the implementation satisfies the spec while tracked work is
still open.

Step 1 now checks for unchecked tasks (outside code fences, the rule
implement counts by) and stops before any assessment, listing them and
pointing to implement, with tasks.md untouched. Once every task is
checked, each task joins the intent inventory, so one marked done but
not built is a finding traced to its ID. Anything appended makes the
next run stop again until it is implemented. The handoff and
docs/reference/agentic-sdd.md describe the gate, and the tests pin the
lifecycle.

Closes github#4269
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review

Projects

None yet

2 participants