Skip to content

fix(taskstoissues): scope issue dedup to the feature the tasks belong to (#4271) - #4314

Open
ntdatt812 wants to merge 1 commit into
github:mainfrom
ntdatt812:fix/4271-taskstoissues-feature-scope
Open

fix(taskstoissues): scope issue dedup to the feature the tasks belong to (#4271)#4314
ntdatt812 wants to merge 1 commit into
github:mainfrom
ntdatt812:fix/4271-taskstoissues-feature-scope

Conversation

@ntdatt812

Copy link
Copy Markdown
Contributor

Closes #4271.

Task IDs are local to a feature — every tasks.md restarts at T001 — but the dedup step matched existing issues on the bare ID:

For each issue title, match it against the task ID pattern \bT\d{3,}\b … and, when it matches one of your task IDs, mark that ID as already having an issue.

So once 001-auth had an issue titled T001: …, running the command for 002-billing saw "T001 exists" and skipped it. The issue was never created and the report said T001 already has an issue, skipping, which reads like success. Every feature after the first loses its low-numbered tasks, and it degrades with repo age rather than showing up on the first run.

This is the inverse of #2968: tightening the matching to stop duplicates made it aggressive enough to suppress valid tasks.

What changed

The canonical title now carries the feature. FEATURE_DIR is already parsed in step 1, so its basename — the NNN-name spec directory — goes in front:

[002-billing] T001: Create project structure

The ID keeps its own word boundaries inside that prefix, so the \bT\d{3,}\b matching #2968 established still works unchanged, including the four-digit case.

A task is skipped only when both match. Feature identity and task ID. A T001 belonging to another feature is a different task and no longer suppresses this one.

Existing issues still count. Anything filed before the prefix existed carries a bare T001: …. Those are treated as matching only when no [<feature>]-prefixed issue for that ID is present in the fetched set, so upgrading does not re-create work that is already tracked — which would have been the obvious way to turn this fix into a fresh duplicate problem.

On the choice of title over label

The issue offers "title or a label". Title, for two reasons: the dedup already reads titles from list_issues, so nothing new is fetched and no extra call is added to a step that is explicitly written to bound its pagination; and a label needs create-label permission and a repo-side convention, which is a heavier requirement for a command whose whole contract is "create issues on the remote". A label would be the better choice if the identifier had to be filterable in the GitHub UI — happy to switch if you would rather have that.

I did not implement the second bullet of the issue (re-checking immediately before creation so two concurrent invocations cannot race). It is a real gap, but it is a different change — the fetch-then-create window — and it is worth its own PR rather than being folded in here where it would be untested.

Tests

tests/unit/test_taskstoissues_feature_scope.py, three cases over the template text:

  • the canonical title rule names [<feature>] T001: and says where <feature> comes from
  • the skip rule requires both the feature and the ID
  • the pre-existing-issue clause is still present, so an upgrade cannot start duplicating

Each assertion fails with the offending instruction line quoted, and _line_containing raises if the instruction disappears entirely — so deleting the step cannot make the suite pass vacuously.

Mutation-checked — dropping [<feature>] from the canonical title fails exactly one case:

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

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

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

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

Legacy-title handling still causes cross-feature skips, and the template contains malformed regex control characters.

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

Pull request overview

Scopes task-to-issue deduplication by feature to prevent cross-feature task suppression.

Changes:

  • Adds feature-qualified issue titles and deduplication rules.
  • Adds regression tests for template instructions.
File summaries
File Description
templates/commands/taskstoissues.md Defines feature-scoped titles and matching.
tests/unit/test_taskstoissues_feature_scope.py Verifies feature-scoping instructions.
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.

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 `T\d{3,}` 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. Treat those as matching only when no `[<feature>]` prefix is present anywhere in the fetched set for that ID, so an upgrade does not re-create issues that are already tracked.
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 `T\d{3,}` matching above is unchanged by the prefix.

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address Copilot feedback. Note that the change should backwards compatible as it will impact anyone using this feature already

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-taskstoissues dedup matches bare task IDs across features — valid tasks are silently skipped

3 participants