Skip to content

fix: prevent task number collisions when moving tasks between tags#1665

Open
withsivram wants to merge 2 commits intoeyaltoledano:mainfrom
withsivram:fix/tag-move-collision-issue-1647
Open

fix: prevent task number collisions when moving tasks between tags#1665
withsivram wants to merge 2 commits intoeyaltoledano:mainfrom
withsivram:fix/tag-move-collision-issue-1647

Conversation

@withsivram
Copy link
Copy Markdown

@withsivram withsivram commented Mar 20, 2026

Summary

  • When moving tasks between tags, if a task's ID already exists in the target tag, the task is now automatically renumbered to the next available ID instead of throwing a TASK_ALREADY_EXISTS error
  • Dependencies within the moved batch are updated to reflect new IDs (both task-level and subtask-level dependencies)
  • The response message and MCP tool output now include details about any renumbered tasks (e.g., 1 → 5)
  • A tip is provided suggesting task-master validate-dependencies when renumbering occurs

Test plan

  • Existing unit tests pass (47 cross-tag move tests)
  • Existing integration tests pass (updated ID collision test to verify auto-renumber behavior)
  • All 76 move-task related tests pass
  • Manual test: create tasks with same IDs in two different tags, move one to the other, verify renumbering
  • Manual test: move multiple tasks with dependencies where some get renumbered, verify dependency updates

Fixes #1647

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • Moving tasks between tags now auto-renumbers conflicting IDs instead of failing.
    • Success messages include renumbering details and a tip to validate dependencies.
    • Dependencies and subtask parent references are updated to preserve relationships (including dotted subtask references) after renumbering.
  • Tests

    • Integration tests updated/added to verify automatic renumbering and dependency remapping.

…ixes eyaltoledano#1647)

When moving tasks between tags, if a task's ID already exists in the
target tag, the task is now automatically renumbered to the next
available ID instead of throwing an error. Dependencies within the
moved batch are updated to reflect the new IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 20, 2026 16:48
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 20, 2026

⚠️ No Changeset found

Latest commit: 7ebc499

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 54c46306-c50a-4655-b14c-cf515c95c850

📥 Commits

Reviewing files that changed from the base of the PR and between 5f88b8c and 7ebc499.

📒 Files selected for processing (3)
  • mcp-server/src/core/direct-functions/move-task-cross-tag.js
  • scripts/modules/task-manager/move-task.js
  • tests/integration/move-task-cross-tag.integration.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcp-server/src/core/direct-functions/move-task-cross-tag.js

📝 Walkthrough

Walkthrough

Automatic renumbering was implemented for tasks moved between tags when destination ID collisions occur. The move operation now assigns new IDs, tracks original→new mappings, updates parent/subtask dependencies accordingly, and returns success messages that include renumbering details.

Changes

Cohort / File(s) Summary
Core Move Operation Logic
scripts/modules/task-manager/move-task.js
Added getNextAvailableId() and idRemapping logic; auto-assigns new IDs on collisions, updates moved tasks' id, subtasks[].parentTaskId, and rewrites dependencies (preserving dotted subtask suffixes); records originalId/newId in movedTasks and adjusts logging.
Cross-Tag Move Response
mcp-server/src/core/direct-functions/move-task-cross-tag.js
Changed success payload construction to report total moved tasks from result.movedTasks.length and append a renumbering details suffix listing originalId → newId pairs when remapping occurred.
Integration Tests
tests/integration/move-task-cross-tag.integration.test.js
Replaced failure expectation with successful auto-renumbering assertions; added checks for movedTasks remapping entries, updated fixtures to include dependencies: [], and added a test for dependency remapping including dotted subtask references and parentTaskId updates.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant API as Move-Task API
    participant MoveOp as executeMoveOperation
    participant IdMgr as getNextAvailableId
    participant DepMgr as Dependency Updater
    participant Storage as Persistence (writeJSON)

    User->>API: Request move tasks between tags
    API->>MoveOp: start move batch
    MoveOp->>MoveOp: detect ID collision(s)
    MoveOp->>IdMgr: request next available ID(s)
    IdMgr-->>MoveOp: return newId(s)
    MoveOp->>MoveOp: record idRemapping (originalId → newId)
    MoveOp->>DepMgr: rewrite dependencies and subtask parent IDs
    DepMgr-->>MoveOp: updated tasks with remapped references
    MoveOp->>Storage: persist updated source/target tag data
    Storage-->>API: persist success
    API-->>User: respond with success message and renumbering details
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #1088: Implements foundational renumbering and ID remapping utilities used/extended here.
  • PR #1135: Previously modified cross-tag collision handling toward error/suggestion UX (alternative approach to the same collision problem).

