fix(tags): Resolve critical tag deletion and migration notice bugs

Major Issues Fixed:

1. Tag Deletion Bug: Fixed critical issue where creating subtasks would delete other tags

   - Root cause: writeJSON function wasn't accepting projectRoot/tag parameters

   - Fixed writeJSON signature and logic to handle tagged data structure

   - Added proper merging of resolved tag data back into full tagged structure

2. Persistent Migration Notice: Fixed FYI notice showing after every command

   - Root cause: markMigrationForNotice was resetting migrationNoticeShown to false

   - Fixed migration logic to only trigger on actual legacy->tagged migrations

   - Added proper _rawTaggedData checks to prevent false migration detection

3. Data Corruption Prevention: Enhanced data integrity safeguards

   - Fixed writeJSON to filter out internal properties

   - Added automatic cleanup of rogue properties

   - Improved hasTaggedStructure detection logic

Commands Fixed: add-subtask, remove-subtask, and all commands now preserve tags correctly
This commit is contained in:
Eyal Toledano
2025-06-12 23:43:48 -04:00
parent b3ec151b27
commit 4585a6bbc7
9 changed files with 205 additions and 75 deletions

View File

@@ -11,6 +11,7 @@ import generateTaskFiles from './generate-task-files.js';
* @param {number|string|null} existingTaskId - ID of an existing task to convert to subtask (optional)
* @param {Object} newSubtaskData - Data for creating a new subtask (used if existingTaskId is null)
* @param {boolean} generateFiles - Whether to regenerate task files after adding the subtask
* @param {Object} context - Context object containing projectRoot and tag information
* @returns {Object} The newly created or converted subtask
*/
async function addSubtask(
@@ -18,13 +19,14 @@ async function addSubtask(
parentId,
existingTaskId = null,
newSubtaskData = null,
generateFiles = true
generateFiles = true,
context = {}
) {
try {
log('info', `Adding subtask to parent task ${parentId}...`);
// Read the existing tasks
const data = readJSON(tasksPath);
// Read the existing tasks with proper context
const data = readJSON(tasksPath, context.projectRoot, context.tag);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
@@ -134,13 +136,13 @@ async function addSubtask(
);
}
// Write the updated tasks back to the file
writeJSON(tasksPath, data);
// Write the updated tasks back to the file with proper context
writeJSON(tasksPath, data, context.projectRoot, context.tag);
// Generate task files if requested
if (generateFiles) {
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
}
return newSubtask;

View File

@@ -226,12 +226,18 @@ async function addTask(
}
// Handle legacy format migration using utilities
if (rawData && Array.isArray(rawData.tasks)) {
report('Migrating legacy tasks.json format to tagged format...', 'info');
const legacyTasks = rawData.tasks;
if (rawData && Array.isArray(rawData.tasks) && !rawData._rawTaggedData) {
report('Legacy format detected. Migrating to tagged format...', 'info');
// This is legacy format - migrate it to tagged format
rawData = {
master: {
tasks: legacyTasks
tasks: rawData.tasks,
metadata: rawData.metadata || {
created: new Date().toISOString(),
updated: new Date().toISOString(),
description: 'Tasks for master context'
}
}
};
// Ensure proper metadata using utility

View File

@@ -11,7 +11,7 @@ import { getDebugFlag } from '../config-manager.js';
* Generate individual task files from tasks.json
* @param {string} tasksPath - Path to the tasks.json file
* @param {string} outputDir - Output directory for task files
* @param {Object} options - Additional options (mcpLog for MCP mode)
* @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot, tag)
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
*/
function generateTaskFiles(tasksPath, outputDir, options = {}) {
@@ -19,7 +19,7 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
// Determine if we're in MCP mode by checking for mcpLog
const isMcpMode = !!options?.mcpLog;
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, options.projectRoot, options.tag);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}
@@ -33,7 +33,12 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
// Validate and fix dependencies before generating files
log('info', `Validating and fixing dependencies`);
validateAndFixDependencies(data, tasksPath);
validateAndFixDependencies(
data,
tasksPath,
options.projectRoot,
options.tag
);
// Get valid task IDs from tasks.json
const validTaskIds = data.tasks.map((task) => task.id);

View File

@@ -566,10 +566,10 @@ async function tags(
tagList.push({
name: tagName,
isCurrent: tagName === currentTag,
taskCount: tasks.length,
completedTasks: tasks.filter(
(t) => t.status === 'done' || t.status === 'completed'
).length,
tasks: tasks || [],
created: metadata.created || 'Unknown',
description: metadata.description || 'No description'
});
@@ -634,7 +634,7 @@ async function tags(
row.push(tagDisplay);
if (showTaskCounts) {
row.push(chalk.white(tag.taskCount.toString()));
row.push(chalk.white(tag.tasks.length.toString()));
row.push(chalk.green(tag.completedTasks.toString()));
}
@@ -1034,8 +1034,7 @@ async function copyTag(
`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`,
copiedFrom: {
tag: sourceName,
date: new Date().toISOString(),
taskCount: sourceTasks.length
date: new Date().toISOString()
}
}
};
@@ -1061,7 +1060,6 @@ async function copyTag(
sourceName,
targetName,
copied: true,
taskCount: sourceTasks.length,
description:
description ||
`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`
@@ -1091,7 +1089,6 @@ async function copyTag(
sourceName,
targetName,
copied: true,
taskCount: sourceTasks.length,
description:
description ||
`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`