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:
@@ -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';
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user