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

@@ -0,0 +1,124 @@
/**
* scope-down.js
* Direct function implementation for scoping down task complexity
*/
import { scopeDownTask } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for scoping down task complexity with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.id - Comma-separated list of task IDs to scope down
* @param {string} [args.strength='regular'] - Strength level (light, regular, heavy)
* @param {string} [args.prompt] - Custom prompt for scoping adjustments
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {boolean} [args.research=false] - Whether to use research capabilities for scoping
* @param {string} args.projectRoot - Project root path
* @param {string} [args.tag] - Tag for the task context (optional)
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function scopeDownDirect(args, log, context = {}) {
// Destructure expected args
const {
tasksJsonPath,
id,
strength = 'regular',
prompt: customPrompt,
research = false,
projectRoot,
tag
} = args;
const { session } = context; // Destructure session from context
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('scopeDownDirect called without tasksJsonPath');
disableSilentMode(); // Disable before returning
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!id) {
log.error('Missing required parameter: id');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'The id parameter is required for scoping down tasks'
}
};
}
// Parse task IDs
const taskIds = id.split(',').map((taskId) => taskId.trim());
log.info(
`Scoping down tasks: ${taskIds.join(', ')}, strength: ${strength}, research: ${research}`
);
// Call the scopeDownTask function
const result = await scopeDownTask(
tasksJsonPath,
taskIds,
strength,
customPrompt,
{
session,
mcpLog,
projectRoot,
commandName: 'scope-down',
outputType: 'mcp',
tag
},
'json', // outputFormat
research
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
updatedTasks: result.updatedTasks,
tasksUpdated: result.updatedTasks.length,
message: `Successfully scoped down ${result.updatedTasks.length} task(s)`,
telemetryData: result.telemetryData
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in scopeDownDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'SCOPE_DOWN_ERROR',
message: error.message
}
};
}
}

View File

@@ -0,0 +1,124 @@
/**
* scope-up.js
* Direct function implementation for scoping up task complexity
*/
import { scopeUpTask } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for scoping up task complexity with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.id - Comma-separated list of task IDs to scope up
* @param {string} [args.strength='regular'] - Strength level (light, regular, heavy)
* @param {string} [args.prompt] - Custom prompt for scoping adjustments
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {boolean} [args.research=false] - Whether to use research capabilities for scoping
* @param {string} args.projectRoot - Project root path
* @param {string} [args.tag] - Tag for the task context (optional)
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function scopeUpDirect(args, log, context = {}) {
// Destructure expected args
const {
tasksJsonPath,
id,
strength = 'regular',
prompt: customPrompt,
research = false,
projectRoot,
tag
} = args;
const { session } = context; // Destructure session from context
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('scopeUpDirect called without tasksJsonPath');
disableSilentMode(); // Disable before returning
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!id) {
log.error('Missing required parameter: id');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'The id parameter is required for scoping up tasks'
}
};
}
// Parse task IDs
const taskIds = id.split(',').map((taskId) => taskId.trim());
log.info(
`Scoping up tasks: ${taskIds.join(', ')}, strength: ${strength}, research: ${research}`
);
// Call the scopeUpTask function
const result = await scopeUpTask(
tasksJsonPath,
taskIds,
strength,
customPrompt,
{
session,
mcpLog,
projectRoot,
commandName: 'scope-up',
outputType: 'mcp',
tag
},
'json', // outputFormat
research
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
updatedTasks: result.updatedTasks,
tasksUpdated: result.updatedTasks.length,
message: `Successfully scoped up ${result.updatedTasks.length} task(s)`,
telemetryData: result.telemetryData
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in scopeUpDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'SCOPE_UP_ERROR',
message: error.message
}
};
}
}

View File

