Task 104: Implement 'scope-up' and 'scope-down' CLI Commands for Dynamic Task Complexity Adjustment (#1069)

* feat(task-104): Complete task 104 - Implement scope-up and scope-down CLI Commands

- Added new CLI commands 'scope-up' and 'scope-down' with comma-separated ID support
- Implemented strength levels (light/regular/heavy) and custom prompt functionality
- Created core complexity adjustment logic with AI integration
- Added MCP tool equivalents for integrated environments
- Comprehensive error handling and task validation
- Full test coverage with TDD approach
- Updated task manager core and UI components

Task 104: Implement 'scope-up' and 'scope-down' CLI Commands for Dynamic Task Complexity Adjustment - Complete implementation with CLI, MCP integration, and testing

* chore: Add changeset for scope-up and scope-down features

- Comprehensive user-facing description with usage examples
- Key features and benefits explanation
- CLI and MCP integration details
- Real-world use cases for agile workflows

* feat(extension): Add scope-up and scope-down to VS Code extension task details

- Added useScopeUpTask and useScopeDownTask hooks in useTaskQueries.ts
- Enhanced AIActionsSection with Task Complexity Adjustment section
- Added strength selection (light/regular/heavy) and custom prompt support
- Integrated scope buttons with proper loading states and error handling
- Uses existing mcpRequest handler for scope_up_task and scope_down_task tools
- Maintains consistent UI patterns with existing AI actions

Extension now supports dynamic task complexity adjustment directly from task details view.
This commit is contained in:
Eyal Toledano
2025-08-02 19:43:04 +03:00
committed by GitHub
parent 64302dc191
commit 72ca68edeb
21 changed files with 3402 additions and 553 deletions

View File

@@ -42,7 +42,10 @@ import {
taskExists,
moveTask,
migrateProject,
setResponseLanguage
setResponseLanguage,
scopeUpTask,
scopeDownTask,
validateStrength
} from './task-manager.js';
import {
@@ -1386,6 +1389,258 @@ function registerCommands(programInstance) {
}
});
// scope-up command
programInstance
.command('scope-up')
.description('Increase task complexity with AI assistance')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-i, --id <ids>',
'Comma-separated task/subtask IDs to scope up (required)'
)
.option(
'-s, --strength <level>',
'Complexity increase strength: light, regular, heavy',
'regular'
)
.option(
'-p, --prompt <text>',
'Custom instructions for targeted scope adjustments'
)
.option('-r, --research', 'Use research AI for more informed adjustments')
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
try {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true,
tag: options.tag
});
const tasksPath = taskMaster.getTasksPath();
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
// Validate required parameters
if (!options.id) {
console.error(chalk.red('Error: --id parameter is required'));
console.log(
chalk.yellow(
'Usage example: task-master scope-up --id=1,2,3 --strength=regular'
)
);
process.exit(1);
}
// Parse and validate task IDs
const taskIds = options.id.split(',').map((id) => {
const parsed = parseInt(id.trim(), 10);
if (Number.isNaN(parsed) || parsed <= 0) {
console.error(chalk.red(`Error: Invalid task ID: ${id.trim()}`));
process.exit(1);
}
return parsed;
});
// Validate strength level
if (!validateStrength(options.strength)) {
console.error(
chalk.red(
`Error: Invalid strength level: ${options.strength}. Must be one of: light, regular, heavy`
)
);
process.exit(1);
}
// Validate tasks file exists
if (!fs.existsSync(tasksPath)) {
console.error(
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
);
process.exit(1);
}
console.log(
chalk.blue(
`Scoping up ${taskIds.length} task(s): ${taskIds.join(', ')}`
)
);
console.log(chalk.blue(`Strength level: ${options.strength}`));
if (options.prompt) {
console.log(chalk.blue(`Custom instructions: ${options.prompt}`));
}
const context = {
projectRoot: taskMaster.getProjectRoot(),
tag,
commandName: 'scope-up',
outputType: 'cli'
};
const result = await scopeUpTask(
tasksPath,
taskIds,
options.strength,
options.prompt || null,
context,
'text'
);
console.log(
chalk.green(
`✅ Successfully scoped up ${result.updatedTasks.length} task(s)`
)
);
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
if (error.message.includes('not found')) {
console.log(chalk.yellow('\nTo fix this issue:'));
console.log(
' 1. Run task-master list to see all available task IDs'
);
console.log(' 2. Use valid task IDs with the --id parameter');
}
if (getDebugFlag()) {
console.error(error);
}
process.exit(1);
}
});
// scope-down command
programInstance
.command('scope-down')
.description('Decrease task complexity with AI assistance')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-i, --id <ids>',
'Comma-separated task/subtask IDs to scope down (required)'
)
.option(
'-s, --strength <level>',
'Complexity decrease strength: light, regular, heavy',
'regular'
)
.option(
'-p, --prompt <text>',
'Custom instructions for targeted scope adjustments'
)
.option('-r, --research', 'Use research AI for more informed adjustments')
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
try {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true,
tag: options.tag
});
const tasksPath = taskMaster.getTasksPath();
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
// Validate required parameters
if (!options.id) {
console.error(chalk.red('Error: --id parameter is required'));
console.log(
chalk.yellow(
'Usage example: task-master scope-down --id=1,2,3 --strength=regular'
)
);
process.exit(1);
}
// Parse and validate task IDs
const taskIds = options.id.split(',').map((id) => {
const parsed = parseInt(id.trim(), 10);
if (Number.isNaN(parsed) || parsed <= 0) {
console.error(chalk.red(`Error: Invalid task ID: ${id.trim()}`));
process.exit(1);
}
return parsed;
});
// Validate strength level
if (!validateStrength(options.strength)) {
console.error(
chalk.red(
`Error: Invalid strength level: ${options.strength}. Must be one of: light, regular, heavy`
)
);
process.exit(1);
}
// Validate tasks file exists
if (!fs.existsSync(tasksPath)) {
console.error(
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
);
process.exit(1);
}
console.log(
chalk.blue(
`Scoping down ${taskIds.length} task(s): ${taskIds.join(', ')}`
)
);
console.log(chalk.blue(`Strength level: ${options.strength}`));
if (options.prompt) {
console.log(chalk.blue(`Custom instructions: ${options.prompt}`));
}
const context = {
projectRoot: taskMaster.getProjectRoot(),
tag,
commandName: 'scope-down',
outputType: 'cli'
};
const result = await scopeDownTask(
tasksPath,
taskIds,
options.strength,
options.prompt || null,
context,
'text'
);
console.log(
chalk.green(
`✅ Successfully scoped down ${result.updatedTasks.length} task(s)`
)
);
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
if (error.message.includes('not found')) {
console.log(chalk.yellow('\nTo fix this issue:'));
console.log(
' 1. Run task-master list to see all available task IDs'
);
console.log(' 2. Use valid task IDs with the --id parameter');
}
if (getDebugFlag()) {
console.error(error);
}
process.exit(1);
}
});
// generate command
programInstance
.command('generate')

