Skip to content

fix(converge): do not re-append work an unchecked task already tracks (#4269) - #4330

Open
ntdatt812 wants to merge 1 commit into
github:mainfrom
ntdatt812:fix/4269-converge-idempotent
Open

fix(converge): do not re-append work an unchecked task already tracks (#4269)#4330
ntdatt812 wants to merge 1 commit into
github:mainfrom
ntdatt812:fix/4269-converge-idempotent

Conversation

@ntdatt812

Copy link
Copy Markdown
Contributor

Closes #4269.

Step 7 knew how to compute the next task ID and nothing about whether a finding was already represented:

1. Scan all existing task IDs; let `M` be the maximum. …
3. Emit one checklist item per actionable finding … assigning IDs `T{M+1:03d}, …`

One item per finding, unconditionally. So the two cases in the issue both land the same way: run converge twice and gaps found both times are appended twice; run it before /speckit-implement has worked the list and it appends remediation for work the plan already tracks. The part that makes it hard to undo is that the second run cannot tell its own previous output apart from the plan's original tasks — both are just unchecked lines by then — so the duplicates accumulate and one piece of work ends up split across two IDs.

What changed

Findings are matched against the existing unchecked tasks before anything is appended, and an already-covered finding is dropped rather than re-issued.

Three details that matter more than the rule itself:

  • Matched on substance, not wording. The comparison is on the work described and the file paths or <source-ref> named — converge writes its own descriptions, so a string match would never fire on its own previous output.
  • Scanned outside code fences, the rule /speckit-clarify already applies and the one [Bug]: /speckit-implement counts checkbox markers inside fenced code blocks — example checkboxes can falsely block implementation #4272 just brought /speckit-implement in line with. A checklist that documents the checkbox format is not tracked work.
  • Dropped findings are reported (F3 — already tracked by T017, not appended). A silently discarded finding is indistinguishable from one that was never found. And when every finding is already tracked, the run takes the converged path saying why — so the operator reads "the work is known", not "the codebase is complete". That distinction is the whole value of the run.

What I deliberately did not do

The issue's first bullet asks converge to verify a prerequisite implementation checkpoint. That needs a new piece of persisted state and a decision about what counts as "implement has run against this task list" — a design call for maintainers, not something to infer inside a dedup fix. This PR takes the second bullet only, which is self-contained and is what stops the duplicates.

Tests

tests/unit/test_converge_idempotency.py, four cases over the template:

  • the append step still exists — so the rest cannot pass vacuously if it is renamed away
  • findings are compared against existing unchecked tasks, and the marker - [ ] is named so the rule is executable
  • the scan excludes code fences
  • an already-tracked finding is reported, not silently dropped

Mutation-checked — weakening "unchecked task" to "existing tasks" and dropping the marker fails exactly one case, the comparison one, and leaves the other three green.

python -m pytest tests/unit/test_converge_idempotency.py -q
4 passed

python -m pytest tests/unit/ -q
470 passed, 2 skipped, 2 failed

The two failures are test_add_source_refuses_symlinked_specify_escape and test_save_records_refuses_symlinked_specify_escape, which fail identically on main on this machine — creating a symlink on Windows needs elevation. Measured on a clean checkout, not assumed.

The append step computed the next task ID but never asked whether a finding
was already represented in tasks.md. So converge run twice, or run before
implement had worked through the list, appended the same remediation work
again under fresh IDs -- and the second run cannot tell its own previous
output apart from the plan's original tasks, so traceability for one piece of
work splits across two entries.

Findings are now matched against existing unchecked tasks before anything is
appended, on the work described and the paths or source-ref it names rather
than on wording, and scanning outside code fences like the other commands do.
A finding that is already tracked is reported as such rather than silently
discarded, and an all-tracked run takes the converged path saying why -- so
the operator reads "the work is known", not "the codebase is complete".

Closes github#4269

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The all-findings-tracked branch incorrectly reports full convergence, and substantive matching lacks regression coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Prevents /speckit-converge from duplicating remediation work already represented by unchecked tasks.

Changes:

  • Adds substantive finding-to-task deduplication guidance.
  • Excludes fenced checkbox examples and reports skipped findings.
  • Adds template contract tests.
File summaries
File Description
templates/commands/converge.md Defines deduplication and reporting behavior.
tests/unit/test_converge_idempotency.py Tests the new template requirements.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +218 to +220
`F3 — already tracked by T017, not appended`. If **every** finding is already tracked,
there is nothing to append: take the `converged` path below and say why, so the operator
sees "the work is known" rather than "the codebase is complete".
Comment on lines +35 to +42
def test_findings_are_compared_against_existing_unchecked_tasks(template_text: str) -> None:
assert re.search(r"unchecked task", template_text, re.IGNORECASE), (
"converge no longer says to compare findings against existing unchecked tasks, so "
"a second run appends the same work again under new IDs"
)
assert re.search(r"`- \[ \]`", template_text), (
"the comparison should name the marker it scans for, so the rule is executable"
)
@mnriem

mnriem commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

This PR addresses the duplication symptom, but the root issue is that converge does not enforce its prerequisite. If tasks.md contains unchecked tasks, it should stop before analysis and tell the user to run implement.

Only after all tracked tasks are complete should converge assess the implementation against every SDD artifact, including each task. Any newly discovered work is then appended, making the next convergence attempt stop until that work is implemented.

Please adjust the PR around this lifecycle rather than routing already-tracked incomplete work through the converged outcome, which incorrectly reports that implementation is complete.

Posted on behalf of @mnriem by GitHub Copilot (model: GPT-5.6 Sol).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: /speckit-converge is not idempotent — re-runs append duplicate remediation tasks

3 participants