Suggested reviewers

  • Crunchyman-ralph
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main change: preventing task collisions by automatic renumbering when moving tasks between tags.
Linked Issues check ✅ Passed The PR successfully implements all core coding requirements from #1647: automatic renumbering of colliding task IDs, dependency updates including dotted subtask references, parentTaskId updates, and improved messaging.
Out of Scope Changes check ✅ Passed All changes are focused on the move-task collision handling logic, including implementation, tests, and related messaging updates with no unrelated modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can enable review details to help with troubleshooting, context usage and more.

Enable the reviews.review_details setting to include review details such as the model used, the time taken for each step and more in the review comments.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates cross-tag task moves to automatically renumber tasks when the destination tag already contains the same task ID, and surfaces renumbering details in both CLI/core responses and the MCP direct-function output.

Changes:

  • Auto-renumber tasks on cross-tag ID collisions instead of throwing TASK_ALREADY_EXISTS.
  • Attempt to remap dependencies within the moved batch to reflect any new IDs (including subtasks).
  • Update integration coverage for the ID-collision scenario and enrich responses with renumbering details + a dependency validation tip.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
tests/integration/move-task-cross-tag.integration.test.js Updates the cross-tag collision test to expect successful move + renumber metadata.
scripts/modules/task-manager/move-task.js Implements auto-renumbering during cross-tag moves, adds renumber info to responses, and adds dependency remapping logic.
mcp-server/src/core/direct-functions/move-task-cross-tag.js Adds renumbering details to the MCP direct-function success message.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +955 to +965
if (movedTask && Array.isArray(movedTask.dependencies)) {
movedTask.dependencies = movedTask.dependencies.map((dep) => {
const normalizedDep = normalizeDependency(dep);
if (
Number.isFinite(normalizedDep) &&
idRemapping.has(normalizedDep)
) {
return idRemapping.get(normalizedDep);
}
return dep;
});
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The dependency remapping logic uses normalizeDependency(dep) and, when a remap exists, replaces the original dependency value with the new parent task ID. For dotted dependencies like "1.2" this drops the subtask portion, and for string dependencies like "2" it changes the type to number. This will corrupt subtask dependency references and can break expectations that string deps remain strings. Update the remap to preserve dotted suffixes (e.g., "oldParent.sub" -> "newParent.sub") and preserve the original value type where possible (string in -> string out).

Copilot uses AI. Check for mistakes.
Comment on lines +968 to +981
if (movedTask && Array.isArray(movedTask.subtasks)) {
movedTask.subtasks.forEach((subtask) => {
if (Array.isArray(subtask.dependencies)) {
subtask.dependencies = subtask.dependencies.map((dep) => {
const normalizedDep = normalizeDependency(dep);
if (
Number.isFinite(normalizedDep) &&
idRemapping.has(normalizedDep)
) {
return idRemapping.get(normalizedDep);
}
return dep;
});
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

This same remapping pattern is applied to subtask.dependencies and has the same issue: dotted dependency strings (e.g., "2.1") will be converted to just the remapped parent ID, losing the ".1" portion, and string deps can become numbers. Adjust this block to preserve dotted subtask references and the original value type when rewriting dependencies.

Copilot uses AI. Check for mistakes.
Comment on lines +895 to +906
// Apply the new ID to the task
taskToMove.id = assignedId;

// Update internal subtask dependency references that pointed to the old parent ID
if (Array.isArray(taskToMove.subtasks)) {
taskToMove.subtasks.forEach((subtask) => {
if (Array.isArray(subtask.dependencies)) {
subtask.dependencies = subtask.dependencies.map((dep) => {
if (typeof dep === 'string' && dep.includes('.')) {
const [depParent, depSub] = dep.split('.');
if (parseInt(depParent, 10) === normalizedTaskId) {
return `${assignedId}.${depSub}`;
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

When a task is renumbered (taskToMove.id = assignedId), any nested subtasks that carry a parentTaskId field (created by add-subtask/convert flows) will still point at the old parent ID. That makes the data internally inconsistent and can break logic that relies on parentTaskId (e.g., isTaskDependentOn). If subtasks have parentTaskId, update it to assignedId when it matches the old normalizedTaskId.

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +130
let message = `Successfully moved ${sourceIds.length} task(s) from "${args.sourceTag}" to "${args.targetTag}"`;
if (renumberedTasks.length > 0) {
const renumberDetails = renumberedTasks
.map((t) => `${t.originalId} → ${t.newId}`)
.join(', ');
message += `. Renumbered to avoid ID collisions: ${renumberDetails}`;
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The success message is built using sourceIds.length, but moveTasksBetweenTags can move additional tasks when withDependencies is true (and result.message / result.movedTasks reflect the actual moved count). Consider basing this message on result.movedTasks.length (or reusing result.message) so MCP output stays accurate for dependency-inclusive moves.

Copilot uses AI. Check for mistakes.
Comment on lines +593 to +607
// Task should be moved successfully with a new ID
expect(result.message).toContain('Successfully moved 1 tasks');
expect(result.message).toContain('Renumbered');
expect(result.movedTasks).toHaveLength(1);
expect(result.movedTasks[0].originalId).toBe(1);
expect(result.movedTasks[0].newId).toBe(2); // Next available ID after existing ID 1

// Verify the target tag now has both tasks
expect(mockUtils.writeJSON).toHaveBeenCalled();
const writtenData = mockUtils.writeJSON.mock.calls[0][1];
expect(writtenData['in-progress'].tasks).toHaveLength(2);
expect(writtenData['in-progress'].tasks[0].id).toBe(1); // Original task
expect(writtenData['in-progress'].tasks[1].id).toBe(2); // Renumbered moved task
expect(writtenData['in-progress'].tasks[1].title).toBe('Backlog Task');

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

This integration test validates auto-renumbering, but it doesn't exercise the new behavior of updating task/subtask dependencies within the moved batch when IDs are remapped. To prevent regressions (especially for dotted dependencies like "1.2"), consider extending the fixture to include at least one dependency pointing at a task that gets renumbered and asserting that the writtenData dependencies are updated accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcp-server/src/core/direct-functions/move-task-cross-tag.js`:
- Around line 120-136: The wrapper builds a success message using
sourceIds.length which can undercount when withDependencies expands the moved
batch; change the wrapper to use the actual moved batch information
(result.movedTasks) or reuse result.message instead of sourceIds.length so the
reported count and text match the core formatter. Locate the block that
constructs message (references: sourceIds, args.sourceTag, args.targetTag,
result.movedTasks, result.message) and replace the manual construction that uses
sourceIds.length with either using result.message directly or computing the
count from result.movedTasks.length and include renumberDetails from
result.movedTasks as currently done.

In `@scripts/modules/task-manager/move-task.js`:
- Around line 955-980: The current remapping normalizes dotted subtask refs into
plain numbers and corrupts sibling subtask refs; change the remapping logic so
it preserves the original dependency "shape": for movedTask.dependencies keep
existing behavior (map numeric IDs and leave dotted strings alone or map their
parent component if desired), but for movedTask.subtasks[].dependencies only
remap the parent ID inside a dotted-string (e.g. "1.2" → map the "1" via
idRemapping and reassemble as "5.2") and do NOT remap pure numeric deps (they
may be local sibling-subtask refs). Update the mapping code around movedTask,
normalizeDependency, idRemapping and subtask.dependencies to detect
dotted-string deps, split and remap only the parent segment, then reassemble,
leaving non-dotted numeric deps unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eab859b7-cbfb-4a2e-b6f6-6406683ac4da

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1211b and 5f88b8c.

📒 Files selected for processing (3)
  • mcp-server/src/core/direct-functions/move-task-cross-tag.js
  • scripts/modules/task-manager/move-task.js
  • tests/integration/move-task-cross-tag.integration.test.js

Comment on lines +955 to +980
if (movedTask && Array.isArray(movedTask.dependencies)) {
movedTask.dependencies = movedTask.dependencies.map((dep) => {
const normalizedDep = normalizeDependency(dep);
if (
Number.isFinite(normalizedDep) &&
idRemapping.has(normalizedDep)
) {
return idRemapping.get(normalizedDep);
}
return dep;
});
}
// Also update subtask dependencies that reference remapped IDs
if (movedTask && Array.isArray(movedTask.subtasks)) {
movedTask.subtasks.forEach((subtask) => {
if (Array.isArray(subtask.dependencies)) {
subtask.dependencies = subtask.dependencies.map((dep) => {
const normalizedDep = normalizeDependency(dep);
if (
Number.isFinite(normalizedDep) &&
idRemapping.has(normalizedDep)
) {
return idRemapping.get(normalizedDep);
}
return dep;
});
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.

⚠️ Potential issue | 🟠 Major

Preserve subtask reference shape when remapping dependencies.

Lines 956-980 normalize every dependency down to its parent task ID. That turns a dotted reference like 1.2 into 5, and inside subtask.dependencies it can also rewrite a local sibling-subtask reference like 1 as if it were a task ID. A renumbered batch move will therefore corrupt both task-level dotted refs and subtask-level dependency refs.

🛠️ Suggested direction
+function remapDependencyReference(dep, idRemapping, { remapPlainTaskIds = true } = {}) {
+	if (typeof dep === 'string' && dep.includes('.')) {
+		const [parentId, subtaskId] = dep.split('.');
+		const remappedParentId = idRemapping.get(parseInt(parentId, 10));
+		return remappedParentId ? `${remappedParentId}.${subtaskId}` : dep;
+	}
+
+	const normalizedDep = normalizeDependency(dep);
+	if (
+		remapPlainTaskIds &&
+		Number.isFinite(normalizedDep) &&
+		idRemapping.has(normalizedDep)
+	) {
+		return idRemapping.get(normalizedDep);
+	}
+
+	return dep;
+}
...
-				movedTask.dependencies = movedTask.dependencies.map((dep) => {
-					const normalizedDep = normalizeDependency(dep);
-					if (
-						Number.isFinite(normalizedDep) &&
-						idRemapping.has(normalizedDep)
-					) {
-						return idRemapping.get(normalizedDep);
-					}
-					return dep;
-				});
+				movedTask.dependencies = movedTask.dependencies.map((dep) =>
+					remapDependencyReference(dep, idRemapping)
+				);
...
-						subtask.dependencies = subtask.dependencies.map((dep) => {
-							const normalizedDep = normalizeDependency(dep);
-							if (
-								Number.isFinite(normalizedDep) &&
-								idRemapping.has(normalizedDep)
-							) {
-								return idRemapping.get(normalizedDep);
-							}
-							return dep;
-						});
+						subtask.dependencies = subtask.dependencies.map((dep) =>
+							remapDependencyReference(dep, idRemapping, {
+								remapPlainTaskIds: false
+							})
+						);
Based on learnings, dependency arrays use numeric IDs for direct task references and dotted strings like `1.2` for subtask refs, and numeric IDs inside subtask dependencies can reference sibling subtasks within the same parent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/modules/task-manager/move-task.js` around lines 955 - 980, The
current remapping normalizes dotted subtask refs into plain numbers and corrupts
sibling subtask refs; change the remapping logic so it preserves the original
dependency "shape": for movedTask.dependencies keep existing behavior (map
numeric IDs and leave dotted strings alone or map their parent component if
desired), but for movedTask.subtasks[].dependencies only remap the parent ID
inside a dotted-string (e.g. "1.2" → map the "1" via idRemapping and reassemble
as "5.2") and do NOT remap pure numeric deps (they may be local sibling-subtask
refs). Update the mapping code around movedTask, normalizeDependency,
idRemapping and subtask.dependencies to detect dotted-string deps, split and
remap only the parent segment, then reassemble, leaving non-dotted numeric deps
unchanged.

…ate, message count

- Preserve dotted subtask suffix when remapping dependencies during ID
  collision (e.g., "1.2" becomes "5.2" when task 1 remaps to 5, not just 5)
- Update subtask parentTaskId when parent task is renumbered
- Use actual moved task count instead of sourceIds.length in MCP message
- Add integration test covering dotted dependency remapping scenario

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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: Moving tasks from one tag to another can create collisions between task number

2 participants