mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-11-09 06:17:23 +00:00
846 lines
25 KiB
JavaScript
846 lines
25 KiB
JavaScript
/**
|
|
* 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`
|
|
);
|
|
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 (e.g., 1, 2, 3 - NOT "1", "2", "3")
|
|
- 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:
|
|
- The 'id' field must be a NUMBER, not a string!
|
|
- 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(),
|
|
testStrategy: z.string()
|
|
})
|
|
)
|
|
});
|
|
|
|
const aiResult = await generateObjectService({
|
|
role: context.research ? 'research' : '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 || [];
|
|
|
|
// Post-process generated subtasks to ensure defaults
|
|
const processedGeneratedSubtasks = generatedSubtasks.map((subtask) => ({
|
|
...subtask,
|
|
status: subtask.status || 'pending',
|
|
testStrategy: subtask.testStrategy || ''
|
|
}));
|
|
|
|
// Update task with preserved subtasks + newly generated ones
|
|
task.subtasks = [...preservedSubtasks, ...processedGeneratedSubtasks];
|
|
|
|
return {
|
|
updatedTask: task,
|
|
regenerated: true,
|
|
preserved: preservedSubtasks.length,
|
|
generated: processedGeneratedSubtasks.length
|
|
};
|
|
} catch (error) {
|
|
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
|
|
- priority: Task priority ('low', 'medium', or 'high')
|
|
|
|
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']).describe('Task priority level')
|
|
});
|
|
|
|
const aiResult = await generateObjectService({
|
|
role: context.research ? 'research' : 'main',
|
|
session: context.session,
|
|
systemPrompt,
|
|
prompt,
|
|
schema: taskSchema,
|
|
objectName: 'updated_task',
|
|
commandName: context.commandName || `scope-${direction}`,
|
|
outputType: context.outputType || 'cli'
|
|
});
|
|
|
|
const updatedTaskData = aiResult.mainResult;
|
|
|
|
// Ensure priority has a value (in case AI didn't provide one)
|
|
const processedTaskData = {
|
|
...updatedTaskData,
|
|
priority: updatedTaskData.priority || task.priority || 'medium'
|
|
};
|
|
|
|
return {
|
|
updatedTask: {
|
|
...task,
|
|
...processedTaskData
|
|
},
|
|
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') {
|
|
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 ? '↘️' : '➡️';
|
|
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') {
|
|
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 ? '↘️' : '➡️';
|
|
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
|
|
};
|
|
}
|