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:
@@ -1,17 +0,0 @@
|
||||
---
|
||||
'task-master-ai': minor
|
||||
---
|
||||
|
||||
Add comprehensive `research` MCP tool for AI-powered research queries
|
||||
|
||||
- **New MCP Tool**: `research` tool enables AI-powered research with project context
|
||||
- **Context Integration**: Supports task IDs, file paths, custom context, and project tree
|
||||
- **Fuzzy Task Discovery**: Automatically finds relevant tasks using semantic search
|
||||
- **Token Management**: Detailed token counting and breakdown by context type
|
||||
- **Multiple Detail Levels**: Support for low, medium, and high detail research responses
|
||||
- **Telemetry Integration**: Full cost tracking and usage analytics
|
||||
- **Direct Function**: `researchDirect` with comprehensive parameter validation
|
||||
- **Silent Mode**: Prevents console output interference with MCP JSON responses
|
||||
- **Error Handling**: Robust error handling with proper MCP response formatting
|
||||
|
||||
This completes subtasks 94.5 (Direct Function) and 94.6 (MCP Tool) for the research command implementation, providing a powerful research interface for integrated development environments like Cursor.
|
||||
5
.changeset/two-lies-start.md
Normal file
5
.changeset/two-lies-start.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Improves dependency management when moving tasks by updating subtask dependencies that reference sibling subtasks by their old parent-based ID
|
||||
62
.taskmaster/tasks/task_106.txt
Normal file
62
.taskmaster/tasks/task_106.txt
Normal file
@@ -0,0 +1,62 @@
|
||||
# Task ID: 106
|
||||
# Title: Implement Fun Easter Egg Commands for Developer Delight
|
||||
# Status: pending
|
||||
# Dependencies: 2, 4
|
||||
# Priority: medium
|
||||
# Description: Add playful easter egg commands to the CLI that provide entertainment and stress relief for developers while maintaining the professional nature of the tool.
|
||||
# Details:
|
||||
## Core Problem Statement
|
||||
|
||||
Developers often work long hours and need moments of levity to maintain productivity and morale. Adding fun, non-intrusive easter egg commands can:
|
||||
|
||||
1. **Boost Developer Morale**: Provide moments of humor and surprise during intense work sessions
|
||||
2. **Showcase Tool Personality**: Give Task Master a friendly, approachable character
|
||||
3. **Create Community Engagement**: Fun features often become talking points and increase tool adoption
|
||||
4. **Stress Relief**: Offer quick mental breaks without leaving the development environment
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
1. **Add Easter Egg Commands**: Implement hidden/fun commands in commands.js:
|
||||
- `fortune` - Display random programming wisdom or motivational quotes
|
||||
- `joke` - Show developer-friendly programming jokes
|
||||
- `zen` - Display programming zen principles (like Python's zen)
|
||||
- `coffee` - ASCII art coffee cup with brewing animation
|
||||
- `rubber-duck` - Rubber duck debugging assistant with encouraging messages
|
||||
- `praise` - Random praise messages for completed tasks
|
||||
|
||||
2. **Command Structure**: Follow existing CLI patterns but make commands discoverable through:
|
||||
- Hidden help section (accessible via `--fun` flag on help command)
|
||||
- Occasional hints when users complete milestones or long work sessions
|
||||
|
||||
3. **Content Management**: Create a separate `easter-eggs.js` module containing:
|
||||
- Arrays of quotes, jokes, zen principles
|
||||
- ASCII art templates
|
||||
- Motivational messages
|
||||
- Randomization logic for content selection
|
||||
|
||||
4. **Integration Points**:
|
||||
- Add subtle hints after task completions ("Try 'task coffee' for a break!")
|
||||
- Include fun stats in status displays (e.g., "You've completed X tasks - that deserves a joke!")
|
||||
- Optional daily/weekly fun fact notifications
|
||||
|
||||
5. **Configuration**: Add optional config settings:
|
||||
- `enableEasterEggs` (default: true)
|
||||
- `funNotificationFrequency` (never, rare, occasional, frequent)
|
||||
- `favoriteEasterEgg` for personalized defaults
|
||||
|
||||
6. **ASCII Art and Animations**: Implement simple text-based animations:
|
||||
- Coffee brewing progress bars
|
||||
- Rubber duck "thinking" animations
|
||||
- Celebration ASCII art for major milestones
|
||||
|
||||
# Test Strategy:
|
||||
- Test each easter egg command individually to ensure proper content display and formatting
|
||||
- Verify ASCII art renders correctly across different terminal sizes and configurations
|
||||
- Test configuration options to ensure easter eggs can be disabled/customized
|
||||
- Validate that fun commands don't interfere with core Task Master functionality
|
||||
- Test hint integration points to ensure they appear at appropriate times without being intrusive
|
||||
- Verify content randomization works properly and doesn't repeat too frequently
|
||||
- Test command discovery through hidden help sections
|
||||
- Ensure all content is appropriate and maintains professional standards while being entertaining
|
||||
- Test performance impact to ensure easter eggs don't slow down core operations
|
||||
- Validate that easter egg commands gracefully handle edge cases (empty content arrays, display errors)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3011,6 +3011,13 @@ Examples:
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find project root for tag resolution
|
||||
const projectRoot = findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
console.error(chalk.red('Error: Could not find project root.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if we're moving multiple tasks (comma-separated IDs)
|
||||
const sourceIds = sourceId.split(',').map((id) => id.trim());
|
||||
const destinationIds = destinationId.split(',').map((id) => id.trim());
|
||||
@@ -3067,7 +3074,8 @@ Examples:
|
||||
tasksPath,
|
||||
fromId,
|
||||
toId,
|
||||
i === sourceIds.length - 1
|
||||
i === sourceIds.length - 1,
|
||||
{ projectRoot }
|
||||
);
|
||||
console.log(
|
||||
chalk.green(
|
||||
@@ -3096,7 +3104,8 @@ Examples:
|
||||
tasksPath,
|
||||
sourceId,
|
||||
destinationId,
|
||||
true
|
||||
true,
|
||||
{ projectRoot }
|
||||
);
|
||||
console.log(
|
||||
chalk.green(
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -895,10 +895,16 @@ function truncateString(str, maxLength) {
|
||||
/**
|
||||
* Display the next task to work on
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} complexityReportPath - Path to the complexity report file
|
||||
* @param {string} tag - Optional tag to override current tag resolution
|
||||
*/
|
||||
async function displayNextTask(tasksPath, complexityReportPath = null) {
|
||||
async function displayNextTask(
|
||||
tasksPath,
|
||||
complexityReportPath = null,
|
||||
tag = null
|
||||
) {
|
||||
// Read the tasks file
|
||||
const data = readJSON(tasksPath);
|
||||
const data = readJSON(tasksPath, tag);
|
||||
if (!data || !data.tasks) {
|
||||
log('error', 'No valid tasks found.');
|
||||
process.exit(1);
|
||||
@@ -1162,16 +1168,19 @@ async function displayNextTask(tasksPath, complexityReportPath = null) {
|
||||
* Display a specific task by ID
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string|number} taskId - The ID of the task to display
|
||||
* @param {string} complexityReportPath - Path to the complexity report file
|
||||
* @param {string} [statusFilter] - Optional status to filter subtasks by
|
||||
* @param {string} tag - Optional tag to override current tag resolution
|
||||
*/
|
||||
async function displayTaskById(
|
||||
tasksPath,
|
||||
taskId,
|
||||
complexityReportPath = null,
|
||||
statusFilter = null
|
||||
statusFilter = null,
|
||||
tag = null
|
||||
) {
|
||||
// Read the tasks file
|
||||
const data = readJSON(tasksPath);
|
||||
const data = readJSON(tasksPath, tag);
|
||||
if (!data || !data.tasks) {
|
||||
log('error', 'No valid tasks found.');
|
||||
process.exit(1);
|
||||
|
||||
@@ -197,9 +197,10 @@ function log(level, ...args) {
|
||||
* Reads and parses a JSON file
|
||||
* @param {string} filepath - Path to the JSON file
|
||||
* @param {string} [projectRoot] - Optional project root for tag resolution (used by MCP)
|
||||
* @param {string} [tag] - Optional tag to use instead of current tag resolution
|
||||
* @returns {Object|null} The parsed JSON data or null if error
|
||||
*/
|
||||
function readJSON(filepath, projectRoot = null) {
|
||||
function readJSON(filepath, projectRoot = null, tag = null) {
|
||||
// GUARD: Prevent circular dependency during config loading
|
||||
let isDebug = false; // Default fallback
|
||||
try {
|
||||
@@ -208,158 +209,170 @@ function readJSON(filepath, projectRoot = null) {
|
||||
} catch (error) {
|
||||
// If getDebugFlag() fails (likely due to circular dependency),
|
||||
// use default false and continue
|
||||
isDebug = false;
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
console.log(
|
||||
`readJSON called with: ${filepath}, projectRoot: ${projectRoot}, tag: ${tag}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!filepath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
if (!fs.existsSync(filepath)) {
|
||||
if (isDebug) {
|
||||
log('debug', `File not found: ${filepath}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawData = fs.readFileSync(filepath, 'utf8');
|
||||
let data = JSON.parse(rawData);
|
||||
|
||||
// Check if this is legacy tasks.json format that needs migration
|
||||
if (
|
||||
data &&
|
||||
data.tasks &&
|
||||
Array.isArray(data.tasks) &&
|
||||
!data.master &&
|
||||
filepath.includes('tasks.json')
|
||||
) {
|
||||
// This is legacy format - migrate to tagged format
|
||||
// Get file creation/modification date for the master tag
|
||||
let createdDate;
|
||||
try {
|
||||
const stats = fs.statSync(filepath);
|
||||
// Use the earlier of creation time or modification time
|
||||
createdDate =
|
||||
stats.birthtime < stats.mtime ? stats.birthtime : stats.mtime;
|
||||
} catch (error) {
|
||||
// Fallback to current date if we can't get file stats
|
||||
createdDate = new Date();
|
||||
}
|
||||
|
||||
const migratedData = {
|
||||
master: {
|
||||
tasks: data.tasks,
|
||||
description: 'Tasks live here by default',
|
||||
created: createdDate.toISOString(),
|
||||
taskCount: data.tasks ? data.tasks.length : 0
|
||||
}
|
||||
};
|
||||
|
||||
// Copy any other top-level properties except 'tasks'
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (key !== 'tasks') {
|
||||
migratedData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the migrated format back using writeJSON for consistency
|
||||
try {
|
||||
writeJSON(filepath, migratedData);
|
||||
|
||||
if (isDebug) {
|
||||
log(
|
||||
'debug',
|
||||
`Silently migrated tasks.json to tagged format: ${filepath}`
|
||||
);
|
||||
}
|
||||
|
||||
// Set global flag for CLI notice
|
||||
global.taskMasterMigrationOccurred = true;
|
||||
|
||||
// Also perform complete project migration (config.json and state.json)
|
||||
performCompleteTagMigration(filepath);
|
||||
|
||||
// Return the migrated data
|
||||
data = migratedData;
|
||||
} catch (writeError) {
|
||||
// If we can't write back, log the error but continue with migrated data in memory
|
||||
if (isDebug) {
|
||||
log(
|
||||
'warn',
|
||||
`Could not write migrated tasks.json to ${filepath}: ${writeError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Set global flag even on write failure
|
||||
global.taskMasterMigrationOccurred = true;
|
||||
|
||||
// Still attempt other migrations
|
||||
performCompleteTagMigration(filepath);
|
||||
|
||||
data = migratedData;
|
||||
}
|
||||
}
|
||||
|
||||
// Tag resolution: If data has tagged format, resolve the current tag and return old format
|
||||
// This makes tag support completely transparent to existing code
|
||||
if (data && !data.tasks && typeof data === 'object') {
|
||||
// Check if this looks like tagged format (has tag-like keys)
|
||||
const hasTaggedFormat = Object.keys(data).some(
|
||||
(key) => data[key] && data[key].tasks && Array.isArray(data[key].tasks)
|
||||
);
|
||||
|
||||
if (hasTaggedFormat) {
|
||||
// Default to master tag if anything goes wrong
|
||||
let resolvedTag = 'master';
|
||||
|
||||
// Try to resolve the correct tag, but don't fail if it doesn't work
|
||||
try {
|
||||
if (projectRoot) {
|
||||
// Use provided projectRoot
|
||||
resolvedTag = resolveTag({ projectRoot });
|
||||
} else {
|
||||
// Try to derive projectRoot from filepath
|
||||
const derivedProjectRoot = findProjectRoot(path.dirname(filepath));
|
||||
if (derivedProjectRoot) {
|
||||
resolvedTag = resolveTag({ projectRoot: derivedProjectRoot });
|
||||
}
|
||||
// If derivedProjectRoot is null, stick with 'master' default
|
||||
}
|
||||
} catch (error) {
|
||||
// If anything fails, just use master
|
||||
resolvedTag = 'master';
|
||||
}
|
||||
|
||||
// Store the raw tagged data BEFORE modifying data
|
||||
const rawTaggedData = { ...data };
|
||||
|
||||
// Return the tasks for the resolved tag, or master as fallback, or empty array
|
||||
if (data[resolvedTag] && data[resolvedTag].tasks) {
|
||||
data = {
|
||||
tag: resolvedTag,
|
||||
tasks: data[resolvedTag].tasks,
|
||||
_rawTaggedData: rawTaggedData
|
||||
};
|
||||
} else if (data.master && data.master.tasks) {
|
||||
data = {
|
||||
tag: 'master',
|
||||
tasks: data.master.tasks,
|
||||
_rawTaggedData: rawTaggedData
|
||||
};
|
||||
} else {
|
||||
// No valid tags found, return empty
|
||||
data = { tasks: [], tag: 'master', _rawTaggedData: rawTaggedData };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
log('error', `Error reading JSON file ${filepath}:`, error.message);
|
||||
data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
|
||||
if (isDebug) {
|
||||
// Use dynamic debug flag
|
||||
// Use log utility for debug output too
|
||||
log('error', 'Full error details:', error);
|
||||
console.log(`Successfully read JSON from ${filepath}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDebug) {
|
||||
console.log(`Failed to read JSON from ${filepath}: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it's not a tasks.json file or already in legacy format, return as-is
|
||||
if (!filepath.includes('tasks.json') || !data || Array.isArray(data.tasks)) {
|
||||
if (isDebug) {
|
||||
console.log(`File is not tagged format or is legacy, returning as-is`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// If we have tagged data, we need to resolve which tag to use
|
||||
if (typeof data === 'object' && !data.tasks) {
|
||||
// This is tagged format
|
||||
if (isDebug) {
|
||||
console.log(`File is in tagged format, resolving tag...`);
|
||||
}
|
||||
|
||||
// Ensure all tags have proper metadata before proceeding
|
||||
for (const tagName in data) {
|
||||
if (
|
||||
data.hasOwnProperty(tagName) &&
|
||||
typeof data[tagName] === 'object' &&
|
||||
data[tagName].tasks
|
||||
) {
|
||||
try {
|
||||
ensureTagMetadata(data[tagName], {
|
||||
description: `Tasks for ${tagName} context`,
|
||||
skipUpdate: true // Don't update timestamp during read operations
|
||||
});
|
||||
} catch (error) {
|
||||
// If ensureTagMetadata fails, continue without metadata
|
||||
if (isDebug) {
|
||||
console.log(
|
||||
`Failed to ensure metadata for tag ${tagName}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store reference to the raw tagged data for functions that need it
|
||||
const originalTaggedData = JSON.parse(JSON.stringify(data));
|
||||
|
||||
try {
|
||||
// Default to master tag if anything goes wrong
|
||||
let resolvedTag = 'master';
|
||||
|
||||
// Try to resolve the correct tag, but don't fail if it doesn't work
|
||||
try {
|
||||
// If tag is provided, use it directly
|
||||
if (tag) {
|
||||
resolvedTag = tag;
|
||||
} else if (projectRoot) {
|
||||
// Use provided projectRoot
|
||||
resolvedTag = resolveTag({ projectRoot });
|
||||
} else {
|
||||
// Try to derive projectRoot from filepath
|
||||
const derivedProjectRoot = findProjectRoot(path.dirname(filepath));
|
||||
if (derivedProjectRoot) {
|
||||
resolvedTag = resolveTag({ projectRoot: derivedProjectRoot });
|
||||
}
|
||||
// If derivedProjectRoot is null, stick with 'master'
|
||||
}
|
||||
} catch (tagResolveError) {
|
||||
if (isDebug) {
|
||||
console.log(
|
||||
`Tag resolution failed, using master: ${tagResolveError.message}`
|
||||
);
|
||||
}
|
||||
// resolvedTag stays as 'master'
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
console.log(`Resolved tag: ${resolvedTag}`);
|
||||
}
|
||||
|
||||
// Get the data for the resolved tag
|
||||
const tagData = data[resolvedTag];
|
||||
if (tagData && tagData.tasks) {
|
||||
// Add the _rawTaggedData property and the resolved tag to the returned data
|
||||
const result = {
|
||||
...tagData,
|
||||
tag: resolvedTag,
|
||||
_rawTaggedData: originalTaggedData
|
||||
};
|
||||
if (isDebug) {
|
||||
console.log(
|
||||
`Returning data for tag '${resolvedTag}' with ${tagData.tasks.length} tasks`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
// If the resolved tag doesn't exist, fall back to master
|
||||
const masterData = data.master;
|
||||
if (masterData && masterData.tasks) {
|
||||
if (isDebug) {
|
||||
console.log(
|
||||
`Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks`
|
||||
);
|
||||
}
|
||||
return {
|
||||
...masterData,
|
||||
tag: 'master',
|
||||
_rawTaggedData: originalTaggedData
|
||||
};
|
||||
} else {
|
||||
if (isDebug) {
|
||||
console.log(`No valid tag data found, returning empty structure`);
|
||||
}
|
||||
// Return empty structure if no valid data
|
||||
return {
|
||||
tasks: [],
|
||||
tag: 'master',
|
||||
_rawTaggedData: originalTaggedData
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDebug) {
|
||||
console.log(`Error during tag resolution: ${error.message}`);
|
||||
}
|
||||
// If anything goes wrong, try to return master or empty
|
||||
const masterData = data.master;
|
||||
if (masterData && masterData.tasks) {
|
||||
return {
|
||||
...masterData,
|
||||
_rawTaggedData: originalTaggedData
|
||||
};
|
||||
}
|
||||
return {
|
||||
tasks: [],
|
||||
_rawTaggedData: originalTaggedData
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, it's some other format
|
||||
if (isDebug) {
|
||||
console.log(`File format not recognized, returning as-is`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -533,9 +546,13 @@ function writeJSON(filepath, data) {
|
||||
|
||||
// Clean the data before writing - remove internal properties that should not be persisted
|
||||
let cleanData = data;
|
||||
if (data && typeof data === 'object' && data._rawTaggedData !== undefined) {
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
(data._rawTaggedData !== undefined || data.tag !== undefined)
|
||||
) {
|
||||
// Create a clean copy without internal properties using destructuring
|
||||
const { _rawTaggedData, ...cleanedData } = data;
|
||||
const { _rawTaggedData, tag, ...cleanedData } = data;
|
||||
cleanData = cleanedData;
|
||||
}
|
||||
|
||||
@@ -1098,6 +1115,48 @@ function flattenTasksWithSubtasks(tasks) {
|
||||
return flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the tag object has a metadata object with created/updated timestamps.
|
||||
* @param {Object} tagObj - The tag object (e.g., data['master'])
|
||||
* @param {Object} [opts] - Optional fields (e.g., description, skipUpdate)
|
||||
* @param {string} [opts.description] - Description for the tag
|
||||
* @param {boolean} [opts.skipUpdate] - If true, don't update the 'updated' timestamp
|
||||
* @returns {Object} The updated tag object (for chaining)
|
||||
*/
|
||||
function ensureTagMetadata(tagObj, opts = {}) {
|
||||
if (!tagObj || typeof tagObj !== 'object') {
|
||||
throw new Error('tagObj must be a valid object');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (!tagObj.metadata) {
|
||||
// Create new metadata object
|
||||
tagObj.metadata = {
|
||||
created: now,
|
||||
updated: now,
|
||||
...(opts.description ? { description: opts.description } : {})
|
||||
};
|
||||
} else {
|
||||
// Ensure existing metadata has required fields
|
||||
if (!tagObj.metadata.created) {
|
||||
tagObj.metadata.created = now;
|
||||
}
|
||||
|
||||
// Update timestamp unless explicitly skipped
|
||||
if (!opts.skipUpdate) {
|
||||
tagObj.metadata.updated = now;
|
||||
}
|
||||
|
||||
// Add description if provided and not already present
|
||||
if (opts.description && !tagObj.metadata.description) {
|
||||
tagObj.metadata.description = opts.description;
|
||||
}
|
||||
}
|
||||
|
||||
return tagObj;
|
||||
}
|
||||
|
||||
// Export all utility functions and configuration
|
||||
export {
|
||||
LOG_LEVELS,
|
||||
@@ -1130,5 +1189,6 @@ export {
|
||||
migrateConfigJson,
|
||||
createStateJson,
|
||||
markMigrationForNotice,
|
||||
flattenTasksWithSubtasks
|
||||
flattenTasksWithSubtasks,
|
||||
ensureTagMetadata
|
||||
};
|
||||
|
||||
70
test-clean-tags.js
Normal file
70
test-clean-tags.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import fs from 'fs';
|
||||
import {
|
||||
createTag,
|
||||
listTags
|
||||
} from './scripts/modules/task-manager/tag-management.js';
|
||||
|
||||
console.log('=== Testing Tag Management with Clean File ===');
|
||||
|
||||
// Create a clean test tasks.json file
|
||||
const testTasksPath = './test-tasks.json';
|
||||
const cleanData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{ id: 1, title: 'Test Task 1', status: 'pending' },
|
||||
{ id: 2, title: 'Test Task 2', status: 'done' }
|
||||
],
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
description: 'Master tag'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Write clean test file
|
||||
fs.writeFileSync(testTasksPath, JSON.stringify(cleanData, null, 2));
|
||||
console.log('Created clean test file');
|
||||
|
||||
try {
|
||||
// Test creating a new tag
|
||||
console.log('\n--- Testing createTag ---');
|
||||
await createTag(
|
||||
testTasksPath,
|
||||
'test-branch',
|
||||
{ copyFromCurrent: true, description: 'Test branch' },
|
||||
{ projectRoot: process.cwd() },
|
||||
'json'
|
||||
);
|
||||
|
||||
// Read the file and check for corruption
|
||||
const resultData = JSON.parse(fs.readFileSync(testTasksPath, 'utf8'));
|
||||
console.log('Keys in result file:', Object.keys(resultData));
|
||||
console.log('Has _rawTaggedData in file:', !!resultData._rawTaggedData);
|
||||
|
||||
if (resultData._rawTaggedData) {
|
||||
console.log('❌ CORRUPTION DETECTED: _rawTaggedData found in file!');
|
||||
} else {
|
||||
console.log('✅ SUCCESS: No _rawTaggedData corruption in file');
|
||||
}
|
||||
|
||||
// Test listing tags
|
||||
console.log('\n--- Testing listTags ---');
|
||||
const tagList = await listTags(
|
||||
testTasksPath,
|
||||
{},
|
||||
{ projectRoot: process.cwd() },
|
||||
'json'
|
||||
);
|
||||
console.log(
|
||||
'Found tags:',
|
||||
tagList.tags.map((t) => t.name)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error during test:', error.message);
|
||||
} finally {
|
||||
// Clean up test file
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
console.log('\nCleaned up test file');
|
||||
}
|
||||
}
|
||||
1
test-tag-functions.js
Normal file
1
test-tag-functions.js
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user