feat(tags): Add --tag flag support to core commands for multi-context task management
- parse-prd now supports creating tasks in specific contexts - Fixed tag preservation logic to prevent data loss - analyze-complexity generates tag-specific reports - Non-existent tags created automatically - Enables rapid prototyping and parallel development workflows
This commit is contained in:
@@ -22,7 +22,8 @@ import {
|
||||
truncate,
|
||||
ensureTagMetadata,
|
||||
performCompleteTagMigration,
|
||||
markMigrationForNotice
|
||||
markMigrationForNotice,
|
||||
getCurrentTag
|
||||
} from '../utils.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { getDefaultPriority } from '../config-manager.js';
|
||||
@@ -253,8 +254,9 @@ async function addTask(
|
||||
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';
|
||||
// Use the provided tag, or the current active tag, or default to 'master'
|
||||
const targetTag =
|
||||
tag || context.tag || getCurrentTag(projectRoot) || 'master';
|
||||
|
||||
// Ensure the target tag exists
|
||||
if (!rawData[targetTag]) {
|
||||
|
||||
@@ -87,6 +87,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
const thresholdScore = parseFloat(options.threshold || '5');
|
||||
const useResearch = options.research || false;
|
||||
const projectRoot = options.projectRoot;
|
||||
const tag = options.tag;
|
||||
// New parameters for task ID filtering
|
||||
const specificIds = options.id
|
||||
? options.id
|
||||
@@ -126,7 +127,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
originalTaskCount = options._originalTaskCount || tasksData.tasks.length;
|
||||
if (!options._originalTaskCount) {
|
||||
try {
|
||||
originalData = readJSON(tasksPath);
|
||||
originalData = readJSON(tasksPath, projectRoot, tag);
|
||||
if (originalData && originalData.tasks) {
|
||||
originalTaskCount = originalData.tasks.length;
|
||||
}
|
||||
@@ -135,7 +136,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
originalData = readJSON(tasksPath);
|
||||
originalData = readJSON(tasksPath, projectRoot, tag);
|
||||
if (
|
||||
!originalData ||
|
||||
!originalData.tasks ||
|
||||
@@ -278,7 +279,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
const existingAnalysisMap = new Map(); // For quick lookups by task ID
|
||||
try {
|
||||
if (fs.existsSync(outputPath)) {
|
||||
existingReport = readJSON(outputPath);
|
||||
existingReport = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
reportLog(`Found existing complexity report at ${outputPath}`, 'info');
|
||||
|
||||
if (
|
||||
@@ -337,7 +338,11 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
complexityAnalysis: existingReport?.complexityAnalysis || []
|
||||
};
|
||||
reportLog(`Writing complexity report to ${outputPath}...`, 'info');
|
||||
writeJSON(outputPath, emptyReport);
|
||||
fs.writeFileSync(
|
||||
outputPath,
|
||||
JSON.stringify(emptyReport, null, '\t'),
|
||||
'utf8'
|
||||
);
|
||||
reportLog(
|
||||
`Task complexity analysis complete. Report written to ${outputPath}`,
|
||||
'success'
|
||||
@@ -564,7 +569,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
complexityAnalysis: finalComplexityAnalysis
|
||||
};
|
||||
reportLog(`Writing complexity report to ${outputPath}...`, 'info');
|
||||
writeJSON(outputPath, report);
|
||||
fs.writeFileSync(outputPath, JSON.stringify(report, null, '\t'), 'utf8');
|
||||
|
||||
reportLog(
|
||||
`Task complexity analysis complete. Report written to ${outputPath}`,
|
||||
|
||||
@@ -11,10 +11,12 @@ import generateTaskFiles from './generate-task-files.js';
|
||||
* Clear subtasks from specified tasks
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} taskIds - Task IDs to clear subtasks from
|
||||
* @param {Object} context - Context object containing projectRoot and tag
|
||||
*/
|
||||
function clearSubtasks(tasksPath, taskIds) {
|
||||
function clearSubtasks(tasksPath, taskIds, context = {}) {
|
||||
const { projectRoot, tag } = context;
|
||||
log('info', `Reading tasks from ${tasksPath}...`);
|
||||
const data = readJSON(tasksPath);
|
||||
const data = readJSON(tasksPath, projectRoot, tag);
|
||||
if (!data || !data.tasks) {
|
||||
log('error', 'No valid tasks found.');
|
||||
process.exit(1);
|
||||
@@ -48,7 +50,7 @@ function clearSubtasks(tasksPath, taskIds) {
|
||||
|
||||
taskIdArray.forEach((taskId) => {
|
||||
const id = parseInt(taskId, 10);
|
||||
if (isNaN(id)) {
|
||||
if (Number.isNaN(id)) {
|
||||
log('error', `Invalid task ID: ${taskId}`);
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +84,7 @@ function clearSubtasks(tasksPath, taskIds) {
|
||||
});
|
||||
|
||||
if (clearedCount > 0) {
|
||||
writeJSON(tasksPath, data);
|
||||
writeJSON(tasksPath, data, projectRoot, tag);
|
||||
|
||||
// Show summary table
|
||||
if (!isSilentMode()) {
|
||||
@@ -99,7 +101,7 @@ function clearSubtasks(tasksPath, taskIds) {
|
||||
|
||||
// Regenerate task files to reflect changes
|
||||
log('info', 'Regenerating task files...');
|
||||
generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
generateTaskFiles(tasksPath, path.dirname(tasksPath), { projectRoot, tag });
|
||||
|
||||
// Success message
|
||||
if (!isSilentMode()) {
|
||||
|
||||
@@ -64,17 +64,29 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
log('info', 'Checking for orphaned task files to clean up...');
|
||||
try {
|
||||
const files = fs.readdirSync(outputDir);
|
||||
const taskFilePattern = /^task_(\d+)\.txt$/;
|
||||
// Tag-aware file patterns: master -> task_001.txt, other tags -> task_001_tagname.txt
|
||||
const masterFilePattern = /^task_(\d+)\.txt$/;
|
||||
const taggedFilePattern = new RegExp(`^task_(\\d+)_${targetTag}\\.txt$`);
|
||||
|
||||
const orphanedFiles = files.filter((file) => {
|
||||
const match = file.match(taskFilePattern);
|
||||
if (match) {
|
||||
const fileTaskId = parseInt(match[1], 10);
|
||||
// Important: Only clean up files for tasks that *should* be in the current tag.
|
||||
// This prevents deleting files from other tags.
|
||||
// A more robust cleanup might need to check across all tags.
|
||||
// For now, this is safer than the previous implementation.
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
let match = null;
|
||||
let fileTaskId = null;
|
||||
|
||||
// Check if file belongs to current tag
|
||||
if (targetTag === 'master') {
|
||||
match = file.match(masterFilePattern);
|
||||
if (match) {
|
||||
fileTaskId = parseInt(match[1], 10);
|
||||
// Only clean up master files when processing master tag
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
}
|
||||
} else {
|
||||
match = file.match(taggedFilePattern);
|
||||
if (match) {
|
||||
fileTaskId = parseInt(match[1], 10);
|
||||
// Only clean up files for the current tag
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -98,10 +110,13 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
// Generate task files for the target tag
|
||||
log('info', `Generating individual task files for tag '${targetTag}'...`);
|
||||
tasksForGeneration.forEach((task) => {
|
||||
const taskPath = path.join(
|
||||
outputDir,
|
||||
`task_${task.id.toString().padStart(3, '0')}.txt`
|
||||
);
|
||||
// Tag-aware file naming: master -> task_001.txt, other tags -> task_001_tagname.txt
|
||||
const taskFileName =
|
||||
targetTag === 'master'
|
||||
? `task_${task.id.toString().padStart(3, '0')}.txt`
|
||||
: `task_${task.id.toString().padStart(3, '0')}_${targetTag}.txt`;
|
||||
|
||||
const taskPath = path.join(outputDir, taskFileName);
|
||||
|
||||
let content = `# Task ID: ${task.id}\n`;
|
||||
content += `# Title: ${task.title}\n`;
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
isSilentMode,
|
||||
readJSON,
|
||||
findTaskById,
|
||||
ensureTagMetadata
|
||||
ensureTagMetadata,
|
||||
getCurrentTag
|
||||
} from '../utils.js';
|
||||
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
@@ -56,6 +57,7 @@ const prdResponseSchema = z.object({
|
||||
* @param {Object} [options.mcpLog] - MCP logger object (optional).
|
||||
* @param {Object} [options.session] - Session object from MCP server (optional).
|
||||
* @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
|
||||
* @param {string} [options.tag] - Target tag for task generation.
|
||||
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
|
||||
*/
|
||||
async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
@@ -66,11 +68,15 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
projectRoot,
|
||||
force = false,
|
||||
append = false,
|
||||
research = false
|
||||
research = false,
|
||||
tag
|
||||
} = options;
|
||||
const isMCP = !!mcpLog;
|
||||
const outputFormat = isMCP ? 'json' : 'text';
|
||||
|
||||
// Use the provided tag, or the current active tag, or default to 'master'
|
||||
const targetTag = tag || getCurrentTag(projectRoot) || 'master';
|
||||
|
||||
const logFn = mcpLog
|
||||
? mcpLog
|
||||
: {
|
||||
@@ -102,34 +108,41 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
let aiServiceResponse = null;
|
||||
|
||||
try {
|
||||
// Handle file existence and overwrite/append logic
|
||||
// Check if there are existing tasks in the target tag
|
||||
let hasExistingTasksInTag = false;
|
||||
if (fs.existsSync(tasksPath)) {
|
||||
try {
|
||||
// Read the entire file to check if the tag exists
|
||||
const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
|
||||
const allData = JSON.parse(existingFileContent);
|
||||
|
||||
// Check if the target tag exists and has tasks
|
||||
if (
|
||||
allData[targetTag] &&
|
||||
Array.isArray(allData[targetTag].tasks) &&
|
||||
allData[targetTag].tasks.length > 0
|
||||
) {
|
||||
hasExistingTasksInTag = true;
|
||||
existingTasks = allData[targetTag].tasks;
|
||||
nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't read the file or parse it, assume no existing tasks in this tag
|
||||
hasExistingTasksInTag = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file existence and overwrite/append logic based on target tag
|
||||
if (hasExistingTasksInTag) {
|
||||
if (append) {
|
||||
report(
|
||||
`Append mode enabled. Reading existing tasks from ${tasksPath}`,
|
||||
`Append mode enabled. Found ${existingTasks.length} existing tasks in tag '${targetTag}'. Next ID will be ${nextId}.`,
|
||||
'info'
|
||||
);
|
||||
const existingData = readJSON(tasksPath); // Use readJSON utility
|
||||
if (existingData && Array.isArray(existingData.tasks)) {
|
||||
existingTasks = existingData.tasks;
|
||||
if (existingTasks.length > 0) {
|
||||
nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
|
||||
report(
|
||||
`Found ${existingTasks.length} existing tasks. Next ID will be ${nextId}.`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
report(
|
||||
`Could not read existing tasks from ${tasksPath} or format is invalid. Proceeding without appending.`,
|
||||
'warn'
|
||||
);
|
||||
existingTasks = []; // Reset if read fails
|
||||
}
|
||||
} else if (!force) {
|
||||
// Not appending and not forcing overwrite
|
||||
// Not appending and not forcing overwrite, and there are existing tasks in the target tag
|
||||
const overwriteError = new Error(
|
||||
`Output file ${tasksPath} already exists. Use --force to overwrite or --append.`
|
||||
`Tag '${targetTag}' already contains ${existingTasks.length} tasks. Use --force to overwrite or --append to add to existing tasks.`
|
||||
);
|
||||
report(overwriteError.message, 'error');
|
||||
if (outputFormat === 'text') {
|
||||
@@ -141,10 +154,16 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
} else {
|
||||
// Force overwrite is true
|
||||
report(
|
||||
`Force flag enabled. Overwriting existing file: ${tasksPath}`,
|
||||
`Force flag enabled. Overwriting existing tasks in tag '${targetTag}'.`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No existing tasks in target tag, proceed without confirmation
|
||||
report(
|
||||
`Tag '${targetTag}' is empty or doesn't exist. Creating/updating tag with new tasks.`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
report(`Reading PRD content from ${prdPath}`, 'info');
|
||||
@@ -314,25 +333,36 @@ Guidelines:
|
||||
? [...existingTasks, ...processedNewTasks]
|
||||
: processedNewTasks;
|
||||
|
||||
// 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'
|
||||
}
|
||||
// Read the existing file to preserve other tags
|
||||
let outputData = {};
|
||||
if (fs.existsSync(tasksPath)) {
|
||||
try {
|
||||
const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
|
||||
outputData = JSON.parse(existingFileContent);
|
||||
} catch (error) {
|
||||
// If we can't read the existing file, start with empty object
|
||||
outputData = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Update only the target tag, preserving other tags
|
||||
outputData[targetTag] = {
|
||||
tasks: finalTasks,
|
||||
metadata: {
|
||||
created:
|
||||
outputData[targetTag]?.metadata?.created || new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: `Tasks for ${targetTag} context`
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the master tag has proper metadata
|
||||
ensureTagMetadata(outputData.master, {
|
||||
description: 'Tasks for master context'
|
||||
// Ensure the target tag has proper metadata
|
||||
ensureTagMetadata(outputData[targetTag], {
|
||||
description: `Tasks for ${targetTag} context`
|
||||
});
|
||||
|
||||
// Write the final tasks to the file
|
||||
writeJSON(tasksPath, outputData);
|
||||
// Write the complete data structure back to the file
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(outputData, null, 2));
|
||||
report(
|
||||
`Successfully ${append ? 'appended' : 'generated'} ${processedNewTasks.length} tasks in ${tasksPath}${research ? ' with research-backed analysis' : ''}`,
|
||||
'success'
|
||||
|
||||
Reference in New Issue
Block a user