View File

@@ -28,6 +28,11 @@ import moveTask from './task-manager/move-task.js';
import { migrateProject } from './task-manager/migrate.js';
import { performResearch } from './task-manager/research.js';
import { readComplexityReport } from './utils.js';
import {
scopeUpTask,
scopeDownTask,
validateStrength
} from './task-manager/scope-adjustment.js';
// Export task manager functions
export {
@@ -55,5 +60,8 @@ export {
moveTask,
readComplexityReport,
migrateProject,
performResearch
performResearch,
scopeUpTask,
scopeDownTask,
validateStrength
};

View File

@@ -0,0 +1,828 @@
/**
* scope-adjustment.js
* Core logic for dynamic task complexity adjustment (scope-up and scope-down)
*/
import { z } from 'zod';
import {
log,
readJSON,
writeJSON,
getCurrentTag,
readComplexityReport,
findTaskInComplexityReport
} from '../utils.js';
import {
generateObjectService,
generateTextService
} from '../ai-services-unified.js';
import { findTaskById, taskExists } from '../task-manager.js';
import analyzeTaskComplexity from './analyze-task-complexity.js';
import { findComplexityReportPath } from '../../../src/utils/path-utils.js';
/**
* Valid strength levels for scope adjustments
*/
const VALID_STRENGTHS = ['light', 'regular', 'heavy'];
/**
* Statuses that should be preserved during subtask regeneration
* These represent work that has been started or intentionally set by the user
*/
const PRESERVE_STATUSES = [
'done',
'in-progress',
'review',
'cancelled',
'deferred',
'blocked'
];
/**
* Statuses that should be regenerated during subtask regeneration
* These represent work that hasn't been started yet
*/
const REGENERATE_STATUSES = ['pending'];
/**
* Validates strength parameter
* @param {string} strength - The strength level to validate
* @returns {boolean} True if valid, false otherwise
*/
export function validateStrength(strength) {
return VALID_STRENGTHS.includes(strength);
}
/**
* Re-analyzes the complexity of a single task after scope adjustment
* @param {Object} task - The task to analyze
* @param {string} tasksPath - Path to tasks.json
* @param {Object} context - Context containing projectRoot, tag, session
* @returns {Promise<number|null>} New complexity score or null if analysis failed
*/
async function reanalyzeTaskComplexity(task, tasksPath, context) {
const { projectRoot, tag, session } = context;
try {
// Create a minimal tasks data structure for analysis
const tasksForAnalysis = {
tasks: [task],
metadata: { analyzedAt: new Date().toISOString() }
};
// Find the complexity report path for this tag
const complexityReportPath = findComplexityReportPath(
null,
{ projectRoot, tag },
null
);
if (!complexityReportPath) {
log('warn', 'No complexity report found - cannot re-analyze complexity');
return null;
}
// Use analyze-task-complexity to re-analyze just this task
const analysisOptions = {
file: tasksPath,
output: complexityReportPath,
id: task.id.toString(), // Analyze only this specific task
projectRoot,
tag,
_filteredTasksData: tasksForAnalysis, // Pass pre-filtered data
_originalTaskCount: 1
};
// Run the analysis with proper context
await analyzeTaskComplexity(analysisOptions, { session });
// Read the updated complexity report to get the new score
const updatedReport = readComplexityReport(complexityReportPath);
if (updatedReport) {
const taskAnalysis = findTaskInComplexityReport(updatedReport, task.id);
if (taskAnalysis) {
log(
'info',
`Re-analyzed task ${task.id} complexity: ${taskAnalysis.complexityScore}/10`
);
return taskAnalysis.complexityScore;
}
}
log(
'warn',
`Could not find updated complexity analysis for task ${task.id}`
);
return null;
} catch (error) {
log('error', `Failed to re-analyze task complexity: ${error.message}`);
return null;
}
}
/**
* Gets the current complexity score for a task from the complexity report
* @param {number} taskId - Task ID to look up
* @param {Object} context - Context containing projectRoot, tag
* @returns {number|null} Current complexity score or null if not found
*/
function getCurrentComplexityScore(taskId, context) {
const { projectRoot, tag } = context;
try {
// Find the complexity report path for this tag
const complexityReportPath = findComplexityReportPath(
null,
{ projectRoot, tag },
null
);
if (!complexityReportPath) {
return null;
}
// Read the current complexity report
const complexityReport = readComplexityReport(complexityReportPath);
if (!complexityReport) {
return null;
}
// Find this task's current complexity
const taskAnalysis = findTaskInComplexityReport(complexityReport, taskId);
return taskAnalysis ? taskAnalysis.complexityScore : null;
} catch (error) {
log('debug', `Could not read current complexity score: ${error.message}`);
return null;
}
}
/**
* Regenerates subtasks for a task based on new complexity while preserving completed work
* @param {Object} task - The updated task object
* @param {string} tasksPath - Path to tasks.json
* @param {Object} context - Context containing projectRoot, tag, session
* @param {string} direction - Direction of scope change (up/down) for logging
* @param {string} strength - Strength level ('light', 'regular', 'heavy')
* @param {number|null} originalComplexity - Original complexity score for smarter adjustments
* @returns {Promise<Object>} Object with updated task and regeneration info
*/
async function regenerateSubtasksForComplexity(
task,
tasksPath,
context,
direction,
strength = 'regular',
originalComplexity = null
) {
const { projectRoot, tag, session } = context;
// Check if task has subtasks
if (
!task.subtasks ||
!Array.isArray(task.subtasks) ||
task.subtasks.length === 0
) {
return {
updatedTask: task,
regenerated: false,
preserved: 0,
generated: 0
};
}
// Identify subtasks to preserve vs regenerate
const preservedSubtasks = task.subtasks.filter((subtask) =>
PRESERVE_STATUSES.includes(subtask.status)
);
const pendingSubtasks = task.subtasks.filter((subtask) =>
REGENERATE_STATUSES.includes(subtask.status)
);
// If no pending subtasks, nothing to regenerate
if (pendingSubtasks.length === 0) {
return {
updatedTask: task,
regenerated: false,
preserved: preservedSubtasks.length,
generated: 0
};
}
// Calculate appropriate number of total subtasks based on direction, complexity, strength, and original complexity
let targetSubtaskCount;
const preservedCount = preservedSubtasks.length;
const currentPendingCount = pendingSubtasks.length;
// Use original complexity to inform decisions (if available)
const complexityFactor = originalComplexity
? Math.max(0.5, originalComplexity / 10)
: 1.0;
const complexityInfo = originalComplexity
? ` (original complexity: ${originalComplexity}/10)`
: '';
if (direction === 'up') {
// Scope up: More subtasks for increased complexity
if (strength === 'light') {
const base = Math.max(
5,
preservedCount + Math.ceil(currentPendingCount * 1.1)
);
targetSubtaskCount = Math.ceil(base * (0.8 + 0.4 * complexityFactor));
} else if (strength === 'regular') {
const base = Math.max(
6,
preservedCount + Math.ceil(currentPendingCount * 1.3)
);
targetSubtaskCount = Math.ceil(base * (0.8 + 0.4 * complexityFactor));
} else {
// heavy
const base = Math.max(
8,
preservedCount + Math.ceil(currentPendingCount * 1.6)
);
targetSubtaskCount = Math.ceil(base * (0.8 + 0.6 * complexityFactor));
}
} else {
// Scope down: Fewer subtasks for decreased complexity
// High complexity tasks get reduced more aggressively
const aggressiveFactor =
originalComplexity >= 8 ? 0.7 : originalComplexity >= 6 ? 0.85 : 1.0;
if (strength === 'light') {
const base = Math.max(
3,
preservedCount + Math.ceil(currentPendingCount * 0.8)
);
targetSubtaskCount = Math.ceil(base * aggressiveFactor);
} else if (strength === 'regular') {
const base = Math.max(
3,
preservedCount + Math.ceil(currentPendingCount * 0.5)
);
targetSubtaskCount = Math.ceil(base * aggressiveFactor);
} else {
// heavy
// Heavy scope-down should be much more aggressive - aim for only core functionality
// Very high complexity tasks (9-10) get reduced to almost nothing
const ultraAggressiveFactor =
originalComplexity >= 9 ? 0.3 : originalComplexity >= 7 ? 0.5 : 0.7;
const base = Math.max(
2,
preservedCount + Math.ceil(currentPendingCount * 0.25)
);
targetSubtaskCount = Math.max(1, Math.ceil(base * ultraAggressiveFactor));
}
}
log(
'debug',
`Complexity-aware subtask calculation${complexityInfo}: ${currentPendingCount} pending -> target ${targetSubtaskCount} total`
);
console.log(
`[DEBUG] Complexity-aware calculation${complexityInfo}: ${currentPendingCount} pending -> ${targetSubtaskCount} total subtasks (${strength} ${direction})`
);
const newSubtasksNeeded = Math.max(1, targetSubtaskCount - preservedCount);
try {
// Generate new subtasks using AI to match the new complexity level
const systemPrompt = `You are an expert project manager who creates task breakdowns that match complexity levels.`;
const prompt = `Based on this updated task, generate ${newSubtasksNeeded} NEW subtasks that reflect the ${direction === 'up' ? 'increased' : 'decreased'} complexity level:
**Task Title**: ${task.title}
**Task Description**: ${task.description}
**Implementation Details**: ${task.details}
**Test Strategy**: ${task.testStrategy}
**Complexity Direction**: This task was recently scoped ${direction} (${strength} strength) to ${direction === 'up' ? 'increase' : 'decrease'} complexity.
${originalComplexity ? `**Original Complexity**: ${originalComplexity}/10 - consider this when determining appropriate scope level.` : ''}
${preservedCount > 0 ? `**Preserved Subtasks**: ${preservedCount} existing subtasks with work already done will be kept.` : ''}
Generate subtasks that:
${
direction === 'up'
? strength === 'heavy'
? `- Add comprehensive implementation steps with advanced features
- Include extensive error handling, validation, and edge cases
- Cover multiple integration scenarios and advanced testing
- Provide thorough documentation and optimization approaches`
: strength === 'regular'
? `- Add more detailed implementation steps
- Include additional error handling and validation
- Cover more edge cases and advanced features
- Provide more comprehensive testing approaches`
: `- Add some additional implementation details
- Include basic error handling considerations
- Cover a few common edge cases
- Enhance testing approaches slightly`
: strength === 'heavy'
? `- Focus ONLY on absolutely essential core functionality
- Strip out ALL non-critical features (error handling, advanced testing, etc.)
- Provide only the minimum viable implementation
- Eliminate any complex integrations or advanced scenarios
- Aim for the simplest possible working solution`
: strength === 'regular'
? `- Focus on core functionality only
- Simplify implementation steps
- Remove non-essential features
- Streamline to basic requirements`
: `- Focus mainly on core functionality
- Slightly simplify implementation steps
- Remove some non-essential features
- Streamline most requirements`
}
Return a JSON object with a "subtasks" array. Each subtask should have:
- id: Sequential number starting from 1
- title: Clear, specific title
- description: Detailed description
- dependencies: Array of dependency IDs as STRINGS (use format ["${task.id}.1", "${task.id}.2"] for siblings, or empty array [] for no dependencies)
- details: Implementation guidance
- status: "pending"
- testStrategy: Testing approach
IMPORTANT: Dependencies must be strings, not numbers!
Ensure the JSON is valid and properly formatted.`;
// Define subtask schema
const subtaskSchema = z.object({
subtasks: z.array(
z.object({
id: z.number().int().positive(),
title: z.string().min(5),
description: z.string().min(10),
dependencies: z.array(z.string()),
details: z.string().min(20),
status: z.string().default('pending'),
testStrategy: z.string().nullable().default('')
})
)
});
const aiResult = await generateObjectService({
role: 'main',
session: context.session,
systemPrompt,
prompt,
schema: subtaskSchema,
objectName: 'subtask_regeneration',
commandName: context.commandName || `subtask-regen-${direction}`,
outputType: context.outputType || 'cli'
});
const generatedSubtasks = aiResult.mainResult.subtasks || [];
// Update task with preserved subtasks + newly generated ones
task.subtasks = [...preservedSubtasks, ...generatedSubtasks];
return {
updatedTask: task,
regenerated: true,
preserved: preservedSubtasks.length,
generated: generatedSubtasks.length
};
} catch (error) {
console.log(
`[WARN] Failed to regenerate subtasks for task ${task.id}: ${error.message}`
);
// Don't fail the whole operation if subtask regeneration fails
return {
updatedTask: task,
regenerated: false,
preserved: preservedSubtasks.length,
generated: 0,
error: error.message
};
}
}
/**
* Generates AI prompt for scope adjustment
* @param {Object} task - The task to adjust
* @param {string} direction - 'up' or 'down'
* @param {string} strength - 'light', 'regular', or 'heavy'
* @param {string} customPrompt - Optional custom instructions
* @returns {string} The generated prompt
*/
function generateScopePrompt(task, direction, strength, customPrompt) {
const isUp = direction === 'up';
const strengthDescriptions = {
light: isUp ? 'minor enhancements' : 'slight simplifications',
regular: isUp
? 'moderate complexity increases'
: 'moderate simplifications',
heavy: isUp ? 'significant complexity additions' : 'major simplifications'
};
let basePrompt = `You are tasked with adjusting the complexity of a task.
CURRENT TASK:
Title: ${task.title}
Description: ${task.description}
Details: ${task.details}
Test Strategy: ${task.testStrategy || 'Not specified'}
ADJUSTMENT REQUIREMENTS:
- Direction: ${isUp ? 'INCREASE' : 'DECREASE'} complexity
- Strength: ${strength} (${strengthDescriptions[strength]})
- Preserve the core purpose and functionality of the task
- Maintain consistency with the existing task structure`;
if (isUp) {
basePrompt += `
- Add more detailed requirements, edge cases, or advanced features
- Include additional implementation considerations
- Enhance error handling and validation requirements
- Expand testing strategies with more comprehensive scenarios`;
} else {
basePrompt += `
- Focus on core functionality and essential requirements
- Remove or simplify non-essential features
- Streamline implementation details
- Simplify testing to focus on basic functionality`;
}
if (customPrompt) {
basePrompt += `\n\nCUSTOM INSTRUCTIONS:\n${customPrompt}`;
}
basePrompt += `\n\nReturn a JSON object with the updated task containing these fields:
- title: Updated task title
- description: Updated task description
- details: Updated implementation details
- testStrategy: Updated test strategy
Ensure the JSON is valid and properly formatted.`;
return basePrompt;
}
/**
* Adjusts task complexity using AI
* @param {Object} task - The task to adjust
* @param {string} direction - 'up' or 'down'
* @param {string} strength - 'light', 'regular', or 'heavy'
* @param {string} customPrompt - Optional custom instructions
* @param {Object} context - Context object with projectRoot, tag, etc.
* @returns {Promise<Object>} Updated task data and telemetry
*/
async function adjustTaskComplexity(
task,
direction,
strength,
customPrompt,
context
) {
const systemPrompt = `You are an expert software project manager who helps adjust task complexity while maintaining clarity and actionability.`;
const prompt = generateScopePrompt(task, direction, strength, customPrompt);
// Define the task schema for structured response using Zod
const taskSchema = z.object({
title: z
.string()
.min(1)
.describe('Updated task title reflecting scope adjustment'),
description: z
.string()
.min(1)
.describe('Updated task description with adjusted scope'),
details: z
.string()
.min(1)
.describe('Updated implementation details with adjusted complexity'),
testStrategy: z
.string()
.min(1)
.describe('Updated testing approach for the adjusted scope'),
priority: z
.enum(['low', 'medium', 'high'])
.optional()
.describe('Task priority level')
});
const aiResult = await generateObjectService({
role: 'main',
session: context.session,
systemPrompt,
prompt,
schema: taskSchema,
objectName: 'updated_task',
commandName: context.commandName || `scope-${direction}`,
outputType: context.outputType || 'cli'
});
const updatedTaskData = aiResult.mainResult;
return {
updatedTask: {
...task,
...updatedTaskData
},
telemetryData: aiResult.telemetryData
};
}
/**
* Increases task complexity (scope-up)
* @param {string} tasksPath - Path to tasks.json file
* @param {Array<number>} taskIds - Array of task IDs to scope up
* @param {string} strength - Strength level ('light', 'regular', 'heavy')
* @param {string} customPrompt - Optional custom instructions
* @param {Object} context - Context object with projectRoot, tag, etc.
* @param {string} outputFormat - Output format ('text' or 'json')
* @returns {Promise<Object>} Results of the scope-up operation
*/
export async function scopeUpTask(
tasksPath,
taskIds,
strength = 'regular',
customPrompt = null,
context = {},
outputFormat = 'text'
) {
// Validate inputs
if (!validateStrength(strength)) {
throw new Error(
`Invalid strength level: ${strength}. Must be one of: ${VALID_STRENGTHS.join(', ')}`
);
}
const { projectRoot = '.', tag = 'master' } = context;
// Read tasks data
const data = readJSON(tasksPath, projectRoot, tag);
const tasks = data?.tasks || [];
// Validate all task IDs exist
for (const taskId of taskIds) {
if (!taskExists(tasks, taskId)) {
throw new Error(`Task with ID ${taskId} not found`);
}
}
const updatedTasks = [];
let combinedTelemetryData = null;
// Process each task
for (const taskId of taskIds) {
const taskResult = findTaskById(tasks, taskId);
const task = taskResult.task;
if (!task) {
throw new Error(`Task with ID ${taskId} not found`);
}
if (outputFormat === 'text') {
log('info', `Scoping up task ${taskId}: ${task.title}`);
}
// Get original complexity score (if available)
const originalComplexity = getCurrentComplexityScore(taskId, context);
if (originalComplexity && outputFormat === 'text') {
console.log(`[INFO] Original complexity: ${originalComplexity}/10`);
}
const adjustResult = await adjustTaskComplexity(
task,
'up',
strength,
customPrompt,
context
);
// Regenerate subtasks based on new complexity while preserving completed work
const subtaskResult = await regenerateSubtasksForComplexity(
adjustResult.updatedTask,
tasksPath,
context,
'up',
strength,
originalComplexity
);
// Log subtask regeneration info if in text mode
if (outputFormat === 'text' && subtaskResult.regenerated) {
log(
'info',
`Regenerated ${subtaskResult.generated} pending subtasks (preserved ${subtaskResult.preserved} completed)`
);
}
// Update task in data
const taskIndex = data.tasks.findIndex((t) => t.id === taskId);
if (taskIndex !== -1) {
data.tasks[taskIndex] = subtaskResult.updatedTask;
updatedTasks.push(subtaskResult.updatedTask);
}
// Re-analyze complexity after scoping (if we have a session for AI calls)
if (context.session && originalComplexity) {
try {
// Write the updated task first so complexity analysis can read it
writeJSON(tasksPath, data, projectRoot, tag);
// Re-analyze complexity
const newComplexity = await reanalyzeTaskComplexity(
subtaskResult.updatedTask,
tasksPath,
context
);
if (newComplexity && outputFormat === 'text') {
const complexityChange = newComplexity - originalComplexity;
const arrow =
complexityChange > 0 ? '↗️' : complexityChange < 0 ? '↘️' : '➡️';
console.log(
`[INFO] New complexity: ${originalComplexity}/10 ${arrow} ${newComplexity}/10 (${complexityChange > 0 ? '+' : ''}${complexityChange})`
);
}
} catch (error) {
if (outputFormat === 'text') {
log('warn', `Could not re-analyze complexity: ${error.message}`);
}
}
}
// Combine telemetry data
if (adjustResult.telemetryData) {
if (!combinedTelemetryData) {
combinedTelemetryData = { ...adjustResult.telemetryData };
} else {
// Sum up costs and tokens
combinedTelemetryData.inputTokens +=
adjustResult.telemetryData.inputTokens || 0;
combinedTelemetryData.outputTokens +=
adjustResult.telemetryData.outputTokens || 0;
combinedTelemetryData.totalTokens +=
adjustResult.telemetryData.totalTokens || 0;
combinedTelemetryData.totalCost +=
adjustResult.telemetryData.totalCost || 0;
}
}
}
// Write updated data
writeJSON(tasksPath, data, projectRoot, tag);
if (outputFormat === 'text') {
log('info', `Successfully scoped up ${updatedTasks.length} task(s)`);
}
return {
updatedTasks,
telemetryData: combinedTelemetryData
};
}
/**
* Decreases task complexity (scope-down)
* @param {string} tasksPath - Path to tasks.json file
* @param {Array<number>} taskIds - Array of task IDs to scope down
* @param {string} strength - Strength level ('light', 'regular', 'heavy')
* @param {string} customPrompt - Optional custom instructions
* @param {Object} context - Context object with projectRoot, tag, etc.
* @param {string} outputFormat - Output format ('text' or 'json')
* @returns {Promise<Object>} Results of the scope-down operation
*/
export async function scopeDownTask(
tasksPath,
taskIds,
strength = 'regular',
customPrompt = null,
context = {},
outputFormat = 'text'
) {
// Validate inputs
if (!validateStrength(strength)) {
throw new Error(
`Invalid strength level: ${strength}. Must be one of: ${VALID_STRENGTHS.join(', ')}`
);
}
const { projectRoot = '.', tag = 'master' } = context;
// Read tasks data
const data = readJSON(tasksPath, projectRoot, tag);
const tasks = data?.tasks || [];
// Validate all task IDs exist
for (const taskId of taskIds) {
if (!taskExists(tasks, taskId)) {
throw new Error(`Task with ID ${taskId} not found`);
}
}
const updatedTasks = [];
let combinedTelemetryData = null;
// Process each task
for (const taskId of taskIds) {
const taskResult = findTaskById(tasks, taskId);
const task = taskResult.task;
if (!task) {
throw new Error(`Task with ID ${taskId} not found`);
}
if (outputFormat === 'text') {
log('info', `Scoping down task ${taskId}: ${task.title}`);
}
// Get original complexity score (if available)
const originalComplexity = getCurrentComplexityScore(taskId, context);
if (originalComplexity && outputFormat === 'text') {
console.log(`[INFO] Original complexity: ${originalComplexity}/10`);
}
const adjustResult = await adjustTaskComplexity(
task,
'down',
strength,
customPrompt,
context
);
// Regenerate subtasks based on new complexity while preserving completed work
const subtaskResult = await regenerateSubtasksForComplexity(
adjustResult.updatedTask,
tasksPath,
context,
'down',
strength,
originalComplexity
);
// Log subtask regeneration info if in text mode
if (outputFormat === 'text' && subtaskResult.regenerated) {
log(
'info',
`Regenerated ${subtaskResult.generated} pending subtasks (preserved ${subtaskResult.preserved} completed)`
);
}
// Update task in data
const taskIndex = data.tasks.findIndex((t) => t.id === taskId);
if (taskIndex !== -1) {
data.tasks[taskIndex] = subtaskResult.updatedTask;
updatedTasks.push(subtaskResult.updatedTask);
}
// Re-analyze complexity after scoping (if we have a session for AI calls)
if (context.session && originalComplexity) {
try {
// Write the updated task first so complexity analysis can read it
writeJSON(tasksPath, data, projectRoot, tag);
// Re-analyze complexity
const newComplexity = await reanalyzeTaskComplexity(
subtaskResult.updatedTask,
tasksPath,
context
);
if (newComplexity && outputFormat === 'text') {
const complexityChange = newComplexity - originalComplexity;
const arrow =
complexityChange > 0 ? '↗️' : complexityChange < 0 ? '↘️' : '➡️';
console.log(
`[INFO] New complexity: ${originalComplexity}/10 ${arrow} ${newComplexity}/10 (${complexityChange > 0 ? '+' : ''}${complexityChange})`
);
}
} catch (error) {
if (outputFormat === 'text') {
log('warn', `Could not re-analyze complexity: ${error.message}`);
}
}
}
// Combine telemetry data
if (adjustResult.telemetryData) {
if (!combinedTelemetryData) {
combinedTelemetryData = { ...adjustResult.telemetryData };
} else {
// Sum up costs and tokens
combinedTelemetryData.inputTokens +=
adjustResult.telemetryData.inputTokens || 0;
combinedTelemetryData.outputTokens +=
adjustResult.telemetryData.outputTokens || 0;
combinedTelemetryData.totalTokens +=
adjustResult.telemetryData.totalTokens || 0;
combinedTelemetryData.totalCost +=
adjustResult.telemetryData.totalCost || 0;
}
}
}
// Write updated data
writeJSON(tasksPath, data, projectRoot, tag);
if (outputFormat === 'text') {
log('info', `Successfully scoped down ${updatedTasks.length} task(s)`);
}
return {
updatedTasks,
telemetryData: combinedTelemetryData
};
}

View File

@@ -1640,23 +1640,80 @@ async function displayTaskById(
}
// --- Suggested Actions ---
console.log(
boxen(
chalk.white.bold('Suggested Actions:') +
'\n' +
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` +
`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}\n` +
// Determine action 3 based on whether subtasks *exist* (use the source list for progress)
(subtasksForProgress && subtasksForProgress.length > 0
? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}` // Example uses .1
: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`),
{
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
const actions = [];
let actionNumber = 1;
// Basic actions
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}`
);
actionNumber++;
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}`
);
actionNumber++;
// Subtask-related action
if (subtasksForProgress && subtasksForProgress.length > 0) {
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}`
);
} else {
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`
);
}
actionNumber++;
// Complexity-based scope adjustment actions
if (task.complexityScore) {
const complexityScore = task.complexityScore;
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Re-analyze complexity: ${chalk.yellow(`task-master analyze-complexity --id=${task.id}`)}`
);
actionNumber++;
// Add scope adjustment suggestions based on current complexity
if (complexityScore >= 7) {
// High complexity - suggest scoping down
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Scope down (simplify): ${chalk.yellow(`task-master scope-down --id=${task.id} --strength=regular`)}`
);
actionNumber++;
if (complexityScore >= 9) {
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Heavy scope down: ${chalk.yellow(`task-master scope-down --id=${task.id} --strength=heavy`)}`
);
actionNumber++;
}
)
} else if (complexityScore <= 4) {
// Low complexity - suggest scoping up
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Scope up (add detail): ${chalk.yellow(`task-master scope-up --id=${task.id} --strength=regular`)}`
);
actionNumber++;
if (complexityScore <= 2) {
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Heavy scope up: ${chalk.yellow(`task-master scope-up --id=${task.id} --strength=heavy`)}`
);
actionNumber++;
}
} else {
// Medium complexity (5-6) - offer both options
actions.push(
`${chalk.cyan(`${actionNumber}.`)} Scope up/down: ${chalk.yellow(`task-master scope-up --id=${task.id} --strength=light`)} or ${chalk.yellow(`scope-down --id=${task.id} --strength=light`)}`
);
actionNumber++;
}
}
console.log(
boxen(chalk.white.bold('Suggested Actions:') + '\n' + actions.join('\n'), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
})
);
// Show FYI notice if migration occurred