@@ -38,6 +38,8 @@ import { listTagsDirect } from './direct-functions/list-tags.js';
import { useTagDirect } from './direct-functions/use-tag.js';
import { renameTagDirect } from './direct-functions/rename-tag.js';
import { copyTagDirect } from './direct-functions/copy-tag.js';
import { scopeUpDirect } from './direct-functions/scope-up.js';
import { scopeDownDirect } from './direct-functions/scope-down.js';
// Re-export utility functions
export { findTasksPath } from './utils/path-utils.js';
@@ -76,7 +78,9 @@ export const directFunctions = new Map([
['listTagsDirect', listTagsDirect],
['useTagDirect', useTagDirect],
['renameTagDirect', renameTagDirect],
['copyTagDirect', copyTagDirect]
['copyTagDirect', copyTagDirect],
['scopeUpDirect', scopeUpDirect],
['scopeDownDirect', scopeDownDirect]
]);
// Re-export all direct function implementations
@@ -113,5 +117,7 @@ export {
listTagsDirect,
useTagDirect,
renameTagDirect,
copyTagDirect
copyTagDirect,
scopeUpDirect,
scopeDownDirect
};

View File

@@ -38,6 +38,8 @@ import { registerRenameTagTool } from './rename-tag.js';
import { registerCopyTagTool } from './copy-tag.js';
import { registerResearchTool } from './research.js';
import { registerRulesTool } from './rules.js';
import { registerScopeUpTool } from './scope-up.js';
import { registerScopeDownTool } from './scope-down.js';
/**
* Register all Task Master tools with the MCP server
@@ -57,6 +59,8 @@ export function registerTaskMasterTools(server) {
registerAnalyzeProjectComplexityTool(server);
registerExpandTaskTool(server);
registerExpandAllTool(server);
registerScopeUpTool(server);
registerScopeDownTool(server);
// Group 3: Task Listing & Viewing
registerListTasksTool(server);

View File

@@ -0,0 +1,104 @@
/**
* tools/scope-down.js
* Tool to scope down task complexity
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { scopeDownDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the scopeDown tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerScopeDownTool(server) {
server.addTool({
name: 'scope_down_task',
description: 'Decrease the complexity of one or more tasks using AI',
parameters: z.object({
id: z
.string()
.describe(
'Comma-separated list of task IDs to scope down (e.g., "1,3,5")'
),
strength: z
.string()
.optional()
.describe(
'Strength level: light, regular, or heavy (default: regular)'
),
prompt: z
.string()
.optional()
.describe('Custom prompt for specific scoping adjustments'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on'),
research: z
.boolean()
.optional()
.describe('Whether to use research capabilities for scoping')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting scope-down with args: ${JSON.stringify(args)}`);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await scopeDownDirect(
{
tasksJsonPath: tasksJsonPath,
id: args.id,
strength: args.strength,
prompt: args.prompt,
research: args.research,
projectRoot: args.projectRoot,
tag: resolvedTag
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error scoping down task',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in scope-down tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -0,0 +1,104 @@
/**
* tools/scope-up.js
* Tool to scope up task complexity
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { scopeUpDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the scopeUp tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerScopeUpTool(server) {
server.addTool({
name: 'scope_up_task',
description: 'Increase the complexity of one or more tasks using AI',
parameters: z.object({
id: z
.string()
.describe(
'Comma-separated list of task IDs to scope up (e.g., "1,3,5")'
),
strength: z
.string()
.optional()
.describe(
'Strength level: light, regular, or heavy (default: regular)'
),
prompt: z
.string()
.optional()
.describe('Custom prompt for specific scoping adjustments'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on'),
research: z
.boolean()
.optional()
.describe('Whether to use research capabilities for scoping')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting scope-up with args: ${JSON.stringify(args)}`);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await scopeUpDirect(
{
tasksJsonPath: tasksJsonPath,
id: args.id,
strength: args.strength,
prompt: args.prompt,
research: args.research,
projectRoot: args.projectRoot,
tag: resolvedTag
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error scoping up task',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in scope-up tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}