fix(core): Fixed move-task.js writing _rawTaggedData directly, updated writeJSON to filter tag fields, fixed CLI move command missing projectRoot, added ensureTagMetadata utility

This commit is contained in:
Eyal Toledano
2025-06-12 18:12:53 -04:00
parent 75e017e371
commit 9efbd38f10
15 changed files with 589 additions and 40487 deletions

View File

@@ -15,7 +15,15 @@ import {
displayAiUsageSummary,
displayContextAnalysis
} from '../ui.js';
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js';
import {
readJSON,
writeJSON,
log as consoleLog,
truncate,
ensureTagMetadata,
performCompleteTagMigration,
markMigrationForNotice
} from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
@@ -41,6 +49,25 @@ const AiTaskDataSchema = z.object({
)
});
/**
* Get all tasks from all tags
* @param {Object} rawData - The raw tagged data object
* @returns {Array} A flat array of all task objects
*/
function getAllTasks(rawData) {
let allTasks = [];
for (const tagName in rawData) {
if (
Object.prototype.hasOwnProperty.call(rawData, tagName) &&
rawData[tagName] &&
Array.isArray(rawData[tagName].tasks)
) {
allTasks = allTasks.concat(rawData[tagName].tasks);
}
}
return allTasks;
}
/**
* Add a new task using AI
* @param {string} tasksPath - Path to the tasks.json file
@@ -58,6 +85,7 @@ const AiTaskDataSchema = z.object({
* @param {string} [context.projectRoot] - Project root path (for MCP/env fallback)
* @param {string} [context.commandName] - The name of the command being executed (for telemetry)
* @param {string} [context.outputType] - The output type ('cli' or 'mcp', for telemetry)
* @param {string} [tag] - Tag for the task (optional)
* @returns {Promise<object>} An object containing newTaskId and telemetryData
*/
async function addTask(
@@ -68,7 +96,8 @@ async function addTask(
context = {},
outputFormat = 'text', // Default to text for CLI
manualTaskData = null,
useResearch = false
useResearch = false,
tag = null
) {
const { session, mcpLog, projectRoot, commandName, outputType } = context;
const isMCP = !!mcpLog;
@@ -90,6 +119,9 @@ async function addTask(
logFn.info(
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
);
if (tag) {
logFn.info(`Using tag context: ${tag}`);
}
let loadingIndicator = null;
let aiServiceResponse = null; // To store the full response from AI service
@@ -165,24 +197,82 @@ async function addTask(
}
try {
// Read the existing tasks
let data = readJSON(tasksPath);
// Read the existing tasks - IMPORTANT: Read the raw data without tag resolution
let rawData = readJSON(tasksPath, projectRoot); // No tag parameter
// If tasks.json doesn't exist or is invalid, create a new one
if (!data || !data.tasks) {
report('tasks.json not found or invalid. Creating a new one.', 'info');
// Create default tasks data structure
data = {
tasks: []
};
// Ensure the directory exists and write the new file
writeJSON(tasksPath, data);
report('Created new tasks.json file with empty tasks array.', 'info');
// Handle the case where readJSON returns resolved data with _rawTaggedData
if (rawData && rawData._rawTaggedData) {
// Use the raw tagged data and discard the resolved view
rawData = rawData._rawTaggedData;
}
// Find the highest task ID to determine the next ID
// If file doesn't exist or is invalid, create a new structure
if (!rawData) {
report('tasks.json not found or invalid. Creating a new one.', 'info');
rawData = {
master: {
tasks: [],
metadata: {
created: new Date().toISOString(),
description: 'Default tasks context'
}
}
};
writeJSON(tasksPath, rawData);
report(
'Created new tasks.json file with a default "master" tag.',
'info'
);
}
// 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;
rawData = {
master: {
tasks: legacyTasks
}
};
// Ensure proper metadata using utility
ensureTagMetadata(rawData.master, {
description: 'Tasks for master context'
});
writeJSON(tasksPath, rawData);
// Perform complete migration (config.json, state.json)
performCompleteTagMigration(tasksPath);
markMigrationForNotice(tasksPath);
report('Successfully migrated to tagged format.', 'success');
}
// Use the provided tag, or the current tag, or default to 'master'
const targetTag = tag || context.tag || 'master';
// Ensure the target tag exists
if (!rawData[targetTag]) {
report(
`Tag "${targetTag}" does not exist. Please create it first using the 'add-tag' command.`,
'error'
);
throw new Error(`Tag "${targetTag}" not found.`);
}
// Ensure the target tag has a metadata object
if (!rawData[targetTag].metadata) {
rawData[targetTag].metadata = {
created: new Date().toISOString(),
description: ``
};
}
// Get a flat list of ALL tasks across ALL tags to calculate new ID and validate dependencies
const allTasks = getAllTasks(rawData);
// Find the highest task ID across all tags to determine the next ID
const highestId =
data.tasks.length > 0 ? Math.max(...data.tasks.map((t) => t.id)) : 0;
allTasks.length > 0 ? Math.max(...allTasks.map((t) => t.id)) : 0;
const newTaskId = highestId + 1;
// Only show UI box for CLI mode
@@ -201,9 +291,7 @@ async function addTask(
const invalidDeps = dependencies.filter((depId) => {
// Ensure depId is parsed as a number for comparison
const numDepId = parseInt(depId, 10);
return (
Number.isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId)
);
return Number.isNaN(numDepId) || !allTasks.some((t) => t.id === numDepId);
});
if (invalidDeps.length > 0) {
@@ -226,12 +314,7 @@ async function addTask(
// First pass: build a complete dependency graph for each specified dependency
for (const depId of numericDependencies) {
const graph = buildDependencyGraph(
data.tasks,
depId,
new Set(),
depthMap
);
const graph = buildDependencyGraph(allTasks, depId, new Set(), depthMap);
if (graph) {
dependencyGraphs.push(graph);
}
@@ -444,7 +527,7 @@ async function addTask(
const allValidDeps = taskData.dependencies.every((depId) => {
const numDepId = parseInt(depId, 10);
return (
!Number.isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId)
!Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
);
});
@@ -456,18 +539,23 @@ async function addTask(
newTask.dependencies = taskData.dependencies.filter((depId) => {
const numDepId = parseInt(depId, 10);
return (
!Number.isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId)
!Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
);
});
}
}
// Add the task to the tasks array
data.tasks.push(newTask);
// Add the task to the tasks array OF THE CORRECT TAG
rawData[targetTag].tasks.push(newTask);
// Update the tag's metadata
ensureTagMetadata(rawData[targetTag], {
description: `Tasks for ${targetTag} context`
});
report('DEBUG: Writing tasks.json...', 'debug');
// Write the updated tasks to the file
writeJSON(tasksPath, data);
// Write the updated raw data back to the file
// The writeJSON function will automatically filter out _rawTaggedData
writeJSON(tasksPath, rawData);
report('DEBUG: tasks.json written.', 'debug');
// Generate markdown task files
@@ -522,7 +610,7 @@ async function addTask(
// Get task titles for dependencies to display
const depTitles = {};
newTask.dependencies.forEach((dep) => {
const depTask = data.tasks.find((t) => t.id === dep);
const depTask = allTasks.find((t) => t.id === dep);
if (depTask) {
depTitles[dep] = truncate(depTask.title, 30);
}
@@ -550,7 +638,7 @@ async function addTask(
chalk.gray('\nUser-specified dependencies that were not used:') +
'\n';
aiRemovedDeps.forEach((dep) => {
const depTask = data.tasks.find((t) => t.id === dep);
const depTask = allTasks.find((t) => t.id === dep);
const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n';
});

View File

@@ -26,6 +26,7 @@ import {
* @param {string} reportPath - Path to the complexity report
* @param {boolean} withSubtasks - Whether to show subtasks
* @param {string} outputFormat - Output format (text or json)
* @param {string} tag - Optional tag to override current tag resolution
* @returns {Object} - Task list result for json format
*/
function listTasks(
@@ -33,10 +34,11 @@ function listTasks(
statusFilter,
reportPath = null,
withSubtasks = false,
outputFormat = 'text'
outputFormat = 'text',
tag = null
) {
try {
const data = readJSON(tasksPath); // Reads the whole tasks.json
const data = readJSON(tasksPath, null, tag); // Pass tag to readJSON
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}

View File

@@ -63,15 +63,32 @@ async function moveTask(
}
// Single move logic
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
// Read the raw data without tag resolution to preserve tagged structure
let rawData = readJSON(tasksPath, options.projectRoot); // No tag parameter
// Handle the case where readJSON returns resolved data with _rawTaggedData
if (rawData && rawData._rawTaggedData) {
// Use the raw tagged data and discard the resolved view
rawData = rawData._rawTaggedData;
}
// Note: readJSON() already handles tag resolution transparently
// data.tasks contains the tasks for the current tag
const tasks = data.tasks;
const currentTag = data.tag || 'master';
// Determine the current tag
const currentTag =
options.tag || getCurrentTag(options.projectRoot) || 'master';
// Ensure the tag exists in the raw data
if (
!rawData ||
!rawData[currentTag] ||
!Array.isArray(rawData[currentTag].tasks)
) {
throw new Error(
`Invalid tasks file or tag "${currentTag}" not found at ${tasksPath}`
);
}
// Get the tasks for the current tag
const tasks = rawData[currentTag].tasks;
log(
'info',
@@ -99,15 +116,11 @@ async function moveTask(
}
// Update the data structure with the modified tasks
data.tasks = tasks;
rawData[currentTag].tasks = tasks;
// If we have raw tagged data, also update that to maintain consistency
if (data._rawTaggedData && data._rawTaggedData[currentTag]) {
data._rawTaggedData[currentTag].tasks = tasks;
}
// Write the updated data back
writeJSON(tasksPath, data._rawTaggedData || data);
// Always write the data object, never the _rawTaggedData directly
// The writeJSON function will filter out _rawTaggedData automatically
writeJSON(tasksPath, rawData);
if (generateFiles) {
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
@@ -426,6 +439,25 @@ function moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId) {
}
});
// Update dependencies within movedTask's subtasks that reference sibling subtasks
if (Array.isArray(movedTask.subtasks)) {
movedTask.subtasks.forEach((subtask) => {
if (Array.isArray(subtask.dependencies)) {
subtask.dependencies = subtask.dependencies.map((dep) => {
// If dependency is a string like "oldParent.subId", update to "newParent.subId"
if (typeof dep === 'string' && dep.includes('.')) {
const [depParent, depSub] = dep.split('.');
if (parseInt(depParent, 10) === sourceTask.id) {
return `${destTaskId}.${depSub}`;
}
}
// If dependency is a number, and matches a subtask ID in the moved task, leave as is (context is implied)
return dep;
});
}
});
}
// Strategy based on commit fixes: remove source first, then replace destination
// This avoids index shifting problems

View File

@@ -11,7 +11,8 @@ import {
disableSilentMode,
isSilentMode,
readJSON,
findTaskById
findTaskById,
ensureTagMetadata
} from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js';
@@ -312,7 +313,23 @@ Guidelines:
const finalTasks = append
? [...existingTasks, ...processedNewTasks]
: processedNewTasks;
const outputData = { tasks: finalTasks };
// Create proper tagged structure with metadata
const outputData = {
master: {
tasks: finalTasks,
metadata: {
created: new Date().toISOString(),
updated: new Date().toISOString(),
description: 'Tasks for master context'
}
}
};
// Ensure the master tag has proper metadata
ensureTagMetadata(outputData.master, {
description: 'Tasks for master context'
});
// Write the final tasks to the file
writeJSON(tasksPath, outputData);

View File

@@ -19,9 +19,16 @@ import {
* @param {string} taskIdInput - Task ID(s) to update
* @param {string} newStatus - New status
* @param {Object} options - Additional options (mcpLog for MCP mode)
* @param {string} tag - Optional tag to override current tag resolution
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
*/
async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
async function setTaskStatus(
tasksPath,
taskIdInput,
newStatus,
options = {},
tag = null
) {
try {
if (!isValidTaskStatus(newStatus)) {
throw new Error(
@@ -43,7 +50,7 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
}
log('info', `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, null, tag);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}