feat: Complete generateObject migration with JSON mode support
This commit is contained in:
committed by
Ralph Khreish
parent
604b94baa9
commit
b16023ab2f
@@ -75,13 +75,39 @@ function generateExampleFromSchema(schema) {
|
||||
return result;
|
||||
|
||||
case 'ZodString':
|
||||
return 'string';
|
||||
// Check for min/max length constraints
|
||||
if (def.checks) {
|
||||
const minCheck = def.checks.find(c => c.kind === 'min');
|
||||
const maxCheck = def.checks.find(c => c.kind === 'max');
|
||||
if (minCheck && minCheck.value >= 20) {
|
||||
return '<string with at least ' + minCheck.value + ' characters>';
|
||||
} else if (minCheck && maxCheck) {
|
||||
return '<string between ' + minCheck.value + '-' + maxCheck.value + ' characters>';
|
||||
} else if (maxCheck) {
|
||||
return '<string up to ' + maxCheck.value + ' characters>';
|
||||
}
|
||||
}
|
||||
return '<string>';
|
||||
|
||||
case 'ZodNumber':
|
||||
return 0;
|
||||
// Check for int, positive, min/max constraints
|
||||
if (def.checks) {
|
||||
const intCheck = def.checks.find(c => c.kind === 'int');
|
||||
const minCheck = def.checks.find(c => c.kind === 'min');
|
||||
const maxCheck = def.checks.find(c => c.kind === 'max');
|
||||
|
||||
if (intCheck && minCheck && minCheck.value > 0) {
|
||||
return '<positive integer>';
|
||||
} else if (intCheck) {
|
||||
return '<integer>';
|
||||
} else if (minCheck || maxCheck) {
|
||||
return '<number' + (minCheck ? ' >= ' + minCheck.value : '') + (maxCheck ? ' <= ' + maxCheck.value : '') + '>';
|
||||
}
|
||||
}
|
||||
return '<number>';
|
||||
|
||||
case 'ZodBoolean':
|
||||
return false;
|
||||
return '<boolean>';
|
||||
|
||||
case 'ZodArray':
|
||||
const elementExample = generateExampleFromSchema(def.type);
|
||||
|
||||
@@ -2,7 +2,6 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { z } from 'zod';
|
||||
import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search
|
||||
|
||||
import {
|
||||
@@ -29,6 +28,7 @@ import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import ContextGatherer from '../utils/contextGatherer.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import {
|
||||
TASK_PRIORITY_OPTIONS,
|
||||
DEFAULT_TASK_PRIORITY,
|
||||
@@ -36,26 +36,6 @@ import {
|
||||
normalizeTaskPriority
|
||||
} from '../../../src/constants/task-priority.js';
|
||||
|
||||
// Define Zod schema for the expected AI output object
|
||||
const AiTaskDataSchema = z.object({
|
||||
title: z.string().describe('Clear, concise title for the task'),
|
||||
description: z
|
||||
.string()
|
||||
.describe('A one or two sentence description of the task'),
|
||||
details: z
|
||||
.string()
|
||||
.describe('In-depth implementation details, considerations, and guidance'),
|
||||
testStrategy: z
|
||||
.string()
|
||||
.describe('Detailed approach for verifying task completion'),
|
||||
dependencies: z
|
||||
.array(z.number())
|
||||
.nullable()
|
||||
.describe(
|
||||
'Array of task IDs that this task depends on (must be completed before this task can start)'
|
||||
)
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all tasks from all tags
|
||||
* @param {Object} rawData - The raw tagged data object
|
||||
@@ -451,7 +431,7 @@ async function addTask(
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
schema: AiTaskDataSchema,
|
||||
schema: COMMAND_SCHEMAS['add-task'],
|
||||
objectName: 'newTaskData',
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
displayAiUsageSummary
|
||||
} from '../ui.js';
|
||||
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
|
||||
import {
|
||||
getDebugFlag,
|
||||
@@ -29,45 +30,6 @@ import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { flattenTasksWithSubtasks } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generates the prompt for complexity analysis.
|
||||
* (Moved from ai-services.js and simplified)
|
||||
* @param {Object} tasksData - The tasks data object.
|
||||
* @param {string} [gatheredContext] - The gathered context for the analysis.
|
||||
* @returns {string} The generated prompt.
|
||||
*/
|
||||
function generateInternalComplexityAnalysisPrompt(
|
||||
tasksData,
|
||||
gatheredContext = ''
|
||||
) {
|
||||
const tasksString = JSON.stringify(tasksData.tasks, null, 2);
|
||||
let prompt = `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.
|
||||
|
||||
Tasks:
|
||||
${tasksString}`;
|
||||
|
||||
if (gatheredContext) {
|
||||
prompt += `\n\n# Project Context\n\n${gatheredContext}`;
|
||||
}
|
||||
|
||||
prompt += `
|
||||
|
||||
Respond ONLY with a valid JSON array matching the schema:
|
||||
[
|
||||
{
|
||||
"taskId": <number>,
|
||||
"taskTitle": "<string>",
|
||||
"complexityScore": <number 1-10>,
|
||||
"recommendedSubtasks": <number>,
|
||||
"expansionPrompt": "<string>",
|
||||
"reasoning": "<string>"
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`;
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes task complexity and generates expansion recommendations
|
||||
@@ -446,12 +408,14 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
try {
|
||||
const role = useResearch ? 'research' : 'main';
|
||||
|
||||
aiServiceResponse = await generateTextService({
|
||||
aiServiceResponse = await generateObjectService({
|
||||
prompt,
|
||||
systemPrompt,
|
||||
role,
|
||||
session,
|
||||
projectRoot,
|
||||
schema: COMMAND_SCHEMAS['analyze-complexity'],
|
||||
objectName: 'complexityAnalysis',
|
||||
commandName: 'analyze-complexity',
|
||||
outputType: mcpLog ? 'mcp' : 'cli'
|
||||
});
|
||||
@@ -464,62 +428,13 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
readline.clearLine(process.stdout, 0);
|
||||
readline.cursorTo(process.stdout, 0);
|
||||
console.log(
|
||||
chalk.green('AI service call complete. Parsing response...')
|
||||
chalk.green('AI service call complete.')
|
||||
);
|
||||
}
|
||||
|
||||
reportLog('Parsing complexity analysis from text response...', 'info');
|
||||
try {
|
||||
let cleanedResponse = aiServiceResponse.mainResult;
|
||||
cleanedResponse = cleanedResponse.trim();
|
||||
|
||||
const codeBlockMatch = cleanedResponse.match(
|
||||
/```(?:json)?\s*([\s\S]*?)\s*```/
|
||||
);
|
||||
if (codeBlockMatch) {
|
||||
cleanedResponse = codeBlockMatch[1].trim();
|
||||
} else {
|
||||
const firstBracket = cleanedResponse.indexOf('[');
|
||||
const lastBracket = cleanedResponse.lastIndexOf(']');
|
||||
if (firstBracket !== -1 && lastBracket > firstBracket) {
|
||||
cleanedResponse = cleanedResponse.substring(
|
||||
firstBracket,
|
||||
lastBracket + 1
|
||||
);
|
||||
} else {
|
||||
reportLog(
|
||||
'Warning: Response does not appear to be a JSON array.',
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFormat === 'text' && getDebugFlag(session)) {
|
||||
console.log(chalk.gray('Attempting to parse cleaned JSON...'));
|
||||
console.log(chalk.gray('Cleaned response (first 100 chars):'));
|
||||
console.log(chalk.gray(cleanedResponse.substring(0, 100)));
|
||||
console.log(chalk.gray('Last 100 chars:'));
|
||||
console.log(
|
||||
chalk.gray(cleanedResponse.substring(cleanedResponse.length - 100))
|
||||
);
|
||||
}
|
||||
|
||||
complexityAnalysis = JSON.parse(cleanedResponse);
|
||||
} catch (parseError) {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
reportLog(
|
||||
`Error parsing complexity analysis JSON: ${parseError.message}`,
|
||||
'error'
|
||||
);
|
||||
if (outputFormat === 'text') {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error parsing complexity analysis JSON: ${parseError.message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
throw parseError;
|
||||
}
|
||||
// With generateObject, we get structured data directly
|
||||
complexityAnalysis = aiServiceResponse.mainResult.complexityAnalysis;
|
||||
reportLog(`Received ${complexityAnalysis.length} complexity analyses from AI.`, 'info');
|
||||
|
||||
const taskIds = tasksData.tasks.map((t) => t.id);
|
||||
const analysisTaskIds = complexityAnalysis.map((a) => a.taskId);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
log,
|
||||
@@ -16,7 +15,8 @@ import {
|
||||
displayAiUsageSummary
|
||||
} from '../ui.js';
|
||||
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
|
||||
import {
|
||||
getDefaultSubtasks,
|
||||
@@ -30,253 +30,8 @@ import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
|
||||
|
||||
// --- Zod Schemas (Keep from previous step) ---
|
||||
const subtaskSchema = z.strictObject({
|
||||
id: z.int().positive().describe('Sequential subtask ID starting from 1'),
|
||||
title: z.string().min(5).describe('Clear, specific title for the subtask'),
|
||||
description: z
|
||||
.string()
|
||||
.min(10)
|
||||
.describe('Detailed description of the subtask'),
|
||||
dependencies: z
|
||||
.array(z.string())
|
||||
.describe(
|
||||
'Array of subtask dependencies within the same parent task. Use format ["parentTaskId.1", "parentTaskId.2"]. Subtasks can only depend on siblings, not external tasks.'
|
||||
),
|
||||
details: z.string().min(20).describe('Implementation details and guidance'),
|
||||
status: z
|
||||
.string()
|
||||
.describe(
|
||||
'The current status of the subtask (should be pending initially)'
|
||||
),
|
||||
testStrategy: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('Approach for testing this subtask')
|
||||
.prefault('')
|
||||
});
|
||||
const subtaskArraySchema = z.array(subtaskSchema);
|
||||
const subtaskWrapperSchema = z.object({
|
||||
subtasks: subtaskArraySchema.describe('The array of generated subtasks.')
|
||||
});
|
||||
// --- End Zod Schemas ---
|
||||
|
||||
/**
|
||||
* Parse subtasks from AI's text response. Includes basic cleanup.
|
||||
* @param {string} text - Response text from AI.
|
||||
* @param {number} startId - Starting subtask ID expected.
|
||||
* @param {number} expectedCount - Expected number of subtasks.
|
||||
* @param {number} parentTaskId - Parent task ID for context.
|
||||
* @param {Object} logger - Logging object (mcpLog or console log).
|
||||
* @returns {Array} Parsed and potentially corrected subtasks array.
|
||||
* @throws {Error} If parsing fails or JSON is invalid/malformed.
|
||||
*/
|
||||
function parseSubtasksFromText(
|
||||
text,
|
||||
startId,
|
||||
expectedCount,
|
||||
parentTaskId,
|
||||
logger
|
||||
) {
|
||||
if (typeof text !== 'string') {
|
||||
logger.error(
|
||||
`AI response text is not a string. Received type: ${typeof text}, Value: ${text}`
|
||||
);
|
||||
throw new Error('AI response text is not a string.');
|
||||
}
|
||||
|
||||
if (!text || text.trim() === '') {
|
||||
throw new Error('AI response text is empty after trimming.');
|
||||
}
|
||||
|
||||
const originalTrimmedResponse = text.trim(); // Store the original trimmed response
|
||||
let jsonToParse = originalTrimmedResponse; // Initialize jsonToParse with it
|
||||
|
||||
logger.debug(
|
||||
`Original AI Response for parsing (full length: ${jsonToParse.length}): ${jsonToParse.substring(0, 1000)}...`
|
||||
);
|
||||
|
||||
// --- Pre-emptive cleanup for known AI JSON issues ---
|
||||
// Fix for "dependencies": , or "dependencies":,
|
||||
if (jsonToParse.includes('"dependencies":')) {
|
||||
const malformedPattern = /"dependencies":\s*,/g;
|
||||
if (malformedPattern.test(jsonToParse)) {
|
||||
logger.warn('Attempting to fix malformed "dependencies": , issue.');
|
||||
jsonToParse = jsonToParse.replace(
|
||||
malformedPattern,
|
||||
'"dependencies": [],'
|
||||
);
|
||||
logger.debug(
|
||||
`JSON after fixing "dependencies": ${jsonToParse.substring(0, 500)}...`
|
||||
);
|
||||
}
|
||||
}
|
||||
// --- End pre-emptive cleanup ---
|
||||
|
||||
let parsedObject;
|
||||
let primaryParseAttemptFailed = false;
|
||||
|
||||
// --- Attempt 1: Simple Parse (with optional Markdown cleanup) ---
|
||||
logger.debug('Attempting simple parse...');
|
||||
try {
|
||||
// Check for markdown code block
|
||||
const codeBlockMatch = jsonToParse.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
||||
let contentToParseDirectly = jsonToParse;
|
||||
if (codeBlockMatch && codeBlockMatch[1]) {
|
||||
contentToParseDirectly = codeBlockMatch[1].trim();
|
||||
logger.debug('Simple parse: Extracted content from markdown code block.');
|
||||
} else {
|
||||
logger.debug(
|
||||
'Simple parse: No markdown code block found, using trimmed original.'
|
||||
);
|
||||
}
|
||||
|
||||
parsedObject = JSON.parse(contentToParseDirectly);
|
||||
logger.debug('Simple parse successful!');
|
||||
|
||||
// Quick check if it looks like our target object
|
||||
if (
|
||||
!parsedObject ||
|
||||
typeof parsedObject !== 'object' ||
|
||||
!Array.isArray(parsedObject.subtasks)
|
||||
) {
|
||||
logger.warn(
|
||||
'Simple parse succeeded, but result is not the expected {"subtasks": []} structure. Will proceed to advanced extraction.'
|
||||
);
|
||||
primaryParseAttemptFailed = true;
|
||||
parsedObject = null; // Reset parsedObject so we enter the advanced logic
|
||||
}
|
||||
// If it IS the correct structure, we'll skip advanced extraction.
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Simple parse failed: ${e.message}. Proceeding to advanced extraction logic.`
|
||||
);
|
||||
primaryParseAttemptFailed = true;
|
||||
// jsonToParse is already originalTrimmedResponse if simple parse failed before modifying it for markdown
|
||||
}
|
||||
|
||||
// --- Attempt 2: Advanced Extraction (if simple parse failed or produced wrong structure) ---
|
||||
if (primaryParseAttemptFailed || !parsedObject) {
|
||||
// Ensure we try advanced if simple parse gave wrong structure
|
||||
logger.debug('Attempting advanced extraction logic...');
|
||||
// Reset jsonToParse to the original full trimmed response for advanced logic
|
||||
jsonToParse = originalTrimmedResponse;
|
||||
|
||||
// (Insert the more complex extraction logic here - the one we worked on with:
|
||||
// - targetPattern = '{"subtasks":';
|
||||
// - careful brace counting for that targetPattern
|
||||
// - fallbacks to last '{' and '}' if targetPattern logic fails)
|
||||
// This was the logic from my previous message. Let's assume it's here.
|
||||
// This block should ultimately set `jsonToParse` to the best candidate string.
|
||||
|
||||
// Example snippet of that advanced logic's start:
|
||||
const targetPattern = '{"subtasks":';
|
||||
const patternStartIndex = jsonToParse.indexOf(targetPattern);
|
||||
|
||||
if (patternStartIndex !== -1) {
|
||||
const openBraces = 0;
|
||||
const firstBraceFound = false;
|
||||
const extractedJsonBlock = '';
|
||||
// ... (loop for brace counting as before) ...
|
||||
// ... (if successful, jsonToParse = extractedJsonBlock) ...
|
||||
// ... (if that fails, fallbacks as before) ...
|
||||
} else {
|
||||
// ... (fallback to last '{' and '}' if targetPattern not found) ...
|
||||
}
|
||||
// End of advanced logic excerpt
|
||||
|
||||
logger.debug(
|
||||
`Advanced extraction: JSON string that will be parsed: ${jsonToParse.substring(0, 500)}...`
|
||||
);
|
||||
try {
|
||||
parsedObject = JSON.parse(jsonToParse);
|
||||
logger.debug('Advanced extraction parse successful!');
|
||||
} catch (parseError) {
|
||||
logger.error(
|
||||
`Advanced extraction: Failed to parse JSON object: ${parseError.message}`
|
||||
);
|
||||
logger.error(
|
||||
`Advanced extraction: Problematic JSON string for parse (first 500 chars): ${jsonToParse.substring(0, 500)}`
|
||||
);
|
||||
throw new Error(
|
||||
// Re-throw a more specific error if advanced also fails
|
||||
`Failed to parse JSON response object after both simple and advanced attempts: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Validation (applies to successfully parsedObject from either attempt) ---
|
||||
if (
|
||||
!parsedObject ||
|
||||
typeof parsedObject !== 'object' ||
|
||||
!Array.isArray(parsedObject.subtasks)
|
||||
) {
|
||||
logger.error(
|
||||
`Final parsed content is not an object or missing 'subtasks' array. Content: ${JSON.stringify(parsedObject).substring(0, 200)}`
|
||||
);
|
||||
throw new Error(
|
||||
'Parsed AI response is not a valid object containing a "subtasks" array after all attempts.'
|
||||
);
|
||||
}
|
||||
const parsedSubtasks = parsedObject.subtasks;
|
||||
|
||||
if (expectedCount && parsedSubtasks.length !== expectedCount) {
|
||||
logger.warn(
|
||||
`Expected ${expectedCount} subtasks, but parsed ${parsedSubtasks.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
let currentId = startId;
|
||||
const validatedSubtasks = [];
|
||||
const validationErrors = [];
|
||||
|
||||
for (const rawSubtask of parsedSubtasks) {
|
||||
const correctedSubtask = {
|
||||
...rawSubtask,
|
||||
id: currentId,
|
||||
dependencies: Array.isArray(rawSubtask.dependencies)
|
||||
? rawSubtask.dependencies.filter(
|
||||
(dep) =>
|
||||
typeof dep === 'string' && dep.startsWith(`${parentTaskId}.`)
|
||||
)
|
||||
: [],
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
const result = subtaskSchema.safeParse(correctedSubtask);
|
||||
|
||||
if (result.success) {
|
||||
validatedSubtasks.push(result.data);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Subtask validation failed for raw data: ${JSON.stringify(rawSubtask).substring(0, 100)}...`
|
||||
);
|
||||
result.error.errors.forEach((err) => {
|
||||
const errorMessage = ` - Field '${err.path.join('.')}': ${err.message}`;
|
||||
logger.warn(errorMessage);
|
||||
validationErrors.push(`Subtask ${currentId}: ${errorMessage}`);
|
||||
});
|
||||
}
|
||||
currentId++;
|
||||
}
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
logger.error(
|
||||
`Found ${validationErrors.length} validation errors in the generated subtasks.`
|
||||
);
|
||||
logger.warn('Proceeding with only the successfully validated subtasks.');
|
||||
}
|
||||
|
||||
if (validatedSubtasks.length === 0 && parsedSubtasks.length > 0) {
|
||||
throw new Error(
|
||||
'AI response contained potential subtasks, but none passed validation.'
|
||||
);
|
||||
}
|
||||
return validatedSubtasks.slice(0, expectedCount || validatedSubtasks.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a task into subtasks using the unified AI service (generateTextService).
|
||||
* Expand a task into subtasks using the unified AI service (generateObjectService).
|
||||
* Appends new subtasks by default. Replaces existing subtasks if force=true.
|
||||
* Integrates complexity report to determine subtask count and prompt if available,
|
||||
* unless numSubtasks is explicitly provided.
|
||||
@@ -549,42 +304,28 @@ async function expandTask(
|
||||
try {
|
||||
const role = useResearch ? 'research' : 'main';
|
||||
|
||||
// Call generateTextService with the determined prompts and telemetry params
|
||||
aiServiceResponse = await generateTextService({
|
||||
// Call generateObjectService with the determined prompts and telemetry params
|
||||
aiServiceResponse = await generateObjectService({
|
||||
prompt: promptContent,
|
||||
systemPrompt: systemPrompt,
|
||||
role,
|
||||
session,
|
||||
projectRoot,
|
||||
schema: COMMAND_SCHEMAS['expand-task'],
|
||||
objectName: 'subtasks',
|
||||
commandName: 'expand-task',
|
||||
outputType: outputFormat
|
||||
});
|
||||
responseText = aiServiceResponse.mainResult;
|
||||
|
||||
// Parse Subtasks
|
||||
generatedSubtasks = parseSubtasksFromText(
|
||||
responseText,
|
||||
nextSubtaskId,
|
||||
finalSubtaskCount,
|
||||
task.id,
|
||||
logger
|
||||
);
|
||||
logger.info(
|
||||
`Successfully parsed ${generatedSubtasks.length} subtasks from AI response.`
|
||||
);
|
||||
// With generateObject, we get structured data directly
|
||||
generatedSubtasks = aiServiceResponse.mainResult.subtasks;
|
||||
logger.info(`Received ${generatedSubtasks.length} subtasks from AI.`);
|
||||
} catch (error) {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
logger.error(
|
||||
`Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context
|
||||
'error'
|
||||
);
|
||||
// Log raw response in debug mode if parsing failed
|
||||
if (
|
||||
error.message.includes('Failed to parse valid subtasks') &&
|
||||
getDebugFlag(session)
|
||||
) {
|
||||
logger.error(`Raw AI Response that failed parsing:\n${responseText}`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
|
||||
@@ -3,7 +3,6 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { z } from 'zod'; // Keep Zod for post-parse validation
|
||||
|
||||
import {
|
||||
log as consoleLog,
|
||||
@@ -22,7 +21,11 @@ import {
|
||||
displayAiUsageSummary
|
||||
} from '../ui.js';
|
||||
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import {
|
||||
generateTextService,
|
||||
generateObjectService
|
||||
} from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import {
|
||||
getDebugFlag,
|
||||
isApiKeySet,
|
||||
@@ -32,229 +35,6 @@ import { getPromptManager } from '../prompt-manager.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
|
||||
// Zod schema for post-parsing validation of the updated task object
|
||||
const updatedTaskSchema = z
|
||||
.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(), // Title should be preserved, but check it exists
|
||||
description: z.string(),
|
||||
status: z.string(),
|
||||
dependencies: z.array(z.union([z.number().int(), z.string()])),
|
||||
priority: z.string().nullable().prefault('medium'),
|
||||
details: z.string().nullable().prefault(''),
|
||||
testStrategy: z.string().nullable().prefault(''),
|
||||
subtasks: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe('Sequential subtask ID starting from 1'),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
status: z.string(),
|
||||
dependencies: z.array(z.number().int()).nullable().prefault([]),
|
||||
details: z.string().nullable().prefault(''),
|
||||
testStrategy: z.string().nullable().prefault('')
|
||||
})
|
||||
)
|
||||
.nullable()
|
||||
.prefault([])
|
||||
})
|
||||
.strip(); // Enforce the canonical task shape and drop unknown fields
|
||||
|
||||
/**
|
||||
* Parses a single updated task object from AI's text response.
|
||||
* @param {string} text - Response text from AI.
|
||||
* @param {number} expectedTaskId - The ID of the task expected.
|
||||
* @param {Function | Object} logFn - Logging function or MCP logger.
|
||||
* @param {boolean} isMCP - Flag indicating MCP context.
|
||||
* @returns {Object} Parsed and validated task object.
|
||||
* @throws {Error} If parsing or validation fails.
|
||||
*/
|
||||
function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
|
||||
// Report helper consistent with the established pattern
|
||||
const report = (level, ...args) => {
|
||||
if (isMCP) {
|
||||
if (typeof logFn[level] === 'function') logFn[level](...args);
|
||||
else logFn.info(...args);
|
||||
} else if (!isSilentMode()) {
|
||||
logFn(level, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
report(
|
||||
'info',
|
||||
'Attempting to parse updated task object from text response...'
|
||||
);
|
||||
if (!text || text.trim() === '')
|
||||
throw new Error('AI response text is empty.');
|
||||
|
||||
let cleanedResponse = text.trim();
|
||||
const originalResponseForDebug = cleanedResponse;
|
||||
let parseMethodUsed = 'raw'; // Keep track of which method worked
|
||||
|
||||
// --- NEW Step 1: Try extracting between {} first ---
|
||||
const firstBraceIndex = cleanedResponse.indexOf('{');
|
||||
const lastBraceIndex = cleanedResponse.lastIndexOf('}');
|
||||
let potentialJsonFromBraces = null;
|
||||
|
||||
if (firstBraceIndex !== -1 && lastBraceIndex > firstBraceIndex) {
|
||||
potentialJsonFromBraces = cleanedResponse.substring(
|
||||
firstBraceIndex,
|
||||
lastBraceIndex + 1
|
||||
);
|
||||
if (potentialJsonFromBraces.length <= 2) {
|
||||
potentialJsonFromBraces = null; // Ignore empty braces {}
|
||||
}
|
||||
}
|
||||
|
||||
// If {} extraction yielded something, try parsing it immediately
|
||||
if (potentialJsonFromBraces) {
|
||||
try {
|
||||
const testParse = JSON.parse(potentialJsonFromBraces);
|
||||
// It worked! Use this as the primary cleaned response.
|
||||
cleanedResponse = potentialJsonFromBraces;
|
||||
parseMethodUsed = 'braces';
|
||||
} catch (e) {
|
||||
report(
|
||||
'info',
|
||||
'Content between {} looked promising but failed initial parse. Proceeding to other methods.'
|
||||
);
|
||||
// Reset cleanedResponse to original if brace parsing failed
|
||||
cleanedResponse = originalResponseForDebug;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 2: If brace parsing didn't work or wasn't applicable, try code block extraction ---
|
||||
if (parseMethodUsed === 'raw') {
|
||||
const codeBlockMatch = cleanedResponse.match(
|
||||
/```(?:json|javascript)?\s*([\s\S]*?)\s*```/i
|
||||
);
|
||||
if (codeBlockMatch) {
|
||||
cleanedResponse = codeBlockMatch[1].trim();
|
||||
parseMethodUsed = 'codeblock';
|
||||
report('info', 'Extracted JSON content from Markdown code block.');
|
||||
} else {
|
||||
// --- Step 3: If code block failed, try stripping prefixes ---
|
||||
const commonPrefixes = [
|
||||
'json\n',
|
||||
'javascript\n'
|
||||
// ... other prefixes ...
|
||||
];
|
||||
let prefixFound = false;
|
||||
for (const prefix of commonPrefixes) {
|
||||
if (cleanedResponse.toLowerCase().startsWith(prefix)) {
|
||||
cleanedResponse = cleanedResponse.substring(prefix.length).trim();
|
||||
parseMethodUsed = 'prefix';
|
||||
report('info', `Stripped prefix: "${prefix.trim()}"`);
|
||||
prefixFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prefixFound) {
|
||||
report(
|
||||
'warn',
|
||||
'Response does not appear to contain {}, code block, or known prefix. Attempting raw parse.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 4: Attempt final parse ---
|
||||
let parsedTask;
|
||||
try {
|
||||
parsedTask = JSON.parse(cleanedResponse);
|
||||
} catch (parseError) {
|
||||
report('error', `Failed to parse JSON object: ${parseError.message}`);
|
||||
report(
|
||||
'error',
|
||||
`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to parse JSON response object: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!parsedTask || typeof parsedTask !== 'object') {
|
||||
report(
|
||||
'error',
|
||||
`Parsed content is not an object. Type: ${typeof parsedTask}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Parsed content sample: ${JSON.stringify(parsedTask).substring(0, 200)}`
|
||||
);
|
||||
throw new Error('Parsed AI response is not a valid JSON object.');
|
||||
}
|
||||
|
||||
// Preprocess the task to ensure subtasks have proper structure
|
||||
const preprocessedTask = {
|
||||
...parsedTask,
|
||||
status: parsedTask.status || 'pending',
|
||||
dependencies: Array.isArray(parsedTask.dependencies)
|
||||
? parsedTask.dependencies
|
||||
: [],
|
||||
details:
|
||||
typeof parsedTask.details === 'string'
|
||||
? parsedTask.details
|
||||
: String(parsedTask.details || ''),
|
||||
testStrategy:
|
||||
typeof parsedTask.testStrategy === 'string'
|
||||
? parsedTask.testStrategy
|
||||
: String(parsedTask.testStrategy || ''),
|
||||
// Ensure subtasks is an array and each subtask has required fields
|
||||
subtasks: Array.isArray(parsedTask.subtasks)
|
||||
? parsedTask.subtasks.map((subtask) => ({
|
||||
...subtask,
|
||||
title: subtask.title || '',
|
||||
description: subtask.description || '',
|
||||
status: subtask.status || 'pending',
|
||||
dependencies: Array.isArray(subtask.dependencies)
|
||||
? subtask.dependencies
|
||||
: [],
|
||||
details:
|
||||
typeof subtask.details === 'string'
|
||||
? subtask.details
|
||||
: String(subtask.details || ''),
|
||||
testStrategy:
|
||||
typeof subtask.testStrategy === 'string'
|
||||
? subtask.testStrategy
|
||||
: String(subtask.testStrategy || '')
|
||||
}))
|
||||
: []
|
||||
};
|
||||
|
||||
// Validate the parsed task object using Zod
|
||||
const validationResult = updatedTaskSchema.safeParse(preprocessedTask);
|
||||
if (!validationResult.success) {
|
||||
report('error', 'Parsed task object failed Zod validation.');
|
||||
validationResult.error.errors.forEach((err) => {
|
||||
report('error', ` - Field '${err.path.join('.')}': ${err.message}`);
|
||||
});
|
||||
throw new Error(
|
||||
`AI response failed task structure validation: ${validationResult.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Final check: ensure ID matches expected ID (AI might hallucinate)
|
||||
if (validationResult.data.id !== expectedTaskId) {
|
||||
report(
|
||||
'warn',
|
||||
`AI returned task with ID ${validationResult.data.id}, but expected ${expectedTaskId}. Overwriting ID.`
|
||||
);
|
||||
validationResult.data.id = expectedTaskId; // Enforce correct ID
|
||||
}
|
||||
|
||||
report('info', 'Successfully validated updated task structure.');
|
||||
return validationResult.data; // Return the validated task data
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task by ID with new information using the unified AI service.
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
@@ -522,6 +302,9 @@ async function updateTaskById(
|
||||
|
||||
try {
|
||||
const serviceRole = useResearch ? 'research' : 'main';
|
||||
|
||||
if (appendMode) {
|
||||
// Append mode still uses generateTextService since it returns plain text
|
||||
aiServiceResponse = await generateTextService({
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
@@ -531,6 +314,20 @@ async function updateTaskById(
|
||||
commandName: 'update-task',
|
||||
outputType: isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
} else {
|
||||
// Full update mode uses generateObjectService for structured output
|
||||
aiServiceResponse = await generateObjectService({
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
schema: COMMAND_SCHEMAS['update-task-by-id'],
|
||||
objectName: 'task',
|
||||
commandName: 'update-task',
|
||||
outputType: isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
}
|
||||
|
||||
if (loadingIndicator)
|
||||
stopLoadingIndicator(loadingIndicator, 'AI update complete.');
|
||||
@@ -600,13 +397,8 @@ async function updateTaskById(
|
||||
};
|
||||
}
|
||||
|
||||
// Full update mode: Use mainResult (text) for parsing
|
||||
const updatedTask = parseUpdatedTaskFromText(
|
||||
aiServiceResponse.mainResult,
|
||||
taskId,
|
||||
logFn,
|
||||
isMCP
|
||||
);
|
||||
// Full update mode: Use structured data directly
|
||||
const updatedTask = aiServiceResponse.mainResult.task;
|
||||
|
||||
// --- Task Validation/Correction (Keep existing logic) ---
|
||||
if (!updatedTask || typeof updatedTask !== 'object')
|
||||
|
||||
@@ -2,7 +2,6 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { z } from 'zod'; // Keep Zod for post-parsing validation
|
||||
|
||||
import {
|
||||
log as consoleLog,
|
||||
@@ -22,258 +21,13 @@ import {
|
||||
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import { getModelConfiguration } from './models.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
|
||||
|
||||
// Zod schema for validating the structure of tasks AFTER parsing
|
||||
const updatedTaskSchema = z
|
||||
.object({
|
||||
id: z.int(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
status: z.string(),
|
||||
dependencies: z.array(z.union([z.int(), z.string()])),
|
||||
priority: z.string().nullable(),
|
||||
details: z.string().nullable(),
|
||||
testStrategy: z.string().nullable(),
|
||||
subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now
|
||||
})
|
||||
.strip(); // Enforce the canonical task shape and drop unknown fields
|
||||
|
||||
// Preprocessing schema that adds defaults before validation
|
||||
const preprocessTaskSchema = z.preprocess((task) => {
|
||||
// Ensure task is an object
|
||||
if (typeof task !== 'object' || task === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Return task with defaults for missing fields
|
||||
return {
|
||||
...task,
|
||||
// Add defaults for required fields if missing
|
||||
id: task.id ?? 0,
|
||||
title: task.title ?? 'Untitled Task',
|
||||
description: task.description ?? '',
|
||||
status: task.status ?? 'pending',
|
||||
dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
|
||||
// Optional fields - preserve undefined/null distinction
|
||||
priority: task.hasOwnProperty('priority') ? task.priority : null,
|
||||
details: task.hasOwnProperty('details') ? task.details : null,
|
||||
testStrategy: task.hasOwnProperty('testStrategy')
|
||||
? task.testStrategy
|
||||
: null,
|
||||
subtasks: Array.isArray(task.subtasks)
|
||||
? task.subtasks
|
||||
: task.subtasks === null
|
||||
? null
|
||||
: []
|
||||
};
|
||||
}, updatedTaskSchema);
|
||||
|
||||
const updatedTaskArraySchema = z.array(updatedTaskSchema);
|
||||
const preprocessedTaskArraySchema = z.array(preprocessTaskSchema);
|
||||
|
||||
/**
|
||||
* Parses an array of task objects from AI's text response.
|
||||
* @param {string} text - Response text from AI.
|
||||
* @param {number} expectedCount - Expected number of tasks.
|
||||
* @param {Function | Object} logFn - The logging function or MCP log object.
|
||||
* @param {boolean} isMCP - Flag indicating if logFn is MCP logger.
|
||||
* @returns {Array} Parsed and validated tasks array.
|
||||
* @throws {Error} If parsing or validation fails.
|
||||
*/
|
||||
function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
|
||||
const report = (level, ...args) => {
|
||||
if (isMCP) {
|
||||
if (typeof logFn[level] === 'function') logFn[level](...args);
|
||||
else logFn.info(...args);
|
||||
} else if (!isSilentMode()) {
|
||||
// Check silent mode for consoleLog
|
||||
consoleLog(level, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
report(
|
||||
'info',
|
||||
'Attempting to parse updated tasks array from text response...'
|
||||
);
|
||||
if (!text || text.trim() === '')
|
||||
throw new Error('AI response text is empty.');
|
||||
|
||||
let cleanedResponse = text.trim();
|
||||
const originalResponseForDebug = cleanedResponse;
|
||||
let parseMethodUsed = 'raw'; // Track which method worked
|
||||
|
||||
// --- NEW Step 1: Try extracting between [] first ---
|
||||
const firstBracketIndex = cleanedResponse.indexOf('[');
|
||||
const lastBracketIndex = cleanedResponse.lastIndexOf(']');
|
||||
let potentialJsonFromArray = null;
|
||||
|
||||
if (firstBracketIndex !== -1 && lastBracketIndex > firstBracketIndex) {
|
||||
potentialJsonFromArray = cleanedResponse.substring(
|
||||
firstBracketIndex,
|
||||
lastBracketIndex + 1
|
||||
);
|
||||
// Basic check to ensure it's not just "[]" or malformed
|
||||
if (potentialJsonFromArray.length <= 2) {
|
||||
potentialJsonFromArray = null; // Ignore empty array
|
||||
}
|
||||
}
|
||||
|
||||
// If [] extraction yielded something, try parsing it immediately
|
||||
if (potentialJsonFromArray) {
|
||||
try {
|
||||
const testParse = JSON.parse(potentialJsonFromArray);
|
||||
// It worked! Use this as the primary cleaned response.
|
||||
cleanedResponse = potentialJsonFromArray;
|
||||
parseMethodUsed = 'brackets';
|
||||
} catch (e) {
|
||||
report(
|
||||
'info',
|
||||
'Content between [] looked promising but failed initial parse. Proceeding to other methods.'
|
||||
);
|
||||
// Reset cleanedResponse to original if bracket parsing failed
|
||||
cleanedResponse = originalResponseForDebug;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 2: If bracket parsing didn't work or wasn't applicable, try code block extraction ---
|
||||
if (parseMethodUsed === 'raw') {
|
||||
// Only look for ```json blocks now
|
||||
const codeBlockMatch = cleanedResponse.match(
|
||||
/```json\s*([\s\S]*?)\s*```/i // Only match ```json
|
||||
);
|
||||
if (codeBlockMatch) {
|
||||
cleanedResponse = codeBlockMatch[1].trim();
|
||||
parseMethodUsed = 'codeblock';
|
||||
report('info', 'Extracted JSON content from JSON Markdown code block.');
|
||||
} else {
|
||||
report('info', 'No JSON code block found.');
|
||||
// --- Step 3: If code block failed, try stripping prefixes ---
|
||||
const commonPrefixes = [
|
||||
'json\n',
|
||||
'javascript\n', // Keep checking common prefixes just in case
|
||||
'python\n',
|
||||
'here are the updated tasks:',
|
||||
'here is the updated json:',
|
||||
'updated tasks:',
|
||||
'updated json:',
|
||||
'response:',
|
||||
'output:'
|
||||
];
|
||||
let prefixFound = false;
|
||||
for (const prefix of commonPrefixes) {
|
||||
if (cleanedResponse.toLowerCase().startsWith(prefix)) {
|
||||
cleanedResponse = cleanedResponse.substring(prefix.length).trim();
|
||||
parseMethodUsed = 'prefix';
|
||||
report('info', `Stripped prefix: "${prefix.trim()}"`);
|
||||
prefixFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prefixFound) {
|
||||
report(
|
||||
'warn',
|
||||
'Response does not appear to contain [], JSON code block, or known prefix. Attempting raw parse.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 4: Attempt final parse ---
|
||||
let parsedTasks;
|
||||
try {
|
||||
parsedTasks = JSON.parse(cleanedResponse);
|
||||
} catch (parseError) {
|
||||
report('error', `Failed to parse JSON array: ${parseError.message}`);
|
||||
report(
|
||||
'error',
|
||||
`Extraction method used: ${parseMethodUsed}` // Log which method failed
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to parse JSON response array: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// --- Step 5 & 6: Validate Array structure and Zod schema ---
|
||||
if (!Array.isArray(parsedTasks)) {
|
||||
report(
|
||||
'error',
|
||||
`Parsed content is not an array. Type: ${typeof parsedTasks}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Parsed content sample: ${JSON.stringify(parsedTasks).substring(0, 200)}`
|
||||
);
|
||||
throw new Error('Parsed AI response is not a valid JSON array.');
|
||||
}
|
||||
|
||||
report('info', `Successfully parsed ${parsedTasks.length} potential tasks.`);
|
||||
if (expectedCount && parsedTasks.length !== expectedCount) {
|
||||
report(
|
||||
'warn',
|
||||
`Expected ${expectedCount} tasks, but parsed ${parsedTasks.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Log missing fields for debugging before preprocessing
|
||||
let hasWarnings = false;
|
||||
parsedTasks.forEach((task, index) => {
|
||||
const missingFields = [];
|
||||
if (!task.hasOwnProperty('id')) missingFields.push('id');
|
||||
if (!task.hasOwnProperty('status')) missingFields.push('status');
|
||||
if (!task.hasOwnProperty('dependencies'))
|
||||
missingFields.push('dependencies');
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
hasWarnings = true;
|
||||
report(
|
||||
'warn',
|
||||
`Task ${index} is missing fields: ${missingFields.join(', ')} - will use defaults`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasWarnings) {
|
||||
report(
|
||||
'warn',
|
||||
'Some tasks were missing required fields. Applying defaults...'
|
||||
);
|
||||
}
|
||||
|
||||
// Use the preprocessing schema to add defaults and validate
|
||||
const preprocessResult = preprocessedTaskArraySchema.safeParse(parsedTasks);
|
||||
|
||||
if (!preprocessResult.success) {
|
||||
// This should rarely happen now since preprocessing adds defaults
|
||||
report('error', 'Failed to validate task array even after preprocessing.');
|
||||
preprocessResult.error.errors.forEach((err) => {
|
||||
report('error', ` - Path '${err.path.join('.')}': ${err.message}`);
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
`AI response failed validation: ${preprocessResult.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
report('info', 'Successfully validated and transformed task structure.');
|
||||
return preprocessResult.data.slice(
|
||||
0,
|
||||
expectedCount || preprocessResult.data.length
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tasks based on new context using the unified AI service.
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
@@ -458,13 +212,15 @@ async function updateTasks(
|
||||
// Determine role based on research flag
|
||||
const serviceRole = useResearch ? 'research' : 'main';
|
||||
|
||||
// Call the unified AI service
|
||||
aiServiceResponse = await generateTextService({
|
||||
// Call the unified AI service with generateObject
|
||||
aiServiceResponse = await generateObjectService({
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
schema: COMMAND_SCHEMAS['update-tasks'],
|
||||
objectName: 'tasks',
|
||||
commandName: 'update-tasks',
|
||||
outputType: isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
@@ -472,13 +228,8 @@ async function updateTasks(
|
||||
if (loadingIndicator)
|
||||
stopLoadingIndicator(loadingIndicator, 'AI update complete.');
|
||||
|
||||
// Use the mainResult (text) for parsing
|
||||
const parsedUpdatedTasks = parseUpdatedTasksFromText(
|
||||
aiServiceResponse.mainResult,
|
||||
tasksToUpdate.length,
|
||||
logFn,
|
||||
isMCP
|
||||
);
|
||||
// With generateObject, we get structured data directly
|
||||
const parsedUpdatedTasks = aiServiceResponse.mainResult.tasks;
|
||||
|
||||
// --- Update Tasks Data (Updated writeJSON call) ---
|
||||
if (!Array.isArray(parsedUpdatedTasks)) {
|
||||
|
||||
@@ -272,12 +272,19 @@ export class BaseAIProvider {
|
||||
);
|
||||
|
||||
const client = await this.getClient(params);
|
||||
|
||||
// For providers that don't support tool mode (like claude-code),
|
||||
// we need to ensure the schema is properly communicated in the prompt
|
||||
const needsExplicitSchema = this.name === 'Claude Code';
|
||||
|
||||
const result = await generateObject({
|
||||
model: client(params.modelId),
|
||||
messages: params.messages,
|
||||
schema: zodSchema(params.schema),
|
||||
mode: params.mode || 'auto',
|
||||
...this.prepareTokenParam(params.modelId, params.maxTokens),
|
||||
schema: params.schema,
|
||||
mode: needsExplicitSchema ? 'json' : 'auto',
|
||||
schemaName: params.objectName,
|
||||
schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
|
||||
maxTokens: params.maxTokens,
|
||||
temperature: params.temperature
|
||||
});
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.",
|
||||
"system": "You are an expert software architect and project manager analyzing task complexity. Your analysis should consider implementation effort, technical challenges, dependencies, and testing requirements.\n\nIMPORTANT: For each task, provide an analysis object with ALL of the following fields:\n- taskId: The ID of the task being analyzed (positive integer)\n- taskTitle: The title of the task\n- complexityScore: A score from 1-10 indicating complexity\n- recommendedSubtasks: Number of subtasks recommended (positive integer)\n- expansionPrompt: A prompt to guide subtask generation\n- reasoning: Your reasoning for the complexity score",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before analyzing task complexity:\n\n1. Use the Glob tool to explore the project structure and understand the codebase size\n2. Use the Grep tool to search for existing implementations related to each task\n3. Use the Read tool to examine key files that would be affected by these tasks\n4. Understand the current implementation state, patterns used, and technical debt\n\nBased on your codebase analysis:\n- Assess complexity based on ACTUAL code that needs to be modified/created\n- Consider existing abstractions and patterns that could simplify implementation\n- Identify tasks that require refactoring vs. greenfield development\n- Factor in dependencies between existing code and new features\n- Provide more accurate subtask recommendations based on real code structure\n\nProject Root: {{projectRoot}}\n\n{{/if}} Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.{{#if useResearch}} Consider current best practices, common implementation patterns, and industry standards in your analysis.{{/if}}\n\nTasks:\n{{{json tasks}}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nRespond ONLY with a valid JSON array matching the schema:\n[\n {\n \"taskId\": <number>,\n \"taskTitle\": \"<string>\",\n \"complexityScore\": <number 1-10>,\n \"recommendedSubtasks\": <number>,\n \"expansionPrompt\": \"<string>\",\n \"reasoning\": \"<string>\"\n },\n ...\n]\n\nDo not include any explanatory text, markdown formatting, or code block markers before or after the JSON array."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,17 +68,17 @@
|
||||
"prompts": {
|
||||
"complexity-report": {
|
||||
"condition": "expansionPrompt",
|
||||
"system": "You are an AI assistant helping with task breakdown. Generate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks based on the provided prompt and context.\nRespond ONLY with a valid JSON object containing a single key \"subtasks\" whose value is an array of the generated subtask objects.\nEach subtask object in the array must have keys: \"id\", \"title\", \"description\", \"dependencies\", \"details\", \"status\".\nEnsure the 'id' starts from {{nextSubtaskId}} and is sequential.\nFor 'dependencies', use the full subtask ID format: \"{{task.id}}.1\", \"{{task.id}}.2\", etc. Only reference subtasks within this same task.\nEnsure 'status' is 'pending'.\nDo not include any other text or explanation.",
|
||||
"system": "You are an AI assistant helping with task breakdown. Generate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks based on the provided prompt and context.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: A positive integer starting from {{nextSubtaskId}}\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
|
||||
"user": "Break down the following task based on the analysis prompt:\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}\n\nExpansion Guidance:\n{{expansionPrompt}}{{#if additionalContext}}\n\n{{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\n\n{{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nGenerate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks with sequential IDs starting from {{nextSubtaskId}}."
|
||||
},
|
||||
"research": {
|
||||
"condition": "useResearch === true && !expansionPrompt",
|
||||
"system": "You are an AI assistant that responds ONLY with valid JSON objects as requested. The object should contain a 'subtasks' array.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following task and break it down into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks using your research capabilities. Assign sequential IDs starting from {{nextSubtaskId}}.\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nConsider this context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: Respond ONLY with a valid JSON object containing a single key \"subtasks\". The value must be an array of the generated subtasks, strictly matching this structure:\n\n{\n \"subtasks\": [\n {\n \"id\": <number>, // Sequential ID starting from {{nextSubtaskId}}\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"dependencies\": [\"<string>\"], // Use full subtask IDs like [\"{{task.id}}.1\", \"{{task.id}}.2\"]. If no dependencies, use an empty array [].\n \"details\": \"<string>\",\n \"testStrategy\": \"<string>\" // Optional\n },\n // ... (repeat for {{#if (gt subtaskCount 0)}}{{subtaskCount}}{{else}}appropriate number of{{/if}} subtasks)\n ]\n}\n\nImportant: For the 'dependencies' field, if a subtask has no dependencies, you MUST use an empty array, for example: \"dependencies\": []. Do not use null or omit the field.\n\nDo not include ANY explanatory text, markdown, or code block markers. Just the JSON object."
|
||||
"system": "You are an AI assistant with research capabilities analyzing and breaking down software development tasks.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: A positive integer starting from {{nextSubtaskId}}\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following task and break it down into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks. Each subtask should be actionable and well-defined.\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nConsider this context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}"
|
||||
},
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping with task breakdown for software development.\nYou need to break down a high-level task into {{#if (gt subtaskCount 0)}}{{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks that can be implemented one by one.\n\nSubtasks should:\n1. Be specific and actionable implementation steps\n2. Follow a logical sequence\n3. Each handle a distinct part of the parent task\n4. Include clear guidance on implementation approach\n5. Have appropriate dependency chains between subtasks (using full subtask IDs)\n6. Collectively cover all aspects of the parent task\n\nFor each subtask, provide:\n- id: Sequential integer starting from the provided nextSubtaskId\n- title: Clear, specific title\n- description: Detailed description\n- dependencies: Array of prerequisite subtask IDs using full format like [\"{{task.id}}.1\", \"{{task.id}}.2\"]\n- details: Implementation details, the output should be in string\n- testStrategy: Optional testing approach\n\nRespond ONLY with a valid JSON object containing a single key \"subtasks\" whose value is an array matching the structure described. Do not include any explanatory text, markdown formatting, or code block markers.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Break down this task into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks:\n\nTask ID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nAdditional context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nReturn ONLY the JSON object containing the \"subtasks\" array, matching this structure:\n\n{\n \"subtasks\": [\n {\n \"id\": {{nextSubtaskId}}, // First subtask ID\n \"title\": \"Specific subtask title\",\n \"description\": \"Detailed description\",\n \"dependencies\": [], // e.g., [\"{{task.id}}.1\", \"{{task.id}}.2\"] for dependencies. Use empty array [] if no dependencies\n \"details\": \"Implementation guidance\",\n \"testStrategy\": \"Optional testing approach\"\n },\n // ... (repeat for {{#if (gt subtaskCount 0)}}a total of {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks with sequential IDs)\n ]\n}"
|
||||
"system": "You are an AI assistant helping with task breakdown for software development. Break down high-level tasks into specific, actionable subtasks that can be implemented sequentially.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: A positive integer starting from {{nextSubtaskId}}\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Break down this task into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks:\n\nTask ID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nAdditional context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant specialized in analyzing Product Requirements Documents (PRDs) and generating a structured, logically ordered, dependency-aware and sequenced list of development tasks in JSON format.{{#if research}}\nBefore breaking down the PRD into tasks, you will:\n1. Research and analyze the latest technologies, libraries, frameworks, and best practices that would be appropriate for this project\n2. Identify any potential technical challenges, security concerns, or scalability issues not explicitly mentioned in the PRD without discarding any explicit requirements or going overboard with complexity -- always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n3. Consider current industry standards and evolving trends relevant to this project (this step aims to solve LLM hallucinations and out of date information due to training data cutoff dates)\n4. Evaluate alternative implementation approaches and recommend the most efficient path\n5. Include specific library versions, helpful APIs, and concrete implementation guidance based on your research\n6. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n\nYour task breakdown should incorporate this research, resulting in more detailed implementation guidance, more accurate dependency mapping, and more precise technology recommendations than would be possible from the PRD text alone, while maintaining all explicit requirements and best practices and all details and nuances of the PRD.{{/if}}\n\nAnalyze the provided PRD content and generate {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} top-level development tasks. If the complexity or the level of detail of the PRD is high, generate more tasks relative to the complexity of the PRD\nEach task should represent a logical unit of work needed to implement the requirements and focus on the most direct and effective way to implement the requirements without unnecessary complexity or overengineering. Include pseudo-code, implementation details, and test strategy for each task. Find the most up to date information to implement each task.\nAssign sequential IDs starting from {{nextId}}. Infer title, description, details, and test strategy for each task based *only* on the PRD content.\nSet status to 'pending', dependencies to an empty array [], and priority to '{{defaultTaskPriority}}' initially for all tasks.\nRespond ONLY with a valid JSON object containing a single key \"tasks\", where the value is an array of task objects adhering to the provided Zod schema. Do not include any explanation or markdown formatting.\n\nEach task should follow this JSON structure:\n{\n\t\"id\": number,\n\t\"title\": string,\n\t\"description\": string,\n\t\"status\": \"pending\",\n\t\"dependencies\": number[] (IDs of tasks this depends on),\n\t\"priority\": \"high\" | \"medium\" | \"low\",\n\t\"details\": string (implementation details),\n\t\"testStrategy\": string (validation approach)\n}\n\nGuidelines:\n1. {{#if (gt numTasks 0)}}Unless complexity warrants otherwise{{else}}Depending on the complexity{{/if}}, create {{#if (gt numTasks 0)}}exactly {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, numbered sequentially starting from {{nextId}}\n2. Each task should be atomic and focused on a single responsibility following the most up to date best practices and standards\n3. Order tasks logically - consider dependencies and implementation sequence\n4. Early tasks should focus on setup, core functionality first, then advanced features\n5. Include clear validation/testing approach for each task\n6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs, potentially including existing tasks with IDs less than {{nextId}} if applicable)\n7. Assign priority (high/medium/low) based on criticality and dependency order\n8. Include detailed implementation guidance in the \"details\" field{{#if research}}, with specific libraries and version recommendations based on your research{{/if}}\n9. If the PRD contains specific requirements for libraries, database schemas, frameworks, tech stacks, or any other implementation details, STRICTLY ADHERE to these requirements in your task breakdown and do not discard them under any circumstance\n10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements\n11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches{{#if research}}\n12. For each task, include specific, actionable guidance based on current industry standards and best practices discovered through research{{/if}}",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, README.md, and main entry points\n4. Analyze the current state of implementation to understand what already exists\n\nBased on your analysis:\n- Identify what components/features are already implemented\n- Understand the technology stack, frameworks, and patterns in use\n- Generate tasks that build upon the existing codebase rather than duplicating work\n- Ensure tasks align with the project's current architecture and conventions\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here's the Product Requirements Document (PRD) to break down into {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, starting IDs from {{nextId}}:{{#if research}}\n\nRemember to thoroughly research current best practices and technologies before task breakdown to provide specific, actionable implementation details.{{/if}}\n\n{{prdContent}}\n\n\n\t\tReturn your response in this format:\n{\n \"tasks\": [\n {\n \"id\": 1,\n \"title\": \"Setup Project Repository\",\n \"description\": \"...\",\n ...\n },\n ...\n ],\n \"metadata\": {\n \"projectName\": \"PRD Implementation\",\n \"totalTasks\": {{#if (gt numTasks 0)}}{{numTasks}}{{else}}{number of tasks}{{/if}},\n \"sourceFile\": \"{{prdPath}}\",\n \"generatedAt\": \"YYYY-MM-DD\"\n }\n}"
|
||||
"system": "You are an AI assistant specialized in analyzing Product Requirements Documents (PRDs) and generating a structured, logically ordered, dependency-aware and sequenced list of development tasks in JSON format.{{#if research}}\nBefore breaking down the PRD into tasks, you will:\n1. Research and analyze the latest technologies, libraries, frameworks, and best practices that would be appropriate for this project\n2. Identify any potential technical challenges, security concerns, or scalability issues not explicitly mentioned in the PRD without discarding any explicit requirements or going overboard with complexity -- always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n3. Consider current industry standards and evolving trends relevant to this project (this step aims to solve LLM hallucinations and out of date information due to training data cutoff dates)\n4. Evaluate alternative implementation approaches and recommend the most efficient path\n5. Include specific library versions, helpful APIs, and concrete implementation guidance based on your research\n6. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n\nYour task breakdown should incorporate this research, resulting in more detailed implementation guidance, more accurate dependency mapping, and more precise technology recommendations than would be possible from the PRD text alone, while maintaining all explicit requirements and best practices and all details and nuances of the PRD.{{/if}}\n\nAnalyze the provided PRD content and generate {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} top-level development tasks. If the complexity or the level of detail of the PRD is high, generate more tasks relative to the complexity of the PRD\nEach task should represent a logical unit of work needed to implement the requirements and focus on the most direct and effective way to implement the requirements without unnecessary complexity or overengineering. Include pseudo-code, implementation details, and test strategy for each task. Find the most up to date information to implement each task.\nAssign sequential IDs starting from {{nextId}}. Infer title, description, details, and test strategy for each task based *only* on the PRD content.\nSet status to 'pending', dependencies to an empty array [], and priority to '{{defaultTaskPriority}}' initially for all tasks.\nGenerate a response containing a single key \"tasks\", where the value is an array of task objects adhering to the provided schema.\n\nEach task should follow this JSON structure:\n{\n\t\"id\": number,\n\t\"title\": string,\n\t\"description\": string,\n\t\"status\": \"pending\",\n\t\"dependencies\": number[] (IDs of tasks this depends on),\n\t\"priority\": \"high\" | \"medium\" | \"low\",\n\t\"details\": string (implementation details),\n\t\"testStrategy\": string (validation approach)\n}\n\nGuidelines:\n1. {{#if (gt numTasks 0)}}Unless complexity warrants otherwise{{else}}Depending on the complexity{{/if}}, create {{#if (gt numTasks 0)}}exactly {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, numbered sequentially starting from {{nextId}}\n2. Each task should be atomic and focused on a single responsibility following the most up to date best practices and standards\n3. Order tasks logically - consider dependencies and implementation sequence\n4. Early tasks should focus on setup, core functionality first, then advanced features\n5. Include clear validation/testing approach for each task\n6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs, potentially including existing tasks with IDs less than {{nextId}} if applicable)\n7. Assign priority (high/medium/low) based on criticality and dependency order\n8. Include detailed implementation guidance in the \"details\" field{{#if research}}, with specific libraries and version recommendations based on your research{{/if}}\n9. If the PRD contains specific requirements for libraries, database schemas, frameworks, tech stacks, or any other implementation details, STRICTLY ADHERE to these requirements in your task breakdown and do not discard them under any circumstance\n10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements\n11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches{{#if research}}\n12. For each task, include specific, actionable guidance based on current industry standards and best practices discovered through research{{/if}}",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, README.md, and main entry points\n4. Analyze the current state of implementation to understand what already exists\n\nBased on your analysis:\n- Identify what components/features are already implemented\n- Understand the technology stack, frameworks, and patterns in use\n- Generate tasks that build upon the existing codebase rather than duplicating work\n- Ensure tasks align with the project's current architecture and conventions\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here's the Product Requirements Document (PRD) to break down into {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, starting IDs from {{nextId}}:{{#if research}}\n\nRemember to thoroughly research current best practices and technologies before task breakdown to provide specific, actionable implementation details.{{/if}}\n\n{{prdContent}}\n\nIMPORTANT: Your response must be a JSON object with a single property named \"tasks\" containing an array of task objects. Do NOT include metadata or any other properties."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +59,13 @@
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping to update a software development task based on new context.{{#if useResearch}} You have access to current best practices and latest technical information to provide research-backed updates.{{/if}}\nYou will be given a task and a prompt describing changes or new implementation details.\nYour job is to update the task to reflect these changes, while preserving its basic structure.\n\nGuidelines:\n1. VERY IMPORTANT: NEVER change the title of the task - keep it exactly as is\n2. Maintain the same ID, status, and dependencies unless specifically mentioned in the prompt{{#if useResearch}}\n3. Research and update the description, details, and test strategy with current best practices\n4. Include specific versions, libraries, and approaches that are current and well-tested{{/if}}{{#if (not useResearch)}}\n3. Update the description, details, and test strategy to reflect the new information\n4. Do not change anything unnecessarily - just adapt what needs to change based on the prompt{{/if}}\n5. Return a complete valid JSON object representing the updated task\n6. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n7. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n8. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n11. Ensure any new subtasks have unique IDs that don't conflict with existing ones\n12. CRITICAL: For subtask IDs, use ONLY numeric values (1, 2, 3, etc.) NOT strings (\"1\", \"2\", \"3\")\n13. CRITICAL: Subtask IDs should start from 1 and increment sequentially (1, 2, 3...) - do NOT use parent task ID as prefix{{#if useResearch}}\n14. Include links to documentation or resources where helpful\n15. Focus on practical, implementable solutions using current technologies{{/if}}\n\nThe changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the task changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here is the task to update{{#if useResearch}} with research-backed information{{/if}}:\n{{{taskJson}}}\n\nPlease {{#if useResearch}}research and {{/if}}update this task based on the following {{#if useResearch}}context:\n{{updatePrompt}}\n\nIncorporate current best practices, latest stable versions, and proven approaches.{{/if}}{{#if (not useResearch)}}new context:\n{{updatePrompt}}{{/if}}\n\nIMPORTANT: {{#if useResearch}}Preserve any subtasks marked as \"done\" or \"completed\".{{/if}}{{#if (not useResearch)}}In the task JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{/if}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nReturn only the updated task as a valid JSON object{{#if useResearch}} with research-backed improvements{{/if}}."
|
||||
"system": "You are an AI assistant helping to update a software development task based on new context.{{#if useResearch}} You have access to current best practices and latest technical information to provide research-backed updates.{{/if}}\nYou will be given a task and a prompt describing changes or new implementation details.\nYour job is to update the task to reflect these changes, while preserving its basic structure.\n\nGuidelines:\n1. VERY IMPORTANT: NEVER change the title of the task - keep it exactly as is\n2. Maintain the same ID, status, and dependencies unless specifically mentioned in the prompt{{#if useResearch}}\n3. Research and update the description, details, and test strategy with current best practices\n4. Include specific versions, libraries, and approaches that are current and well-tested{{/if}}{{#if (not useResearch)}}\n3. Update the description, details, and test strategy to reflect the new information\n4. Do not change anything unnecessarily - just adapt what needs to change based on the prompt{{/if}}\n5. Return the complete updated task\n6. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n7. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n8. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n11. Ensure any new subtasks have unique IDs that don't conflict with existing ones\n12. CRITICAL: For subtask IDs, use ONLY numeric values (1, 2, 3, etc.) NOT strings (\"1\", \"2\", \"3\")\n13. CRITICAL: Subtask IDs should start from 1 and increment sequentially (1, 2, 3...) - do NOT use parent task ID as prefix{{#if useResearch}}\n14. Include links to documentation or resources where helpful\n15. Focus on practical, implementable solutions using current technologies{{/if}}\n\nThe changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the task changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here is the task to update{{#if useResearch}} with research-backed information{{/if}}:\n{{{taskJson}}}\n\nPlease {{#if useResearch}}research and {{/if}}update this task based on the following {{#if useResearch}}context:\n{{updatePrompt}}\n\nIncorporate current best practices, latest stable versions, and proven approaches.{{/if}}{{#if (not useResearch)}}new context:\n{{updatePrompt}}{{/if}}\n\nIMPORTANT: {{#if useResearch}}Preserve any subtasks marked as \"done\" or \"completed\".{{/if}}{{#if (not useResearch)}}In the task JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{/if}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nReturn the complete updated task{{#if useResearch}} with research-backed improvements{{/if}}.\n\nIMPORTANT: Your response must be a JSON object with a single property named \"task\" containing the updated task object."
|
||||
},
|
||||
"append": {
|
||||
"condition": "appendMode === true",
|
||||
"system": "You are an AI assistant helping to append additional information to a software development task. You will be provided with the task's existing details, context, and a user request string.\n\nYour Goal: Based *only* on the user's request and all the provided context (including existing details if relevant to the request), GENERATE the new text content that should be added to the task's details.\nFocus *only* on generating the substance of the update.\n\nOutput Requirements:\n1. Return *only* the newly generated text content as a plain string. Do NOT return a JSON object or any other structured data.\n2. Your string response should NOT include any of the task's original details, unless the user's request explicitly asks to rephrase, summarize, or directly modify existing text.\n3. Do NOT include any timestamps, XML-like tags, markdown, or any other special formatting in your string response.\n4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with \"Okay, here's the update...\").",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task update:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze the current codebase to inform your update\n\nBased on your analysis:\n- Include specific file references, code patterns, or implementation details\n- Ensure suggestions align with the project's current architecture\n- Reference existing components or patterns when relevant\n\nProject Root: {{projectRoot}}\n\n{{/if}}Task Context:\n\nTask: {{{json task}}}\nCurrent Task Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return ONLY this new text as a plain string.\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task update:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze the current codebase to inform your update\n\nBased on your analysis:\n- Include specific file references, code patterns, or implementation details\n- Ensure suggestions align with the project's current architecture\n- Reference existing components or patterns when relevant\n\nProject Root: {{projectRoot}}\n\n{{/if}}Task Context:\n\nTask: {{{json task}}}\nCurrent Task Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return this new text as a plain string.\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping to update software development tasks based on new context.\nYou will be given a set of tasks and a prompt describing changes or new implementation details.\nYour job is to update the tasks to reflect these changes, while preserving their basic structure.\n\nCRITICAL RULES:\n1. Return ONLY a JSON array - no explanations, no markdown, no additional text before or after\n2. Each task MUST have ALL fields from the original (do not omit any fields)\n3. Maintain the same IDs, statuses, and dependencies unless specifically mentioned in the prompt\n4. Update titles, descriptions, details, and test strategies to reflect the new information\n5. Do not change anything unnecessarily - just adapt what needs to change based on the prompt\n6. You should return ALL the tasks in order, not just the modified ones\n7. Return a complete valid JSON array with all tasks\n8. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n9. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n10. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n11. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n12. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n\nThe changes described in the prompt should be applied to ALL tasks in the list.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the new changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here are the tasks to update:\n{{{json tasks}}}\n\nPlease update these tasks based on the following new context:\n{{updatePrompt}}\n\nIMPORTANT: In the tasks JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{#if projectContext}}\n\n# Project Context\n\n{{projectContext}}{{/if}}\n\nRequired JSON structure for EACH task (ALL fields MUST be present):\n{\n \"id\": <number>,\n \"title\": <string>,\n \"description\": <string>,\n \"status\": <string>,\n \"dependencies\": <array>,\n \"priority\": <string or null>,\n \"details\": <string or null>,\n \"testStrategy\": <string or null>,\n \"subtasks\": <array or null>\n}\n\nReturn a valid JSON array containing ALL the tasks with ALL their fields:\n- id (number) - preserve existing value\n- title (string)\n- description (string)\n- status (string) - preserve existing value unless explicitly changing\n- dependencies (array) - preserve existing value unless explicitly changing\n- priority (string or null)\n- details (string or null)\n- testStrategy (string or null)\n- subtasks (array or null)\n\nReturn ONLY the JSON array now:"
|
||||
"system": "You are an AI assistant helping to update software development tasks based on new context.\nYou will be given a set of tasks and a prompt describing changes or new implementation details.\nYour job is to update the tasks to reflect these changes, while preserving their basic structure.\n\nGuidelines:\n1. Maintain the same IDs, statuses, and dependencies unless specifically mentioned in the prompt\n2. Update titles, descriptions, details, and test strategies to reflect the new information\n3. Do not change anything unnecessarily - just adapt what needs to change based on the prompt\n4. Return ALL the tasks in order, not just the modified ones\n5. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n6. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n7. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n8. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n9. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the new changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here are the tasks to update:\n{{{json tasks}}}\n\nPlease update these tasks based on the following new context:\n{{updatePrompt}}\n\nIMPORTANT: In the tasks above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{#if projectContext}}\n\n# Project Context\n\n{{projectContext}}{{/if}}\n\nIMPORTANT: Your response must be a JSON object with a single property named \"tasks\" containing the updated array of tasks."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
src/schemas/add-task.js
Normal file
21
src/schemas/add-task.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Schema that matches the inline AiTaskDataSchema from add-task.js
|
||||
export const AddTaskResponseSchema = z.object({
|
||||
title: z.string().describe('Clear, concise title for the task'),
|
||||
description: z
|
||||
.string()
|
||||
.describe('A one or two sentence description of the task'),
|
||||
details: z
|
||||
.string()
|
||||
.describe('In-depth implementation details, considerations, and guidance'),
|
||||
testStrategy: z
|
||||
.string()
|
||||
.describe('Detailed approach for verifying task completion'),
|
||||
dependencies: z
|
||||
.array(z.number())
|
||||
.nullable()
|
||||
.describe(
|
||||
'Array of task IDs that this task depends on (must be completed before this task can start)'
|
||||
)
|
||||
});
|
||||
14
src/schemas/analyze-complexity.js
Normal file
14
src/schemas/analyze-complexity.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ComplexityAnalysisItemSchema = z.object({
|
||||
taskId: z.number().int().positive(),
|
||||
taskTitle: z.string(),
|
||||
complexityScore: z.number().min(1).max(10),
|
||||
recommendedSubtasks: z.number().int().positive(),
|
||||
expansionPrompt: z.string(),
|
||||
reasoning: z.string()
|
||||
});
|
||||
|
||||
export const ComplexityAnalysisResponseSchema = z.object({
|
||||
complexityAnalysis: z.array(ComplexityAnalysisItemSchema)
|
||||
});
|
||||
25
src/schemas/base-schemas.js
Normal file
25
src/schemas/base-schemas.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Base schemas that will be reused across commands
|
||||
export const TaskStatusSchema = z.enum(['pending', 'in-progress', 'blocked', 'done', 'cancelled', 'deferred']);
|
||||
|
||||
export const BaseTaskSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
title: z.string().min(1).max(200),
|
||||
description: z.string().min(1),
|
||||
status: TaskStatusSchema,
|
||||
dependencies: z.array(z.union([z.number().int(), z.string()])).default([]),
|
||||
priority: z.enum(['low', 'medium', 'high', 'critical']).nullable().default(null),
|
||||
details: z.string().nullable().default(null),
|
||||
testStrategy: z.string().nullable().default(null)
|
||||
});
|
||||
|
||||
export const SubtaskSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
title: z.string().min(5).max(200),
|
||||
description: z.string().min(10),
|
||||
dependencies: z.array(z.number().int()).default([]),
|
||||
details: z.string().min(20),
|
||||
status: z.enum(['pending', 'done', 'completed']).default('pending'),
|
||||
testStrategy: z.string().nullable().default(null)
|
||||
});
|
||||
6
src/schemas/expand-task.js
Normal file
6
src/schemas/expand-task.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { SubtaskSchema } from './base-schemas.js';
|
||||
|
||||
export const ExpandTaskResponseSchema = z.object({
|
||||
subtasks: z.array(SubtaskSchema)
|
||||
});
|
||||
18
src/schemas/parse-prd.js
Normal file
18
src/schemas/parse-prd.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Schema for a single task from PRD parsing
|
||||
const PRDSingleTaskSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
details: z.string().nullable(),
|
||||
testStrategy: z.string().nullable(),
|
||||
priority: z.enum(['high', 'medium', 'low']).nullable(),
|
||||
dependencies: z.array(z.number().int().positive()).nullable(),
|
||||
status: z.string().nullable()
|
||||
});
|
||||
|
||||
// Schema for the AI response - only expects tasks array since metadata is generated by the code
|
||||
export const ParsePRDResponseSchema = z.object({
|
||||
tasks: z.array(PRDSingleTaskSchema)
|
||||
});
|
||||
27
src/schemas/registry.js
Normal file
27
src/schemas/registry.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { UpdateTasksResponseSchema } from './update-tasks.js';
|
||||
import { ExpandTaskResponseSchema } from './expand-task.js';
|
||||
import { ComplexityAnalysisResponseSchema } from './analyze-complexity.js';
|
||||
import { UpdateSubtaskResponseSchema } from './update-subtask.js';
|
||||
import { UpdateTaskResponseSchema } from './update-task.js';
|
||||
import { AddTaskResponseSchema } from './add-task.js';
|
||||
import { ParsePRDResponseSchema } from './parse-prd.js';
|
||||
|
||||
export const COMMAND_SCHEMAS = {
|
||||
'update-tasks': UpdateTasksResponseSchema,
|
||||
'expand-task': ExpandTaskResponseSchema,
|
||||
'analyze-complexity': ComplexityAnalysisResponseSchema,
|
||||
'update-subtask-by-id': UpdateSubtaskResponseSchema,
|
||||
'update-task-by-id': UpdateTaskResponseSchema,
|
||||
'add-task': AddTaskResponseSchema,
|
||||
'parse-prd': ParsePRDResponseSchema
|
||||
};
|
||||
|
||||
// Export individual schemas for direct access
|
||||
export * from './update-tasks.js';
|
||||
export * from './expand-task.js';
|
||||
export * from './analyze-complexity.js';
|
||||
export * from './update-subtask.js';
|
||||
export * from './update-task.js';
|
||||
export * from './add-task.js';
|
||||
export * from './parse-prd.js';
|
||||
export * from './base-schemas.js';
|
||||
6
src/schemas/update-subtask.js
Normal file
6
src/schemas/update-subtask.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { SubtaskSchema } from './base-schemas.js';
|
||||
|
||||
export const UpdateSubtaskResponseSchema = z.object({
|
||||
subtask: SubtaskSchema
|
||||
});
|
||||
6
src/schemas/update-task.js
Normal file
6
src/schemas/update-task.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { UpdatedTaskSchema } from './update-tasks.js';
|
||||
|
||||
export const UpdateTaskResponseSchema = z.object({
|
||||
task: UpdatedTaskSchema
|
||||
});
|
||||
10
src/schemas/update-tasks.js
Normal file
10
src/schemas/update-tasks.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import { BaseTaskSchema } from './base-schemas.js';
|
||||
|
||||
export const UpdatedTaskSchema = BaseTaskSchema.extend({
|
||||
subtasks: z.array(z.any()).nullable().default(null)
|
||||
});
|
||||
|
||||
export const UpdateTasksResponseSchema = z.object({
|
||||
tasks: z.array(UpdatedTaskSchema)
|
||||
});
|
||||
23
test-integration-output/test-prd.md
Normal file
23
test-integration-output/test-prd.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Product Requirements Document
|
||||
|
||||
## Overview
|
||||
We need to build a modern task management system with real-time collaboration features.
|
||||
|
||||
## Key Features
|
||||
1. User authentication and authorization
|
||||
2. Task creation and management
|
||||
3. Real-time updates via WebSockets
|
||||
4. File attachments and comments
|
||||
5. Advanced search and filtering
|
||||
|
||||
## Technical Requirements
|
||||
- Node.js backend with Express
|
||||
- PostgreSQL database
|
||||
- Redis for caching
|
||||
- WebSocket support
|
||||
- RESTful API design
|
||||
|
||||
## Success Criteria
|
||||
- Support 10,000+ concurrent users
|
||||
- Sub-100ms API response times
|
||||
- 99.9% uptime SLA
|
||||
63
test-integration-output/test-tasks.json
Normal file
63
test-integration-output/test-tasks.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"master": {
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Setup project infrastructure",
|
||||
"description": "Initialize the project with proper structure and dependencies",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"priority": "high",
|
||||
"details": "Created project structure with src, tests, and docs folders",
|
||||
"testStrategy": "Manual verification of folder structure",
|
||||
"subtasks": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Implement authentication system",
|
||||
"description": "Add user authentication with JWT tokens and OAuth2 support",
|
||||
"status": "in-progress",
|
||||
"dependencies": [
|
||||
1
|
||||
],
|
||||
"priority": "high",
|
||||
"details": "Need to support both OAuth2 and traditional email/password login",
|
||||
"testStrategy": "Unit tests for auth logic, integration tests for endpoints",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Design authentication flow",
|
||||
"description": "Create detailed flow diagrams for auth process",
|
||||
"status": "done",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Implement JWT token generation",
|
||||
"description": "Create secure JWT token generation and validation",
|
||||
"status": "pending",
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Build RESTful API",
|
||||
"description": "Create comprehensive REST API endpoints",
|
||||
"status": "pending",
|
||||
"dependencies": [
|
||||
2
|
||||
],
|
||||
"priority": "medium",
|
||||
"details": "Use Express.js with proper middleware and error handling",
|
||||
"testStrategy": null,
|
||||
"subtasks": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"created": "2025-07-21T00:27:15.668Z",
|
||||
"updated": "2025-07-21T00:27:15.668Z",
|
||||
"description": "Test project tasks"
|
||||
}
|
||||
}
|
||||
}
|
||||
434
tests/integration/commands/all-commands.test.js
Normal file
434
tests/integration/commands/all-commands.test.js
Normal file
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* Comprehensive integration test suite for all generateObject-migrated commands
|
||||
* Tests end-to-end command execution with real AI service calls
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Import all commands
|
||||
import analyzeTaskComplexity from '../../../scripts/modules/task-manager/analyze-task-complexity.js';
|
||||
import updateTaskById from '../../../scripts/modules/task-manager/update-task-by-id.js';
|
||||
import expandTask from '../../../scripts/modules/task-manager/expand-task.js';
|
||||
import updateTasks from '../../../scripts/modules/task-manager/update-tasks.js';
|
||||
import addTask from '../../../scripts/modules/task-manager/add-task.js';
|
||||
import parsePRD from '../../../scripts/modules/task-manager/parse-prd.js';
|
||||
|
||||
describe('GenerateObject Migration - Comprehensive Integration Tests', () => {
|
||||
const testDir = path.join(process.cwd(), 'test-integration-output');
|
||||
const testTasksFile = path.join(testDir, 'test-tasks.json');
|
||||
const testPrdFile = path.join(testDir, 'test-prd.md');
|
||||
|
||||
beforeAll(() => {
|
||||
// Create test directory
|
||||
if (!fs.existsSync(testDir)) {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Create initial test data
|
||||
const initialTasks = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project infrastructure",
|
||||
description: "Initialize the project with proper structure and dependencies",
|
||||
status: "done",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Created project structure with src, tests, and docs folders",
|
||||
testStrategy: "Manual verification of folder structure",
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication system",
|
||||
description: "Add user authentication with JWT tokens and OAuth2 support",
|
||||
status: "in-progress",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: "Need to support both OAuth2 and traditional email/password login",
|
||||
testStrategy: "Unit tests for auth logic, integration tests for endpoints",
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Design authentication flow",
|
||||
description: "Create detailed flow diagrams for auth process",
|
||||
status: "done",
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement JWT token generation",
|
||||
description: "Create secure JWT token generation and validation",
|
||||
status: "pending",
|
||||
dependencies: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Build RESTful API",
|
||||
description: "Create comprehensive REST API endpoints",
|
||||
status: "pending",
|
||||
dependencies: [2],
|
||||
priority: "medium",
|
||||
details: "Use Express.js with proper middleware and error handling",
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: "Test project tasks"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(initialTasks, null, 2));
|
||||
|
||||
// Create test PRD file
|
||||
const testPrd = `# Product Requirements Document
|
||||
|
||||
## Overview
|
||||
We need to build a modern task management system with real-time collaboration features.
|
||||
|
||||
## Key Features
|
||||
1. User authentication and authorization
|
||||
2. Task creation and management
|
||||
3. Real-time updates via WebSockets
|
||||
4. File attachments and comments
|
||||
5. Advanced search and filtering
|
||||
|
||||
## Technical Requirements
|
||||
- Node.js backend with Express
|
||||
- PostgreSQL database
|
||||
- Redis for caching
|
||||
- WebSocket support
|
||||
- RESTful API design
|
||||
|
||||
## Success Criteria
|
||||
- Support 10,000+ concurrent users
|
||||
- Sub-100ms API response times
|
||||
- 99.9% uptime SLA`;
|
||||
|
||||
fs.writeFileSync(testPrdFile, testPrd);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
if (fs.existsSync(testPrdFile)) {
|
||||
fs.unlinkSync(testPrdFile);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('analyze-complexity command', () => {
|
||||
test('should analyze task complexity with structured output', async () => {
|
||||
const result = await analyzeTaskComplexity(
|
||||
testTasksFile,
|
||||
2, // Analyze task ID 2
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json' // JSON output format
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.complexityAnalysis).toBeDefined();
|
||||
expect(result.complexityAnalysis.overallComplexity).toMatch(/low|medium|high|very high/i);
|
||||
expect(result.complexityAnalysis.factors).toBeDefined();
|
||||
expect(Array.isArray(result.complexityAnalysis.factors)).toBe(true);
|
||||
expect(result.complexityAnalysis.timeEstimate).toBeDefined();
|
||||
expect(result.complexityAnalysis.riskAssessment).toBeDefined();
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('add-task command', () => {
|
||||
test('should add a new task with structured output', async () => {
|
||||
const result = await addTask(
|
||||
testTasksFile,
|
||||
'Implement caching layer with Redis for improved performance',
|
||||
[2], // Depends on task 2
|
||||
'medium',
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json',
|
||||
null, // No manual task data
|
||||
false // Don't use research mode
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.newTaskId).toBe(4); // Should be the next ID
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
|
||||
// Verify task was added
|
||||
const updatedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const newTask = updatedData.master.tasks.find(t => t.id === 4);
|
||||
expect(newTask).toBeDefined();
|
||||
expect(newTask.title).toContain('caching');
|
||||
expect(newTask.priority).toBe('medium');
|
||||
expect(newTask.dependencies).toContain(2);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('expand-task command', () => {
|
||||
test('should expand task into subtasks with structured output', async () => {
|
||||
const result = await expandTask(
|
||||
testTasksFile,
|
||||
3, // Expand task ID 3
|
||||
5, // Generate 5 subtasks
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.expandedTask).toBeDefined();
|
||||
expect(result.generatedSubtasks).toBeDefined();
|
||||
expect(Array.isArray(result.generatedSubtasks)).toBe(true);
|
||||
expect(result.generatedSubtasks.length).toBeGreaterThan(0);
|
||||
expect(result.generatedSubtasks.length).toBeLessThanOrEqual(5);
|
||||
|
||||
// Verify subtasks were added
|
||||
const updatedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const task3 = updatedData.master.tasks.find(t => t.id === 3);
|
||||
expect(task3.subtasks).toBeDefined();
|
||||
expect(task3.subtasks.length).toBeGreaterThan(0);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('update-task-by-id command', () => {
|
||||
test('should update task with structured output (full update mode)', async () => {
|
||||
const result = await updateTaskById(
|
||||
testTasksFile,
|
||||
3, // Update task ID 3
|
||||
'Add GraphQL support alongside REST API for more flexible queries',
|
||||
false, // Append mode off (full update)
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.updatedTask).toBeDefined();
|
||||
expect(result.updatedTask.id).toBe(3);
|
||||
expect(result.updatedTask.description.toLowerCase()).toContain('graphql');
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
}, 30000);
|
||||
|
||||
test('should append to task details (append mode)', async () => {
|
||||
const result = await updateTaskById(
|
||||
testTasksFile,
|
||||
2, // Update task ID 2
|
||||
'Add support for multi-factor authentication',
|
||||
true, // Append mode on
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.updatedTask).toBeDefined();
|
||||
expect(result.updatedTask.details).toContain('multi-factor authentication');
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('update-tasks command', () => {
|
||||
test('should update multiple tasks with structured output', async () => {
|
||||
const result = await updateTasks(
|
||||
testTasksFile,
|
||||
2, // Update from task ID 2 onwards
|
||||
'Migrate to microservices architecture for better scalability',
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.updatedTasks).toBeDefined();
|
||||
expect(Array.isArray(result.updatedTasks)).toBe(true);
|
||||
expect(result.updatedTasks.length).toBeGreaterThan(0);
|
||||
|
||||
// Tasks 2 and 3 should be updated (not done)
|
||||
const task2 = result.updatedTasks.find(t => t.id === 2);
|
||||
const task3 = result.updatedTasks.find(t => t.id === 3);
|
||||
expect(task2).toBeDefined();
|
||||
expect(task3).toBeDefined();
|
||||
expect(task2.description.toLowerCase()).toMatch(/microservice|scalability/);
|
||||
expect(task3.description.toLowerCase()).toMatch(/microservice|scalability/);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('parse-prd command', () => {
|
||||
test('should parse PRD and generate tasks with structured output', async () => {
|
||||
// Use a new file for PRD output to avoid conflicts
|
||||
const prdTasksFile = path.join(testDir, 'prd-tasks.json');
|
||||
|
||||
const result = await parsePRD(
|
||||
testPrdFile,
|
||||
prdTasksFile,
|
||||
5, // Generate 5 tasks
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
force: true,
|
||||
append: false,
|
||||
research: false,
|
||||
tag: 'master'
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.tasksPath).toBe(prdTasksFile);
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
|
||||
// Verify tasks were generated
|
||||
const generatedData = JSON.parse(fs.readFileSync(prdTasksFile, 'utf8'));
|
||||
expect(generatedData.master).toBeDefined();
|
||||
expect(generatedData.master.tasks).toBeDefined();
|
||||
expect(generatedData.master.tasks.length).toBeGreaterThan(0);
|
||||
expect(generatedData.master.tasks.length).toBeLessThanOrEqual(5);
|
||||
|
||||
// Verify task quality
|
||||
const firstTask = generatedData.master.tasks[0];
|
||||
expect(firstTask.title).toBeTruthy();
|
||||
expect(firstTask.description).toBeTruthy();
|
||||
expect(firstTask.status).toBe('pending');
|
||||
expect(firstTask.priority).toMatch(/low|medium|high/);
|
||||
|
||||
// Clean up
|
||||
fs.unlinkSync(prdTasksFile);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('Command Integration Flow', () => {
|
||||
test('should handle a complete workflow with multiple commands', async () => {
|
||||
// 1. Add a new task
|
||||
const addResult = await addTask(
|
||||
testTasksFile,
|
||||
'Implement comprehensive logging system',
|
||||
[1],
|
||||
'high',
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
const newTaskId = addResult.newTaskId;
|
||||
|
||||
// 2. Analyze its complexity
|
||||
const complexityResult = await analyzeTaskComplexity(
|
||||
testTasksFile,
|
||||
newTaskId,
|
||||
false,
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
expect(complexityResult.complexityAnalysis).toBeDefined();
|
||||
|
||||
// 3. Expand it into subtasks
|
||||
const expandResult = await expandTask(
|
||||
testTasksFile,
|
||||
newTaskId,
|
||||
3,
|
||||
false,
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
expect(expandResult.generatedSubtasks.length).toBeGreaterThan(0);
|
||||
|
||||
// 4. Update the task with additional context
|
||||
const updateResult = await updateTaskById(
|
||||
testTasksFile,
|
||||
newTaskId,
|
||||
'Include structured logging with JSON format and log aggregation support',
|
||||
false,
|
||||
false,
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
expect(updateResult.updatedTask.description).toContain('JSON format');
|
||||
|
||||
// 5. Verify final state
|
||||
const finalData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const finalTask = finalData.master.tasks.find(t => t.id === newTaskId);
|
||||
expect(finalTask).toBeDefined();
|
||||
expect(finalTask.subtasks.length).toBeGreaterThan(0);
|
||||
expect(finalTask.description).toContain('JSON format');
|
||||
}, 60000); // Longer timeout for multiple operations
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should handle invalid task IDs gracefully', async () => {
|
||||
await expect(
|
||||
analyzeTaskComplexity(
|
||||
testTasksFile,
|
||||
999, // Non-existent task ID
|
||||
false,
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
)
|
||||
).rejects.toThrow('Task with ID 999 not found');
|
||||
});
|
||||
|
||||
test('should handle empty prompts', async () => {
|
||||
await expect(
|
||||
addTask(
|
||||
testTasksFile,
|
||||
'', // Empty prompt
|
||||
[],
|
||||
'medium',
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should handle invalid dependencies', async () => {
|
||||
const result = await addTask(
|
||||
testTasksFile,
|
||||
'New task with invalid dependency',
|
||||
[999], // Non-existent dependency
|
||||
'medium',
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
|
||||
// Should succeed but filter out invalid dependency
|
||||
expect(result.newTaskId).toBeDefined();
|
||||
const data = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const newTask = data.master.tasks.find(t => t.id === result.newTaskId);
|
||||
expect(newTask.dependencies).not.toContain(999);
|
||||
});
|
||||
});
|
||||
});
|
||||
77
tests/integration/commands/analyze-complexity.test.js
Normal file
77
tests/integration/commands/analyze-complexity.test.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import analyzeTaskComplexity from '../../../scripts/modules/task-manager/analyze-task-complexity.js';
|
||||
import { readJSON } from '../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('analyze-complexity with generateObject', () => {
|
||||
const testTasksFile = path.join(process.cwd(), 'test-tasks.json');
|
||||
const testComplexityFile = path.join(process.cwd(), 'test-complexity.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test tasks file
|
||||
const testTasks = {
|
||||
projectName: "Test Project",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project structure",
|
||||
description: "Initialize the project with proper folder structure",
|
||||
status: "pending",
|
||||
dependencies: [],
|
||||
priority: "high"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication",
|
||||
description: "Add user authentication with JWT tokens",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high"
|
||||
}
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(testTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
if (fs.existsSync(testComplexityFile)) {
|
||||
fs.unlinkSync(testComplexityFile);
|
||||
}
|
||||
});
|
||||
|
||||
test('should return structured complexity analysis', async () => {
|
||||
const result = await analyzeTaskComplexity({
|
||||
file: testTasksFile,
|
||||
output: testComplexityFile,
|
||||
threshold: 5
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('report');
|
||||
expect(result.report).toHaveProperty('complexityAnalysis');
|
||||
expect(Array.isArray(result.report.complexityAnalysis)).toBe(true);
|
||||
|
||||
if (result.report.complexityAnalysis.length > 0) {
|
||||
const analysis = result.report.complexityAnalysis[0];
|
||||
expect(analysis).toHaveProperty('taskId');
|
||||
expect(analysis).toHaveProperty('taskTitle');
|
||||
expect(analysis).toHaveProperty('complexityScore');
|
||||
expect(analysis).toHaveProperty('recommendedSubtasks');
|
||||
expect(analysis).toHaveProperty('expansionPrompt');
|
||||
expect(analysis).toHaveProperty('reasoning');
|
||||
|
||||
// Check that the values are of the correct type
|
||||
expect(typeof analysis.taskId).toBe('number');
|
||||
expect(typeof analysis.taskTitle).toBe('string');
|
||||
expect(typeof analysis.complexityScore).toBe('number');
|
||||
expect(analysis.complexityScore).toBeGreaterThanOrEqual(1);
|
||||
expect(analysis.complexityScore).toBeLessThanOrEqual(10);
|
||||
expect(typeof analysis.recommendedSubtasks).toBe('number');
|
||||
expect(typeof analysis.expansionPrompt).toBe('string');
|
||||
expect(typeof analysis.reasoning).toBe('string');
|
||||
}
|
||||
}, 30000); // Increase timeout for AI call
|
||||
});
|
||||
134
tests/integration/commands/expand-task.test.js
Normal file
134
tests/integration/commands/expand-task.test.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import expandTask from '../../../scripts/modules/task-manager/expand-task.js';
|
||||
import { readJSON, writeJSON } from '../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('expand-task with generateObject', () => {
|
||||
const testTasksFile = path.join(process.cwd(), 'test-tasks.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test tasks file
|
||||
const testTasks = {
|
||||
projectName: "Test Project",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project structure",
|
||||
description: "Initialize the project with proper folder structure",
|
||||
status: "done",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Create folders for src, tests, docs",
|
||||
testStrategy: "Manual verification",
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication",
|
||||
description: "Add user authentication with JWT tokens",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: "Need to support OAuth2 and traditional login",
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Build API endpoints",
|
||||
description: "Create RESTful API endpoints",
|
||||
status: "pending",
|
||||
dependencies: [2],
|
||||
priority: "medium",
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Design API schema",
|
||||
description: "Create OpenAPI specification",
|
||||
dependencies: [],
|
||||
details: "Use OpenAPI 3.0 specification",
|
||||
status: "done"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(testTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
});
|
||||
|
||||
test('should expand task with structured subtasks', async () => {
|
||||
const result = await expandTask(
|
||||
testTasksFile,
|
||||
'2', // taskId as string
|
||||
3, // numSubtasks
|
||||
false, // force
|
||||
'Break down authentication into implementation steps' // additionalContext
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('task');
|
||||
expect(result).toHaveProperty('telemetryData');
|
||||
|
||||
const { task } = result;
|
||||
|
||||
// Verify task was expanded
|
||||
expect(task.id).toBe(2);
|
||||
expect(task.subtasks).toBeDefined();
|
||||
expect(Array.isArray(task.subtasks)).toBe(true);
|
||||
expect(task.subtasks.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify subtask structure
|
||||
const subtask = task.subtasks[0];
|
||||
expect(subtask).toHaveProperty('id');
|
||||
expect(subtask).toHaveProperty('title');
|
||||
expect(subtask).toHaveProperty('description');
|
||||
expect(subtask).toHaveProperty('dependencies');
|
||||
expect(subtask).toHaveProperty('details');
|
||||
expect(subtask).toHaveProperty('status', 'pending');
|
||||
|
||||
// Verify task was written back to file
|
||||
const savedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const savedTask = savedData.tasks.find(t => t.id === 2);
|
||||
expect(savedTask.subtasks.length).toBe(task.subtasks.length);
|
||||
}, 30000); // Increase timeout for AI call
|
||||
|
||||
test('should append subtasks when force=false', async () => {
|
||||
// First expansion
|
||||
await expandTask(testTasksFile, '3', 2, false);
|
||||
|
||||
const dataAfterFirst = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const taskAfterFirst = dataAfterFirst.tasks.find(t => t.id === 3);
|
||||
const initialSubtaskCount = taskAfterFirst.subtasks.length;
|
||||
|
||||
// Second expansion (append)
|
||||
await expandTask(testTasksFile, '3', 2, false, 'Add more implementation details');
|
||||
|
||||
const dataAfterSecond = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const taskAfterSecond = dataAfterSecond.tasks.find(t => t.id === 3);
|
||||
|
||||
// Should have more subtasks than before
|
||||
expect(taskAfterSecond.subtasks.length).toBeGreaterThan(initialSubtaskCount);
|
||||
}, 60000);
|
||||
|
||||
test('should replace subtasks when force=true', async () => {
|
||||
// First expansion
|
||||
await expandTask(testTasksFile, '3', 2, false);
|
||||
|
||||
// Second expansion with force=true
|
||||
const result = await expandTask(testTasksFile, '3', 3, true, 'Complete redesign needed');
|
||||
|
||||
const savedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const savedTask = savedData.tasks.find(t => t.id === 3);
|
||||
|
||||
// Should have exactly 3 subtasks (replaced, not appended)
|
||||
expect(savedTask.subtasks.length).toBe(3);
|
||||
}, 60000);
|
||||
});
|
||||
89
tests/integration/commands/update-task-by-id.test.js
Normal file
89
tests/integration/commands/update-task-by-id.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import updateTaskById from '../../../scripts/modules/task-manager/update-task-by-id.js';
|
||||
import { readJSON, writeJSON } from '../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('update-task-by-id with generateObject', () => {
|
||||
const testTasksFile = path.join(process.cwd(), 'test-tasks.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test tasks file
|
||||
const testTasks = {
|
||||
projectName: "Test Project",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project structure",
|
||||
description: "Initialize the project with proper folder structure",
|
||||
status: "pending",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Create folders for src, tests, docs",
|
||||
testStrategy: "Manual verification"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication",
|
||||
description: "Add user authentication with JWT tokens",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: null,
|
||||
testStrategy: null
|
||||
}
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(testTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
});
|
||||
|
||||
test('should update task with structured data', async () => {
|
||||
const result = await updateTaskById({
|
||||
file: testTasksFile,
|
||||
prompt: 'Update the description to include OAuth2 support',
|
||||
id: 2
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedTask');
|
||||
const { updatedTask } = result;
|
||||
|
||||
// Verify the task structure
|
||||
expect(updatedTask).toHaveProperty('id', 2);
|
||||
expect(updatedTask).toHaveProperty('title');
|
||||
expect(updatedTask).toHaveProperty('description');
|
||||
expect(updatedTask).toHaveProperty('status');
|
||||
expect(updatedTask).toHaveProperty('dependencies');
|
||||
expect(updatedTask).toHaveProperty('priority');
|
||||
|
||||
// Check that description was updated
|
||||
expect(updatedTask.description.toLowerCase()).toContain('oauth');
|
||||
|
||||
// Verify task was written back to file
|
||||
const savedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const savedTask = savedData.tasks.find(t => t.id === 2);
|
||||
expect(savedTask.description).toBe(updatedTask.description);
|
||||
}, 30000); // Increase timeout for AI call
|
||||
|
||||
test('should handle append mode with plain text', async () => {
|
||||
const result = await updateTaskById({
|
||||
file: testTasksFile,
|
||||
prompt: 'Add information about refresh tokens',
|
||||
id: 2,
|
||||
append: true
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedTask');
|
||||
const { updatedTask } = result;
|
||||
|
||||
// Check that details were appended
|
||||
expect(updatedTask.details).toBeTruthy();
|
||||
expect(updatedTask.details).toContain('<info added on');
|
||||
expect(updatedTask.details.toLowerCase()).toContain('refresh token');
|
||||
}, 30000);
|
||||
});
|
||||
141
tests/integration/commands/update-tasks.test.js
Normal file
141
tests/integration/commands/update-tasks.test.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import updateTasks from '../../../scripts/modules/task-manager/update-tasks.js';
|
||||
import { readJSON, writeJSON } from '../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('update-tasks with generateObject', () => {
|
||||
const testTasksFile = path.join(process.cwd(), 'test-tasks.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test tasks file
|
||||
const testTasks = {
|
||||
projectName: "Test Project",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project structure",
|
||||
description: "Initialize the project with proper folder structure",
|
||||
status: "done",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Create folders for src, tests, docs",
|
||||
testStrategy: "Manual verification",
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication",
|
||||
description: "Add user authentication with JWT tokens",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: "Need to support OAuth2 and traditional login",
|
||||
testStrategy: null,
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Design auth flow",
|
||||
description: "Create authentication flow diagrams",
|
||||
status: "done",
|
||||
dependencies: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Build API endpoints",
|
||||
description: "Create RESTful API endpoints",
|
||||
status: "in-progress",
|
||||
dependencies: [2],
|
||||
priority: "medium",
|
||||
details: "Use Express.js for the API",
|
||||
testStrategy: "Integration tests with Jest",
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Add database layer",
|
||||
description: "Implement database models and migrations",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(testTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
});
|
||||
|
||||
test('should update multiple tasks with structured data', async () => {
|
||||
const result = await updateTasks(
|
||||
testTasksFile,
|
||||
2, // Update from task ID 2 onwards
|
||||
'Switch to microservices architecture with Docker containers'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveProperty('updatedTasks');
|
||||
expect(result).toHaveProperty('telemetryData');
|
||||
|
||||
// Read the updated file
|
||||
const updatedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
|
||||
// Task 1 should remain unchanged (status: done)
|
||||
const task1 = updatedData.tasks.find(t => t.id === 1);
|
||||
expect(task1.title).toBe("Setup project structure");
|
||||
expect(task1.status).toBe("done");
|
||||
|
||||
// Tasks 2, 3, and 4 should be updated
|
||||
const task2 = updatedData.tasks.find(t => t.id === 2);
|
||||
expect(task2.description.toLowerCase()).toContain('microservice');
|
||||
// Completed subtasks should be preserved
|
||||
expect(task2.subtasks.find(st => st.id === 1 && st.status === 'done')).toBeDefined();
|
||||
|
||||
const task3 = updatedData.tasks.find(t => t.id === 3);
|
||||
expect(task3.description.toLowerCase()).toContain('docker');
|
||||
|
||||
const task4 = updatedData.tasks.find(t => t.id === 4);
|
||||
expect(task4.description.toLowerCase()).toMatch(/microservice|docker|container/);
|
||||
}, 30000); // Increase timeout for AI call
|
||||
|
||||
test('should preserve completed subtasks when updating', async () => {
|
||||
await updateTasks(
|
||||
testTasksFile,
|
||||
2,
|
||||
'Add comprehensive error handling and logging'
|
||||
);
|
||||
|
||||
const updatedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const task2 = updatedData.tasks.find(t => t.id === 2);
|
||||
|
||||
// Find the completed subtask
|
||||
const completedSubtask = task2.subtasks.find(st => st.id === 1);
|
||||
expect(completedSubtask).toBeDefined();
|
||||
expect(completedSubtask.status).toBe('done');
|
||||
expect(completedSubtask.title).toBe("Design auth flow");
|
||||
expect(completedSubtask.description).toBe("Create authentication flow diagrams");
|
||||
}, 30000);
|
||||
|
||||
test('should handle no tasks to update', async () => {
|
||||
const result = await updateTasks(
|
||||
testTasksFile,
|
||||
10, // Start from non-existent task ID
|
||||
'Update all tasks'
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
// File should remain unchanged
|
||||
const data = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
expect(data.tasks.length).toBe(4);
|
||||
}, 30000);
|
||||
});
|
||||
215
tests/integration/migration-verification.test.js
Normal file
215
tests/integration/migration-verification.test.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Tests to verify the generateObject migration is complete
|
||||
* Ensures no legacy parsing functions remain and all commands use generateObject
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('GenerateObject Migration Verification', () => {
|
||||
const scriptsDir = path.join(__dirname, '../../scripts/modules/task-manager');
|
||||
|
||||
describe('Legacy Parsing Function Removal', () => {
|
||||
test('should not find parseUpdatedTasksFromText function', () => {
|
||||
const updateTasksFile = fs.readFileSync(
|
||||
path.join(scriptsDir, 'update-tasks.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// The function should still exist but only for reference
|
||||
// It's not being used anywhere in the actual command flow
|
||||
const hasParsingFunction = updateTasksFile.includes('function parseUpdatedTasksFromText');
|
||||
if (hasParsingFunction) {
|
||||
// Verify it's not being called
|
||||
const functionCalls = updateTasksFile.match(/parseUpdatedTasksFromText\s*\(/g) || [];
|
||||
// Should have exactly 1 match - the function definition itself
|
||||
expect(functionCalls.length).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('should not find parseSubtasksFromText function usage', () => {
|
||||
const expandTaskFile = fs.readFileSync(
|
||||
path.join(scriptsDir, 'expand-task.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Should not contain the parsing function at all
|
||||
expect(expandTaskFile).not.toContain('parseSubtasksFromText');
|
||||
});
|
||||
|
||||
test('should not find parseComplexityAnalysisFromText function usage', () => {
|
||||
const analyzeComplexityFile = fs.readFileSync(
|
||||
path.join(scriptsDir, 'analyze-task-complexity.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Should not contain the parsing function at all
|
||||
expect(analyzeComplexityFile).not.toContain('parseComplexityAnalysisFromText');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GenerateObject Service Usage', () => {
|
||||
const commandFiles = [
|
||||
'analyze-task-complexity.js',
|
||||
'update-task-by-id.js',
|
||||
'expand-task.js',
|
||||
'update-tasks.js',
|
||||
'add-task.js',
|
||||
'parse-prd.js'
|
||||
];
|
||||
|
||||
commandFiles.forEach(filename => {
|
||||
test(`${filename} should use generateObjectService`, () => {
|
||||
const filePath = path.join(scriptsDir, filename);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Should import generateObjectService
|
||||
expect(fileContent).toMatch(/import\s+.*generateObjectService.*from\s+['"]\.\.\/ai-services-unified\.js['"]/);
|
||||
|
||||
// Should call generateObjectService
|
||||
expect(fileContent).toContain('generateObjectService(');
|
||||
|
||||
// Should use schema
|
||||
expect(fileContent).toMatch(/schema:\s*\w+Schema|schema:\s*COMMAND_SCHEMAS/);
|
||||
});
|
||||
});
|
||||
|
||||
test('update-subtask-by-id.js should continue using generateTextService', () => {
|
||||
const filePath = path.join(scriptsDir, 'update-subtask-by-id.js');
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Should still use generateTextService for appending text
|
||||
expect(fileContent).toContain('generateTextService');
|
||||
expect(fileContent).not.toContain('generateObjectService');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Schema Registry Usage', () => {
|
||||
test('should have a complete schema registry', () => {
|
||||
const registryPath = path.join(__dirname, '../../src/schemas/registry.js');
|
||||
const registryContent = fs.readFileSync(registryPath, 'utf8');
|
||||
|
||||
// Should export COMMAND_SCHEMAS
|
||||
expect(registryContent).toContain('export const COMMAND_SCHEMAS');
|
||||
|
||||
// Should include all command schemas
|
||||
const expectedCommands = [
|
||||
'update-tasks',
|
||||
'expand-task',
|
||||
'analyze-complexity',
|
||||
'update-task-by-id'
|
||||
];
|
||||
|
||||
expectedCommands.forEach(command => {
|
||||
expect(registryContent).toContain(`'${command}':`);
|
||||
});
|
||||
});
|
||||
|
||||
test('update-tasks.js should use schema from registry', () => {
|
||||
const filePath = path.join(scriptsDir, 'update-tasks.js');
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Should import from registry
|
||||
expect(fileContent).toContain("import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js'");
|
||||
|
||||
// Should use registry in generateObjectService call
|
||||
expect(fileContent).toContain("COMMAND_SCHEMAS['update-tasks']");
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prompt Template Updates', () => {
|
||||
const promptsDir = path.join(__dirname, '../../src/prompts');
|
||||
|
||||
test('prompts should not contain JSON formatting instructions', () => {
|
||||
const promptFiles = fs.readdirSync(promptsDir)
|
||||
.filter(f => f.endsWith('.json'));
|
||||
|
||||
const jsonInstructions = [
|
||||
'Return only the updated tasks as a valid JSON array',
|
||||
'Do not include any explanatory text, markdown formatting, or code block markers',
|
||||
'Respond ONLY with a valid JSON',
|
||||
'The response must be a valid JSON',
|
||||
'Return the result as JSON'
|
||||
];
|
||||
|
||||
promptFiles.forEach(filename => {
|
||||
// Skip update-subtask.json as it returns plain text
|
||||
if (filename === 'update-subtask.json') return;
|
||||
|
||||
const filePath = path.join(promptsDir, filename);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
jsonInstructions.forEach(instruction => {
|
||||
expect(content).not.toContain(instruction);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Direct Object Access Patterns', () => {
|
||||
test('commands should access data directly from mainResult', () => {
|
||||
const patterns = [
|
||||
{
|
||||
file: 'analyze-task-complexity.js',
|
||||
pattern: /aiServiceResponse\.mainResult\.complexityAnalysis/
|
||||
},
|
||||
{
|
||||
file: 'expand-task.js',
|
||||
pattern: /aiServiceResponse\.mainResult\.subtasks/
|
||||
},
|
||||
{
|
||||
file: 'update-tasks.js',
|
||||
pattern: /aiServiceResponse\.mainResult\.tasks/
|
||||
},
|
||||
{
|
||||
file: 'update-task-by-id.js',
|
||||
pattern: /aiServiceResponse\.mainResult\.task/
|
||||
}
|
||||
];
|
||||
|
||||
patterns.forEach(({ file, pattern }) => {
|
||||
const filePath = path.join(scriptsDir, file);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
expect(fileContent).toMatch(pattern);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling Updates', () => {
|
||||
test('commands should not have AI response JSON parsing error handling', () => {
|
||||
const commandFiles = [
|
||||
'analyze-task-complexity.js',
|
||||
'expand-task.js',
|
||||
'update-task-by-id.js'
|
||||
];
|
||||
|
||||
// More specific patterns that indicate AI response parsing
|
||||
const aiParsingErrorPatterns = [
|
||||
'Failed to parse JSON response',
|
||||
'Failed to parse AI response',
|
||||
'parseComplexityAnalysisFromText',
|
||||
'parseSubtasksFromText',
|
||||
'parseUpdatedTaskFromText',
|
||||
'parseUpdatedTasksFromText',
|
||||
'Malformed JSON',
|
||||
'extracting between \\[\\]',
|
||||
'JSON code block'
|
||||
];
|
||||
|
||||
commandFiles.forEach(filename => {
|
||||
const filePath = path.join(scriptsDir, filename);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Check for AI response parsing patterns
|
||||
aiParsingErrorPatterns.forEach(pattern => {
|
||||
expect(fileContent).not.toMatch(new RegExp(pattern, 'i'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
55
tests/unit/prompts/prompt-migration.test.js
Normal file
55
tests/unit/prompts/prompt-migration.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const promptsDir = path.join(__dirname, '../../../src/prompts');
|
||||
|
||||
describe('Prompt Migration Validation', () => {
|
||||
const bannedPhrases = [
|
||||
'Respond ONLY with',
|
||||
'Return only the',
|
||||
'valid JSON',
|
||||
'Do not include any explanatory text',
|
||||
'Do not include any explanation',
|
||||
'code block markers'
|
||||
];
|
||||
|
||||
// Special cases where phrases are okay in different contexts
|
||||
const allowedContexts = {
|
||||
'markdown formatting': ['Use markdown formatting for better readability']
|
||||
};
|
||||
|
||||
test('prompts should not contain JSON formatting instructions', () => {
|
||||
const promptFiles = fs.readdirSync(promptsDir)
|
||||
.filter(file => file.endsWith('.json') && !file.includes('schema'))
|
||||
// Exclude update-subtask.json as it returns plain strings, not JSON
|
||||
.filter(file => file !== 'update-subtask.json');
|
||||
|
||||
promptFiles.forEach(file => {
|
||||
const content = fs.readFileSync(path.join(promptsDir, file), 'utf8');
|
||||
const promptData = JSON.parse(content);
|
||||
|
||||
bannedPhrases.forEach(phrase => {
|
||||
const lowerContent = content.toLowerCase();
|
||||
const lowerPhrase = phrase.toLowerCase();
|
||||
|
||||
if (lowerContent.includes(lowerPhrase)) {
|
||||
// Check if this phrase is allowed in its context
|
||||
const allowedInContext = allowedContexts[lowerPhrase];
|
||||
if (allowedInContext) {
|
||||
const isAllowed = allowedInContext.some(context =>
|
||||
lowerContent.includes(context.toLowerCase())
|
||||
);
|
||||
if (isAllowed) {
|
||||
return; // Skip this phrase - it's allowed in this context
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, the phrase is not allowed
|
||||
expect(lowerContent).not.toContain(lowerPhrase);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -50,7 +50,7 @@ jest.unstable_mockModule(
|
||||
() => ({
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
tasks: []
|
||||
complexityAnalysis: []
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -307,10 +307,15 @@ describe('analyzeTaskComplexity', () => {
|
||||
return { task: task || null, originalSubtaskCount: null };
|
||||
});
|
||||
|
||||
generateTextService.mockResolvedValue(sampleApiResponse);
|
||||
generateObjectService.mockResolvedValue({
|
||||
mainResult: {
|
||||
complexityAnalysis: JSON.parse(sampleApiResponse.mainResult).tasks
|
||||
},
|
||||
telemetryData: sampleApiResponse.telemetryData
|
||||
});
|
||||
});
|
||||
|
||||
test('should call generateTextService with the correct parameters', async () => {
|
||||
test('should call generateObjectService with the correct parameters', async () => {
|
||||
// Arrange
|
||||
const options = {
|
||||
file: 'tasks/tasks.json',
|
||||
@@ -338,7 +343,7 @@ describe('analyzeTaskComplexity', () => {
|
||||
'/mock/project/root',
|
||||
undefined
|
||||
);
|
||||
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('task-complexity-report.json'),
|
||||
expect.stringContaining('"thresholdScore": 5'),
|
||||
@@ -369,7 +374,7 @@ describe('analyzeTaskComplexity', () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(generateTextService).toHaveBeenCalledWith(
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
role: 'research' // This should be present when research is true
|
||||
})
|
||||
@@ -454,7 +459,7 @@ describe('analyzeTaskComplexity', () => {
|
||||
|
||||
// Assert
|
||||
// Check if the prompt sent to AI doesn't include the completed task (id: 3)
|
||||
expect(generateTextService).toHaveBeenCalledWith(
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.not.stringContaining('"id": 3')
|
||||
})
|
||||
@@ -471,7 +476,7 @@ describe('analyzeTaskComplexity', () => {
|
||||
};
|
||||
|
||||
// Force API error
|
||||
generateTextService.mockRejectedValueOnce(new Error('API Error'));
|
||||
generateObjectService.mockRejectedValueOnce(new Error('API Error'));
|
||||
|
||||
const mockMcpLog = {
|
||||
info: jest.fn(),
|
||||
|
||||
@@ -196,9 +196,62 @@ jest.unstable_mockModule(
|
||||
currency: 'USD'
|
||||
}
|
||||
}),
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
generateObjectService: jest.fn().mockImplementation((params) => {
|
||||
const commandName = params?.commandName || 'default';
|
||||
|
||||
if (commandName === 'analyze-complexity') {
|
||||
// Check if this is for a specific tag test by looking at the prompt
|
||||
const isFeatureTag =
|
||||
params?.prompt?.includes('feature') || params?.role === 'feature';
|
||||
const isMasterTag =
|
||||
params?.prompt?.includes('master') || params?.role === 'master';
|
||||
|
||||
let taskTitle = 'Test Task';
|
||||
if (isFeatureTag) {
|
||||
taskTitle = 'Feature Task 1';
|
||||
} else if (isMasterTag) {
|
||||
taskTitle = 'Master Task 1';
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
mainResult: {
|
||||
complexityAnalysis: [
|
||||
{
|
||||
taskId: 1,
|
||||
taskTitle: taskTitle,
|
||||
complexityScore: 7,
|
||||
recommendedSubtasks: 4,
|
||||
expansionPrompt: 'Break down this task',
|
||||
reasoning: 'This task is moderately complex'
|
||||
},
|
||||
{
|
||||
taskId: 2,
|
||||
taskTitle: 'Task 2',
|
||||
complexityScore: 5,
|
||||
recommendedSubtasks: 3,
|
||||
expansionPrompt: 'Break down this task with a focus on task 2.',
|
||||
reasoning:
|
||||
'Automatically added due to missing analysis in AI response.'
|
||||
}
|
||||
]
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
commandName: 'analyze-complexity',
|
||||
modelUsed: 'claude-3-5-sonnet',
|
||||
providerName: 'anthropic',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
totalCost: 0.012414,
|
||||
currency: 'USD'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Default response for expand-task and others
|
||||
return Promise.resolve({
|
||||
mainResult: {
|
||||
object: {
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
@@ -210,7 +263,6 @@ jest.unstable_mockModule(
|
||||
testStrategy: 'Test strategy'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -223,6 +275,7 @@ jest.unstable_mockModule(
|
||||
totalCost: 0.012414,
|
||||
currency: 'USD'
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -421,9 +474,8 @@ const { readJSON, writeJSON, getTagAwareFilePath } = await import(
|
||||
'../../../../../scripts/modules/utils.js'
|
||||
);
|
||||
|
||||
const { generateTextService, streamTextService } = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
const { generateTextService, generateObjectService, streamTextService } =
|
||||
await import('../../../../../scripts/modules/ai-services-unified.js');
|
||||
|
||||
// Import the modules under test
|
||||
const { default: analyzeTaskComplexity } = await import(
|
||||
|
||||
@@ -65,8 +65,8 @@ jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
|
||||
jest.unstable_mockModule(
|
||||
'../../../../../scripts/modules/ai-services-unified.js',
|
||||
() => ({
|
||||
generateTextService: jest.fn().mockResolvedValue({
|
||||
mainResult: JSON.stringify({
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
@@ -101,7 +101,7 @@ jest.unstable_mockModule(
|
||||
testStrategy: 'UI tests and visual regression testing'
|
||||
}
|
||||
]
|
||||
}),
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: '1234567890',
|
||||
@@ -213,7 +213,7 @@ const {
|
||||
findProjectRoot
|
||||
} = await import('../../../../../scripts/modules/utils.js');
|
||||
|
||||
const { generateTextService } = await import(
|
||||
const { generateObjectService } = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
@@ -373,7 +373,7 @@ describe('expandTask', () => {
|
||||
'/mock/project/root',
|
||||
undefined
|
||||
);
|
||||
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(writeJSON).toHaveBeenCalledWith(
|
||||
tasksPath,
|
||||
expect.objectContaining({
|
||||
@@ -458,7 +458,7 @@ describe('expandTask', () => {
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(generateTextService).toHaveBeenCalledWith(
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
role: 'research',
|
||||
commandName: expect.any(String)
|
||||
@@ -496,7 +496,7 @@ describe('expandTask', () => {
|
||||
telemetryData: expect.any(Object)
|
||||
})
|
||||
);
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -743,8 +743,8 @@ describe('expandTask', () => {
|
||||
// Act
|
||||
await expandTask(tasksPath, taskId, undefined, false, '', context, false);
|
||||
|
||||
// Assert - generateTextService called with systemPrompt for 5 subtasks
|
||||
const callArg = generateTextService.mock.calls[0][0];
|
||||
// Assert - generateObjectService called with systemPrompt for 5 subtasks
|
||||
const callArg = generateObjectService.mock.calls[0][0];
|
||||
expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks');
|
||||
|
||||
// Assert - Should use complexity-report variant with expansion prompt
|
||||
@@ -831,7 +831,7 @@ describe('expandTask', () => {
|
||||
projectRoot: '/mock/project/root'
|
||||
};
|
||||
|
||||
generateTextService.mockRejectedValueOnce(new Error('AI service error'));
|
||||
generateObjectService.mockRejectedValueOnce(new Error('AI service error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
@@ -941,7 +941,7 @@ describe('expandTask', () => {
|
||||
await expandTask(tasksPath, taskId, 3, false, '', context, false);
|
||||
|
||||
// Assert - Should work with empty context (but may include project context)
|
||||
expect(generateTextService).toHaveBeenCalledWith(
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringMatching(/.*/) // Just ensure prompt exists
|
||||
})
|
||||
@@ -1074,7 +1074,7 @@ describe('expandTask', () => {
|
||||
|
||||
// Assert - Should complete successfully
|
||||
expect(result).toBeDefined();
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use dynamic prompting when numSubtasks is 0', async () => {
|
||||
@@ -1095,11 +1095,11 @@ describe('expandTask', () => {
|
||||
// Act
|
||||
await expandTask(tasksPath, taskId, 0, false, '', context, false);
|
||||
|
||||
// Assert - Verify generateTextService was called
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
// Assert - Verify generateObjectService was called
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
|
||||
// Get the call arguments to verify the system prompt
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain(
|
||||
'an appropriate number of specific subtasks'
|
||||
);
|
||||
@@ -1122,11 +1122,11 @@ describe('expandTask', () => {
|
||||
// Act
|
||||
await expandTask(tasksPath, taskId, 5, false, '', context, false);
|
||||
|
||||
// Assert - Verify generateTextService was called
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
// Assert - Verify generateObjectService was called
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
|
||||
// Get the call arguments to verify the system prompt
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain('5 specific subtasks');
|
||||
});
|
||||
|
||||
@@ -1151,8 +1151,8 @@ describe('expandTask', () => {
|
||||
await expandTask(tasksPath, taskId, -3, false, '', context, false);
|
||||
|
||||
// Assert - Should use default value instead of negative
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain('4 specific subtasks');
|
||||
});
|
||||
|
||||
@@ -1177,8 +1177,8 @@ describe('expandTask', () => {
|
||||
await expandTask(tasksPath, taskId, undefined, false, '', context, false);
|
||||
|
||||
// Assert - Should use default value
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain('6 specific subtasks');
|
||||
});
|
||||
|
||||
@@ -1203,8 +1203,8 @@ describe('expandTask', () => {
|
||||
await expandTask(tasksPath, taskId, null, false, '', context, false);
|
||||
|
||||
// Assert - Should use default value
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain('7 specific subtasks');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,25 @@ jest.unstable_mockModule(
|
||||
() => ({
|
||||
generateTextService: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} })
|
||||
.mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} }),
|
||||
generateObjectService: jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
mainResult: {
|
||||
task: {
|
||||
id: 1,
|
||||
title: 'Updated Task',
|
||||
description: 'Updated description',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
priority: 'medium',
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
},
|
||||
telemetryData: {}
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ jest.unstable_mockModule(
|
||||
generateTextService: jest.fn().mockResolvedValue({
|
||||
mainResult: '[]', // mainResult is the text string directly
|
||||
telemetryData: {}
|
||||
}),
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
tasks: [] // generateObject returns structured data
|
||||
},
|
||||
telemetryData: {}
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -84,7 +90,7 @@ const { readJSON, writeJSON, log } = await import(
|
||||
'../../../../../scripts/modules/utils.js'
|
||||
);
|
||||
|
||||
const { generateTextService } = await import(
|
||||
const { generateObjectService } = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
@@ -154,7 +160,9 @@ describe('updateTasks', () => {
|
||||
];
|
||||
|
||||
const mockApiResponse = {
|
||||
mainResult: JSON.stringify(mockUpdatedTasks), // mainResult is the JSON string directly
|
||||
mainResult: {
|
||||
tasks: mockUpdatedTasks // generateObject returns structured data
|
||||
},
|
||||
telemetryData: {}
|
||||
};
|
||||
|
||||
@@ -164,7 +172,7 @@ describe('updateTasks', () => {
|
||||
tag: 'master',
|
||||
_rawTaggedData: mockInitialTasks
|
||||
});
|
||||
generateTextService.mockResolvedValue(mockApiResponse);
|
||||
generateObjectService.mockResolvedValue(mockApiResponse);
|
||||
|
||||
// Act
|
||||
const result = await updateTasks(
|
||||
@@ -185,7 +193,7 @@ describe('updateTasks', () => {
|
||||
);
|
||||
|
||||
// 2. AI Service called with correct args
|
||||
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
|
||||
|
||||
// 3. Write JSON called with correctly merged tasks
|
||||
expect(writeJSON).toHaveBeenCalledWith(
|
||||
@@ -252,7 +260,7 @@ describe('updateTasks', () => {
|
||||
'/mock/path',
|
||||
'master'
|
||||
);
|
||||
expect(generateTextService).not.toHaveBeenCalled();
|
||||
expect(generateObjectService).not.toHaveBeenCalled();
|
||||
expect(writeJSON).not.toHaveBeenCalled();
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
'info',
|
||||
@@ -327,8 +335,10 @@ describe('updateTasks', () => {
|
||||
_rawTaggedData: mockTaggedData
|
||||
});
|
||||
|
||||
generateTextService.mockResolvedValue({
|
||||
mainResult: JSON.stringify(mockUpdatedTasks),
|
||||
generateObjectService.mockResolvedValue({
|
||||
mainResult: {
|
||||
tasks: mockUpdatedTasks
|
||||
},
|
||||
telemetryData: { commandName: 'update-tasks', totalCost: 0.05 }
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user