refactor(config): Standardize env var access and config getters
This commit focuses on standardizing configuration and API key access patterns across key modules as part of subtask 61.34. Key changes include: - Refactored `ai-services.js` to remove global AI clients and use `resolveEnvVariable` for API key checks. Client instantiation now relies on `getAnthropicClient`/`getPerplexityClient` accepting a session object. - Refactored `task-manager.js` (`analyzeTaskComplexity` function) to use the unified `generateTextService` from `ai-services-unified.js`, removing direct AI client calls. - Replaced direct `process.env` access for model parameters and other configurations (`PERPLEXITY_MODEL`, `CONFIG.*`) in `task-manager.js` with calls to the appropriate getters from `config-manager.js` (e.g., `getResearchModelId(session)`, `getMainMaxTokens(session)`). - Ensured `utils.js` (`resolveEnvVariable`) correctly handles potentially undefined session objects. - Updated function signatures where necessary to propagate the `session` object for correct context-aware configuration/key retrieval. This moves towards the goal of using `ai-client-factory.js` and `ai-services-unified.js` as the standard pattern for AI interactions and centralizing configuration management through `config-manager.js`.
This commit is contained in:
261
scripts/modules/task-manager/expand-task.js
Normal file
261
scripts/modules/task-manager/expand-task.js
Normal file
@@ -0,0 +1,261 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { log, readJSON, writeJSON, isSilentMode } from '../utils.js';
|
||||
|
||||
import { startLoadingIndicator, stopLoadingIndicator } from '../ui.js';
|
||||
|
||||
import {
|
||||
generateSubtasksWithPerplexity,
|
||||
_handleAnthropicStream,
|
||||
getConfiguredAnthropicClient,
|
||||
parseSubtasksFromText
|
||||
} from '../ai-services.js';
|
||||
|
||||
import { getDefaultSubtasks } from '../config-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
|
||||
/**
|
||||
* Expand a task into subtasks
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {number} taskId - Task ID to expand
|
||||
* @param {number} numSubtasks - Number of subtasks to generate
|
||||
* @param {boolean} useResearch - Whether to use research with Perplexity
|
||||
* @param {string} additionalContext - Additional context
|
||||
* @param {Object} options - Options for expanding tasks
|
||||
* @param {function} options.reportProgress - Function to report progress
|
||||
* @param {Object} options.mcpLog - MCP logger object
|
||||
* @param {Object} options.session - Session object from MCP
|
||||
* @returns {Promise<Object>} Expanded task
|
||||
*/
|
||||
async function expandTask(
|
||||
tasksPath,
|
||||
taskId,
|
||||
numSubtasks,
|
||||
useResearch = false,
|
||||
additionalContext = '',
|
||||
{ reportProgress, mcpLog, session } = {}
|
||||
) {
|
||||
// Determine output format based on mcpLog presence (simplification)
|
||||
const outputFormat = mcpLog ? 'json' : 'text';
|
||||
|
||||
// Create custom reporter that checks for MCP log and silent mode
|
||||
const report = (message, level = 'info') => {
|
||||
if (mcpLog) {
|
||||
mcpLog[level](message);
|
||||
} else if (!isSilentMode() && outputFormat === 'text') {
|
||||
// Only log to console if not in silent mode and outputFormat is 'text'
|
||||
log(level, message);
|
||||
}
|
||||
};
|
||||
|
||||
// Keep the mcpLog check for specific MCP context logging
|
||||
if (mcpLog) {
|
||||
mcpLog.info(
|
||||
`expandTask - reportProgress available: ${!!reportProgress}, session available: ${!!session}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the tasks.json file
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error('Invalid or missing tasks.json');
|
||||
}
|
||||
|
||||
// Find the task
|
||||
const task = data.tasks.find((t) => t.id === parseInt(taskId, 10));
|
||||
if (!task) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
report(`Expanding task ${taskId}: ${task.title}`);
|
||||
|
||||
// If the task already has subtasks and force flag is not set, return the existing subtasks
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
report(`Task ${taskId} already has ${task.subtasks.length} subtasks`);
|
||||
return task;
|
||||
}
|
||||
|
||||
// Determine the number of subtasks to generate
|
||||
let subtaskCount = parseInt(numSubtasks, 10) || getDefaultSubtasks(); // Use getter
|
||||
|
||||
// Check if we have a complexity analysis for this task
|
||||
let taskAnalysis = null;
|
||||
try {
|
||||
const reportPath = 'scripts/task-complexity-report.json';
|
||||
if (fs.existsSync(reportPath)) {
|
||||
const report = readJSON(reportPath);
|
||||
if (report && report.complexityAnalysis) {
|
||||
taskAnalysis = report.complexityAnalysis.find(
|
||||
(a) => a.taskId === task.id
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
report(`Could not read complexity analysis: ${error.message}`, 'warn');
|
||||
}
|
||||
|
||||
// Use recommended subtask count if available
|
||||
if (taskAnalysis) {
|
||||
report(
|
||||
`Found complexity analysis for task ${taskId}: Score ${taskAnalysis.complexityScore}/10`
|
||||
);
|
||||
|
||||
// Use recommended number of subtasks if available
|
||||
if (
|
||||
taskAnalysis.recommendedSubtasks &&
|
||||
subtaskCount === getDefaultSubtasks() // Use getter
|
||||
) {
|
||||
subtaskCount = taskAnalysis.recommendedSubtasks;
|
||||
report(`Using recommended number of subtasks: ${subtaskCount}`);
|
||||
}
|
||||
|
||||
// Use the expansion prompt from analysis as additional context
|
||||
if (taskAnalysis.expansionPrompt && !additionalContext) {
|
||||
additionalContext = taskAnalysis.expansionPrompt;
|
||||
report(`Using expansion prompt from complexity analysis`);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate subtasks with AI
|
||||
let generatedSubtasks = [];
|
||||
|
||||
// Only create loading indicator if not in silent mode and no mcpLog (CLI mode)
|
||||
let loadingIndicator = null;
|
||||
if (!isSilentMode() && !mcpLog) {
|
||||
loadingIndicator = startLoadingIndicator(
|
||||
useResearch
|
||||
? 'Generating research-backed subtasks...'
|
||||
: 'Generating subtasks...'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine the next subtask ID
|
||||
const nextSubtaskId = 1;
|
||||
|
||||
if (useResearch) {
|
||||
// Use Perplexity for research-backed subtasks
|
||||
if (!perplexity) {
|
||||
report(
|
||||
'Perplexity AI is not available. Falling back to Claude AI.',
|
||||
'warn'
|
||||
);
|
||||
useResearch = false;
|
||||
} else {
|
||||
report('Using Perplexity for research-backed subtasks');
|
||||
generatedSubtasks = await generateSubtasksWithPerplexity(
|
||||
task,
|
||||
subtaskCount,
|
||||
nextSubtaskId,
|
||||
additionalContext,
|
||||
{ reportProgress, mcpLog, silentMode: isSilentMode(), session }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!useResearch) {
|
||||
report('Using regular Claude for generating subtasks');
|
||||
|
||||
// Use our getConfiguredAnthropicClient function instead of getAnthropicClient
|
||||
const client = getConfiguredAnthropicClient(session);
|
||||
|
||||
// Build the system prompt
|
||||
const systemPrompt = `You are an AI assistant helping with task breakdown for software development.
|
||||
You need to break down a high-level task into ${subtaskCount} specific subtasks that can be implemented one by one.
|
||||
|
||||
Subtasks should:
|
||||
1. Be specific and actionable implementation steps
|
||||
2. Follow a logical sequence
|
||||
3. Each handle a distinct part of the parent task
|
||||
4. Include clear guidance on implementation approach
|
||||
5. Have appropriate dependency chains between subtasks
|
||||
6. Collectively cover all aspects of the parent task
|
||||
|
||||
For each subtask, provide:
|
||||
- A clear, specific title
|
||||
- Detailed implementation steps
|
||||
- Dependencies on previous subtasks
|
||||
- Testing approach
|
||||
|
||||
Each subtask should be implementable in a focused coding session.`;
|
||||
|
||||
const contextPrompt = additionalContext
|
||||
? `\n\nAdditional context to consider: ${additionalContext}`
|
||||
: '';
|
||||
|
||||
const userPrompt = `Please break down this task into ${subtaskCount} specific, actionable subtasks:
|
||||
|
||||
Task ID: ${task.id}
|
||||
Title: ${task.title}
|
||||
Description: ${task.description}
|
||||
Current details: ${task.details || 'None provided'}
|
||||
${contextPrompt}
|
||||
|
||||
Return exactly ${subtaskCount} subtasks with the following JSON structure:
|
||||
[
|
||||
{
|
||||
"id": ${nextSubtaskId},
|
||||
"title": "First subtask title",
|
||||
"description": "Detailed description",
|
||||
"dependencies": [],
|
||||
"details": "Implementation details"
|
||||
},
|
||||
...more subtasks...
|
||||
]
|
||||
|
||||
Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use an empty array if there are no dependencies.`;
|
||||
|
||||
// Prepare API parameters
|
||||
const apiParams = {
|
||||
model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [{ role: 'user', content: userPrompt }]
|
||||
};
|
||||
|
||||
// Call the streaming API using our helper
|
||||
const responseText = await _handleAnthropicStream(
|
||||
client,
|
||||
apiParams,
|
||||
{ reportProgress, mcpLog, silentMode: isSilentMode() }, // Pass isSilentMode() directly
|
||||
!isSilentMode() // Only use CLI mode if not in silent mode
|
||||
);
|
||||
|
||||
// Parse the subtasks from the response
|
||||
generatedSubtasks = parseSubtasksFromText(
|
||||
responseText,
|
||||
nextSubtaskId,
|
||||
subtaskCount,
|
||||
task.id
|
||||
);
|
||||
}
|
||||
|
||||
// Add the generated subtasks to the task
|
||||
task.subtasks = generatedSubtasks;
|
||||
|
||||
// Write the updated tasks back to the file
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Generate the individual task files
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
|
||||
return task;
|
||||
} catch (error) {
|
||||
report(`Error expanding task: ${error.message}`, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
// Always stop the loading indicator if we created one
|
||||
if (loadingIndicator) {
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
report(`Error expanding task: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default expandTask;
|
||||
Reference in New Issue
Block a user