Compare commits

...

12 Commits

Author SHA1 Message Date
Ralph Khreish
58cc143aef remove "aiServiceResponse" duplicate
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-02 16:18:47 +02:00
Ralph Khreish
3aac7ac349 chore: remove unused responseText variable
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-02 15:44:55 +02:00
Ralph Khreish
f68330efb3 chore: apply requested changes 2025-10-02 15:30:33 +02:00
Ralph Khreish
1d197fe9c2 chore: fix CI
- adapt tests to new codebase
- improve integration tests by reducing the amount of tasks (to make the tests faster)
2025-10-02 14:53:11 +02:00
Ralph Khreish
7660a29a1a chore: implement requested changes 2025-10-02 11:54:30 +02:00
Ben Vargas
0aaa105021 test: remove integration tests that require real API calls
Integration tests that make real API calls cannot run in CI without
proper API keys. These tests should either be mocked or run in a
separate test suite with appropriate infrastructure.
2025-10-02 11:54:29 +02:00
Ben Vargas
6b15788c58 chore: remove generateObject documentation files 2025-10-02 11:54:29 +02:00
Ben Vargas
a2de49dd90 chore: fix formatting issues 2025-10-02 11:54:29 +02:00
Ben Vargas
2063dc4b7d fix: apply formatting to modified files 2025-10-02 11:54:29 +02:00
Ben Vargas
7e6319a56f chore: add changeset for generateObject migration 2025-10-02 11:54:29 +02:00
Ben Vargas
b0504a00d5 fix: enforce sequential subtask ID numbering in AI prompts
Fixed issue where AI was generating inconsistent subtask IDs (101-105, 601-603)
instead of sequential numbering (1, 2, 3...) after the generateObject migration.

Changes:
- Updated all expand-task prompt variants with forceful "CRITICAL" instructions
- Made ID requirements explicit with examples: id={{nextSubtaskId}}, id={{nextSubtaskId}}+1
- Added warning not to use parent task ID in subtask numbering
- Removed parseSubtasksFromText post-processing that was overwriting AI-generated IDs

This ensures subtasks display correctly as X.1, X.2, X.3 format and the
`tm show X.Y` command works as expected.
2025-10-02 11:54:29 +02:00
Ben Vargas
b16023ab2f feat: Complete generateObject migration with JSON mode support 2025-10-02 11:54:29 +02:00
34 changed files with 862 additions and 1034 deletions

View File

@@ -0,0 +1,30 @@
---
"task-master-ai": minor
---
Migrate AI services to use generateObject for structured data generation
This update migrates all AI service calls from generateText to generateObject, ensuring more reliable and structured responses across all commands.
### Key Changes:
- **Unified AI Service**: Replaced separate generateText implementations with a single generateObjectService that handles structured data generation
- **JSON Mode Support**: Added proper JSON mode configuration for providers that support it (OpenAI, Anthropic, Google, Groq)
- **Schema Validation**: Integrated Zod schemas for all AI-generated content with automatic validation
- **Provider Compatibility**: Maintained compatibility with all existing providers while leveraging their native structured output capabilities
- **Improved Reliability**: Structured output generation reduces parsing errors and ensures consistent data formats
### Technical Improvements:
- Centralized provider configuration in `ai-providers-unified.js`
- Added `generateObject` support detection for each provider
- Implemented proper error handling for schema validation failures
- Maintained backward compatibility with existing prompt structures
### Bug Fixes:
- Fixed subtask ID numbering issue where AI was generating inconsistent IDs (101-105, 601-603) instead of sequential numbering (1, 2, 3...)
- Enhanced prompt instructions to enforce proper ID generation patterns
- Ensured subtasks display correctly as X.1, X.2, X.3 format
This migration improves the reliability and consistency of AI-generated content throughout the Task Master application.

View File

@@ -158,10 +158,18 @@ export function displayUpgradeNotification(
export async function performAutoUpdate( export async function performAutoUpdate(
latestVersion: string latestVersion: string
): Promise<boolean> { ): Promise<boolean> {
if (process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' || process.env.CI) { if (
console.log( process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' ||
chalk.dim('Skipping auto-update (TASKMASTER_SKIP_AUTO_UPDATE/CI).') process.env.CI ||
); process.env.NODE_ENV === 'test'
) {
const reason =
process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1'
? 'TASKMASTER_SKIP_AUTO_UPDATE=1'
: process.env.CI
? 'CI environment'
: 'NODE_ENV=test';
console.log(chalk.dim(`Skipping auto-update (${reason})`));
return false; return false;
} }
const spinner = ora({ const spinner = ora({

View File

@@ -75,13 +75,50 @@ function generateExampleFromSchema(schema) {
return result; return result;
case 'ZodString': 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 && maxCheck) {
return (
'<string between ' +
minCheck.value +
'-' +
maxCheck.value +
' characters>'
);
} else if (minCheck) {
return '<string with at least ' + minCheck.value + ' characters>';
} else if (maxCheck) {
return '<string up to ' + maxCheck.value + ' characters>';
}
}
return '<string>';
case 'ZodNumber': 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': case 'ZodBoolean':
return false; return '<boolean>';
case 'ZodArray': case 'ZodArray':
const elementExample = generateExampleFromSchema(def.type); const elementExample = generateExampleFromSchema(def.type);

View File

@@ -2,7 +2,6 @@ import path from 'path';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import Table from 'cli-table3'; import Table from 'cli-table3';
import { z } from 'zod';
import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search
import { import {
@@ -29,6 +28,7 @@ import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js'; import { getPromptManager } from '../prompt-manager.js';
import ContextGatherer from '../utils/contextGatherer.js'; import ContextGatherer from '../utils/contextGatherer.js';
import generateTaskFiles from './generate-task-files.js'; import generateTaskFiles from './generate-task-files.js';
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
import { import {
TASK_PRIORITY_OPTIONS, TASK_PRIORITY_OPTIONS,
DEFAULT_TASK_PRIORITY, DEFAULT_TASK_PRIORITY,
@@ -36,26 +36,6 @@ import {
normalizeTaskPriority normalizeTaskPriority
} from '../../../src/constants/task-priority.js'; } 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 * Get all tasks from all tags
* @param {Object} rawData - The raw tagged data object * @param {Object} rawData - The raw tagged data object
@@ -451,7 +431,7 @@ async function addTask(
role: serviceRole, role: serviceRole,
session: session, session: session,
projectRoot: projectRoot, projectRoot: projectRoot,
schema: AiTaskDataSchema, schema: COMMAND_SCHEMAS['add-task'],
objectName: 'newTaskData', objectName: 'newTaskData',
systemPrompt: systemPrompt, systemPrompt: systemPrompt,
prompt: userPrompt, prompt: userPrompt,

View File

@@ -11,7 +11,8 @@ import {
displayAiUsageSummary displayAiUsageSummary
} from '../ui.js'; } 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 { import {
getDebugFlag, getDebugFlag,
@@ -29,46 +30,6 @@ import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js'; import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks } from '../utils.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 * Analyzes task complexity and generates expansion recommendations
* @param {Object} options Command options * @param {Object} options Command options
@@ -446,12 +407,14 @@ async function analyzeTaskComplexity(options, context = {}) {
try { try {
const role = useResearch ? 'research' : 'main'; const role = useResearch ? 'research' : 'main';
aiServiceResponse = await generateTextService({ aiServiceResponse = await generateObjectService({
prompt, prompt,
systemPrompt, systemPrompt,
role, role,
session, session,
projectRoot, projectRoot,
schema: COMMAND_SCHEMAS['analyze-complexity'],
objectName: 'complexityAnalysis',
commandName: 'analyze-complexity', commandName: 'analyze-complexity',
outputType: mcpLog ? 'mcp' : 'cli' outputType: mcpLog ? 'mcp' : 'cli'
}); });
@@ -463,63 +426,15 @@ async function analyzeTaskComplexity(options, context = {}) {
if (outputFormat === 'text') { if (outputFormat === 'text') {
readline.clearLine(process.stdout, 0); readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0); readline.cursorTo(process.stdout, 0);
console.log( console.log(chalk.green('AI service call complete.'));
chalk.green('AI service call complete. Parsing response...')
);
} }
reportLog('Parsing complexity analysis from text response...', 'info'); // With generateObject, we get structured data directly
try { complexityAnalysis = aiServiceResponse.mainResult.complexityAnalysis;
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( reportLog(
'Warning: Response does not appear to be a JSON array.', `Received ${complexityAnalysis.length} complexity analyses from AI.`,
'warn' 'info'
); );
}
}
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;
}
const taskIds = tasksData.tasks.map((t) => t.id); const taskIds = tasksData.tasks.map((t) => t.id);
const analysisTaskIds = complexityAnalysis.map((a) => a.taskId); const analysisTaskIds = complexityAnalysis.map((a) => a.taskId);

View File

@@ -1,22 +1,22 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { z } from 'zod';
import { import {
getTagAwareFilePath,
isSilentMode,
log, log,
readJSON, readJSON,
writeJSON, writeJSON
isSilentMode,
getTagAwareFilePath
} from '../utils.js'; } from '../utils.js';
import { import {
displayAiUsageSummary,
startLoadingIndicator, startLoadingIndicator,
stopLoadingIndicator, stopLoadingIndicator
displayAiUsageSummary
} from '../ui.js'; } from '../ui.js';
import { generateTextService } from '../ai-services-unified.js'; import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
import { generateObjectService } from '../ai-services-unified.js';
import { import {
getDefaultSubtasks, getDefaultSubtasks,
@@ -24,259 +24,12 @@ import {
hasCodebaseAnalysis hasCodebaseAnalysis
} from '../config-manager.js'; } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js'; import { getPromptManager } from '../prompt-manager.js';
import generateTaskFiles from './generate-task-files.js'; import { findProjectRoot, flattenTasksWithSubtasks } from '../utils.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
import { ContextGatherer } from '../utils/contextGatherer.js'; import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.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. * Expand a task into subtasks using the unified AI service (generateObjectService).
* @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).
* Appends new subtasks by default. Replaces existing subtasks if force=true. * Appends new subtasks by default. Replaces existing subtasks if force=true.
* Integrates complexity report to determine subtask count and prompt if available, * Integrates complexity report to determine subtask count and prompt if available,
* unless numSubtasks is explicitly provided. * unless numSubtasks is explicitly provided.
@@ -444,6 +197,10 @@ async function expandTask(
} }
// Determine prompt content AND system prompt // Determine prompt content AND system prompt
// Calculate the next subtask ID to match current behavior:
// - Start from the number of existing subtasks + 1
// - This creates sequential IDs: 1, 2, 3, 4...
// - Display format shows as parentTaskId.subtaskId (e.g., "1.1", "1.2", "2.1")
const nextSubtaskId = (task.subtasks?.length || 0) + 1; const nextSubtaskId = (task.subtasks?.length || 0) + 1;
// Load prompts using PromptManager // Load prompts using PromptManager
@@ -504,7 +261,6 @@ async function expandTask(
hasCodebaseAnalysis: hasCodebaseAnalysisCapability, hasCodebaseAnalysis: hasCodebaseAnalysisCapability,
projectRoot: projectRoot || '' projectRoot: projectRoot || ''
}; };
let variantKey = 'default'; let variantKey = 'default';
if (expansionPromptText) { if (expansionPromptText) {
variantKey = 'complexity-report'; variantKey = 'complexity-report';
@@ -534,7 +290,7 @@ async function expandTask(
); );
// --- End Complexity Report / Prompt Logic --- // --- End Complexity Report / Prompt Logic ---
// --- AI Subtask Generation using generateTextService --- // --- AI Subtask Generation using generateObjectService ---
let generatedSubtasks = []; let generatedSubtasks = [];
let loadingIndicator = null; let loadingIndicator = null;
if (outputFormat === 'text') { if (outputFormat === 'text') {
@@ -543,48 +299,36 @@ async function expandTask(
); );
} }
let responseText = '';
let aiServiceResponse = null; let aiServiceResponse = null;
try { try {
const role = useResearch ? 'research' : 'main'; const role = useResearch ? 'research' : 'main';
// Call generateTextService with the determined prompts and telemetry params // Call generateObjectService with the determined prompts and telemetry params
aiServiceResponse = await generateTextService({ aiServiceResponse = await generateObjectService({
prompt: promptContent, prompt: promptContent,
systemPrompt: systemPrompt, systemPrompt: systemPrompt,
role, role,
session, session,
projectRoot, projectRoot,
schema: COMMAND_SCHEMAS['expand-task'],
objectName: 'subtasks',
commandName: 'expand-task', commandName: 'expand-task',
outputType: outputFormat outputType: outputFormat
}); });
responseText = aiServiceResponse.mainResult;
// Parse Subtasks // With generateObject, we expect structured data verify it before use
generatedSubtasks = parseSubtasksFromText( const mainResult = aiServiceResponse?.mainResult;
responseText, if (!mainResult || !Array.isArray(mainResult.subtasks)) {
nextSubtaskId, throw new Error('AI response did not include a valid subtasks array.');
finalSubtaskCount, }
task.id, generatedSubtasks = mainResult.subtasks;
logger logger.info(`Received ${generatedSubtasks.length} subtasks from AI.`);
);
logger.info(
`Successfully parsed ${generatedSubtasks.length} subtasks from AI response.`
);
} catch (error) { } catch (error) {
if (loadingIndicator) stopLoadingIndicator(loadingIndicator); if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
logger.error( logger.error(
`Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context `Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context
'error' '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; throw error;
} finally { } finally {
if (loadingIndicator) stopLoadingIndicator(loadingIndicator); if (loadingIndicator) stopLoadingIndicator(loadingIndicator);

View File

@@ -3,7 +3,6 @@ import path from 'path';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import Table from 'cli-table3'; import Table from 'cli-table3';
import { z } from 'zod'; // Keep Zod for post-parse validation
import { import {
log as consoleLog, log as consoleLog,
@@ -22,7 +21,11 @@ import {
displayAiUsageSummary displayAiUsageSummary
} from '../ui.js'; } 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 { import {
getDebugFlag, getDebugFlag,
isApiKeySet, isApiKeySet,
@@ -32,229 +35,6 @@ import { getPromptManager } from '../prompt-manager.js';
import { ContextGatherer } from '../utils/contextGatherer.js'; import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.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. * Update a task by ID with new information using the unified AI service.
* @param {string} tasksPath - Path to the tasks.json file * @param {string} tasksPath - Path to the tasks.json file
@@ -522,6 +302,9 @@ async function updateTaskById(
try { try {
const serviceRole = useResearch ? 'research' : 'main'; const serviceRole = useResearch ? 'research' : 'main';
if (appendMode) {
// Append mode still uses generateTextService since it returns plain text
aiServiceResponse = await generateTextService({ aiServiceResponse = await generateTextService({
role: serviceRole, role: serviceRole,
session: session, session: session,
@@ -531,6 +314,20 @@ async function updateTaskById(
commandName: 'update-task', commandName: 'update-task',
outputType: isMCP ? 'mcp' : 'cli' 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) if (loadingIndicator)
stopLoadingIndicator(loadingIndicator, 'AI update complete.'); stopLoadingIndicator(loadingIndicator, 'AI update complete.');
@@ -600,13 +397,8 @@ async function updateTaskById(
}; };
} }
// Full update mode: Use mainResult (text) for parsing // Full update mode: Use structured data directly
const updatedTask = parseUpdatedTaskFromText( const updatedTask = aiServiceResponse.mainResult.task;
aiServiceResponse.mainResult,
taskId,
logFn,
isMCP
);
// --- Task Validation/Correction (Keep existing logic) --- // --- Task Validation/Correction (Keep existing logic) ---
if (!updatedTask || typeof updatedTask !== 'object') if (!updatedTask || typeof updatedTask !== 'object')

View File

@@ -2,7 +2,6 @@ import path from 'path';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import Table from 'cli-table3'; import Table from 'cli-table3';
import { z } from 'zod'; // Keep Zod for post-parsing validation
import { import {
log as consoleLog, log as consoleLog,
@@ -22,258 +21,13 @@ import {
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js'; import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js'; import { getPromptManager } from '../prompt-manager.js';
import generateTaskFiles from './generate-task-files.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 { getModelConfiguration } from './models.js';
import { ContextGatherer } from '../utils/contextGatherer.js'; import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js'; import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.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. * Update tasks based on new context using the unified AI service.
* @param {string} tasksPath - Path to the tasks.json file * @param {string} tasksPath - Path to the tasks.json file
@@ -458,13 +212,15 @@ async function updateTasks(
// Determine role based on research flag // Determine role based on research flag
const serviceRole = useResearch ? 'research' : 'main'; const serviceRole = useResearch ? 'research' : 'main';
// Call the unified AI service // Call the unified AI service with generateObject
aiServiceResponse = await generateTextService({ aiServiceResponse = await generateObjectService({
role: serviceRole, role: serviceRole,
session: session, session: session,
projectRoot: projectRoot, projectRoot: projectRoot,
systemPrompt: systemPrompt, systemPrompt: systemPrompt,
prompt: userPrompt, prompt: userPrompt,
schema: COMMAND_SCHEMAS['update-tasks'],
objectName: 'tasks',
commandName: 'update-tasks', commandName: 'update-tasks',
outputType: isMCP ? 'mcp' : 'cli' outputType: isMCP ? 'mcp' : 'cli'
}); });
@@ -472,13 +228,8 @@ async function updateTasks(
if (loadingIndicator) if (loadingIndicator)
stopLoadingIndicator(loadingIndicator, 'AI update complete.'); stopLoadingIndicator(loadingIndicator, 'AI update complete.');
// Use the mainResult (text) for parsing // With generateObject, we get structured data directly
const parsedUpdatedTasks = parseUpdatedTasksFromText( const parsedUpdatedTasks = aiServiceResponse.mainResult.tasks;
aiServiceResponse.mainResult,
tasksToUpdate.length,
logFn,
isMCP
);
// --- Update Tasks Data (Updated writeJSON call) --- // --- Update Tasks Data (Updated writeJSON call) ---
if (!Array.isArray(parsedUpdatedTasks)) { if (!Array.isArray(parsedUpdatedTasks)) {

View File

@@ -21,6 +21,13 @@ export class BaseAIProvider {
// Each provider must set their name // Each provider must set their name
this.name = this.constructor.name; this.name = this.constructor.name;
/**
* Whether this provider needs explicit schema in JSON mode
* Can be overridden by subclasses
* @type {boolean}
*/
this.needsExplicitJsonSchema = false;
} }
/** /**
@@ -272,12 +279,15 @@ export class BaseAIProvider {
); );
const client = await this.getClient(params); const client = await this.getClient(params);
const result = await generateObject({ const result = await generateObject({
model: client(params.modelId), model: client(params.modelId),
messages: params.messages, messages: params.messages,
schema: zodSchema(params.schema), schema: params.schema,
mode: params.mode || 'auto', mode: this.needsExplicitJsonSchema ? 'json' : 'auto',
...this.prepareTokenParam(params.modelId, params.maxTokens), schemaName: params.objectName,
schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
maxTokens: params.maxTokens,
temperature: params.temperature temperature: params.temperature
}); });
@@ -298,7 +308,7 @@ export class BaseAIProvider {
// Check if this is a JSON parsing error that we can potentially fix // Check if this is a JSON parsing error that we can potentially fix
if ( if (
NoObjectGeneratedError.isInstance(error) && NoObjectGeneratedError.isInstance(error) &&
JSONParseError.isInstance(error.cause) && error.cause instanceof JSONParseError &&
error.cause.text error.cause.text
) { ) {
log( log(

View File

@@ -32,6 +32,8 @@ export class ClaudeCodeProvider extends BaseAIProvider {
super(); super();
this.name = 'Claude Code'; this.name = 'Claude Code';
this.supportedModels = ['sonnet', 'opus']; this.supportedModels = ['sonnet', 'opus'];
// Claude Code requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
} }
/** /**

View File

@@ -15,6 +15,8 @@ export class GeminiCliProvider extends BaseAIProvider {
constructor() { constructor() {
super(); super();
this.name = 'Gemini CLI'; this.name = 'Gemini CLI';
// Gemini CLI requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
} }
/** /**
@@ -587,7 +589,7 @@ Generate ${subtaskCount} subtasks based on the original task context. Return ONL
system: systemPrompt, system: systemPrompt,
messages: messages, messages: messages,
schema: params.schema, schema: params.schema,
mode: 'json', // Use json mode instead of auto for Gemini mode: this.needsExplicitJsonSchema ? 'json' : 'auto',
maxOutputTokens: params.maxTokens, maxOutputTokens: params.maxTokens,
temperature: params.temperature temperature: params.temperature
}); });

View File

@@ -11,6 +11,8 @@ export class GrokCliProvider extends BaseAIProvider {
constructor() { constructor() {
super(); super();
this.name = 'Grok CLI'; this.name = 'Grok CLI';
// Grok CLI requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
} }
/** /**

View File

@@ -44,8 +44,8 @@
}, },
"prompts": { "prompts": {
"default": { "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 (non-negative integer; 0 if no expansion needed)\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." "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"
} }
} }
} }

View File

@@ -68,17 +68,18 @@
"prompts": { "prompts": {
"complexity-report": { "complexity-report": {
"condition": "expansionPrompt", "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: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\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}}."
"user": "Break down the following task:\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}\n\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. CRITICAL: Use sequential IDs starting from {{nextSubtaskId}} (first={{nextSubtaskId}}, second={{nextSubtaskId}}+1, etc.)."
}, },
"research": { "research": {
"condition": "useResearch === true && !expansionPrompt", "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.", "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: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\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 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." "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}}\n\nCRITICAL: You MUST use sequential IDs starting from {{nextSubtaskId}}. The first subtask MUST have id={{nextSubtaskId}}, the second MUST have id={{nextSubtaskId}}+1, and so on. Do NOT use parent task ID in subtask numbering!"
}, },
"default": { "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.", "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: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\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}}\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}" "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\nCRITICAL: You MUST use sequential IDs starting from {{nextSubtaskId}}. The first subtask MUST have id={{nextSubtaskId}}, the second MUST have id={{nextSubtaskId}}+1, and so on. Do NOT use parent task ID in subtask numbering!"
} }
} }
} }

View File

@@ -56,8 +56,8 @@
}, },
"prompts": { "prompts": {
"default": { "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}}", "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\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}" "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."
} }
} }
} }

View File

@@ -59,13 +59,13 @@
}, },
"prompts": { "prompts": {
"default": { "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.", "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 only the updated task as a valid JSON object{{#if useResearch}} with research-backed improvements{{/if}}." "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": { "append": {
"condition": "appendMode === true", "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...\").", "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}}"
} }
} }
} }

View File

@@ -43,8 +43,8 @@
}, },
"prompts": { "prompts": {
"default": { "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.", "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 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:" "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
View 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)'
)
});

View 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().nonnegative(),
expansionPrompt: z.string(),
reasoning: z.string()
});
export const ComplexityAnalysisResponseSchema = z.object({
complexityAnalysis: z.array(ComplexityAnalysisItemSchema)
});

View File

@@ -0,0 +1,35 @@
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)
});

View 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
View 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
View File

@@ -0,0 +1,27 @@
import { AddTaskResponseSchema } from './add-task.js';
import { ComplexityAnalysisResponseSchema } from './analyze-complexity.js';
import { ExpandTaskResponseSchema } from './expand-task.js';
import { ParsePRDResponseSchema } from './parse-prd.js';
import { UpdateSubtaskResponseSchema } from './update-subtask.js';
import { UpdateTaskResponseSchema } from './update-task.js';
import { UpdateTasksResponseSchema } from './update-tasks.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';

View File

@@ -0,0 +1,6 @@
import { z } from 'zod';
import { SubtaskSchema } from './base-schemas.js';
export const UpdateSubtaskResponseSchema = z.object({
subtask: SubtaskSchema
});

View File

@@ -0,0 +1,6 @@
import { z } from 'zod';
import { UpdatedTaskSchema } from './update-tasks.js';
export const UpdateTaskResponseSchema = z.object({
task: UpdatedTaskSchema
});

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
import { BaseTaskSchema, SubtaskSchema } from './base-schemas.js';
export const UpdatedTaskSchema = BaseTaskSchema.extend({
subtasks: z.array(SubtaskSchema).nullable().default(null)
});
export const UpdateTasksResponseSchema = z.object({
tasks: z.array(UpdatedTaskSchema)
});

View File

@@ -330,7 +330,7 @@ describe('Complex Cross-Tag Scenarios', () => {
describe('Large Task Set Performance', () => { describe('Large Task Set Performance', () => {
it('should handle large task sets efficiently', () => { it('should handle large task sets efficiently', () => {
// Create a large task set (100 tasks) // Create a large task set (50 tasks)
const largeTaskSet = { const largeTaskSet = {
master: { master: {
tasks: [], tasks: [],
@@ -348,8 +348,8 @@ describe('Complex Cross-Tag Scenarios', () => {
} }
}; };
// Add 50 tasks to master with dependencies // Add 25 tasks to master with dependencies
for (let i = 1; i <= 50; i++) { for (let i = 1; i <= 25; i++) {
largeTaskSet.master.tasks.push({ largeTaskSet.master.tasks.push({
id: i, id: i,
title: `Task ${i}`, title: `Task ${i}`,
@@ -359,8 +359,8 @@ describe('Complex Cross-Tag Scenarios', () => {
}); });
} }
// Add 50 tasks to in-progress // Add 25 tasks to in-progress (ensure no ID conflict with master)
for (let i = 51; i <= 100; i++) { for (let i = 26; i <= 50; i++) {
largeTaskSet['in-progress'].tasks.push({ largeTaskSet['in-progress'].tasks.push({
id: i, id: i,
title: `Task ${i}`, title: `Task ${i}`,
@@ -371,21 +371,32 @@ describe('Complex Cross-Tag Scenarios', () => {
} }
fs.writeFileSync(tasksPath, JSON.stringify(largeTaskSet, null, 2)); fs.writeFileSync(tasksPath, JSON.stringify(largeTaskSet, null, 2));
// Should complete within reasonable time // Execute move; correctness is validated below (no timing assertion)
const timeout = process.env.CI ? 12000 : 8000;
const startTime = Date.now();
execSync( execSync(
`node ${binPath} move --from=50 --from-tag=master --to-tag=in-progress --with-dependencies`, `node ${binPath} move --from=25 --from-tag=master --to-tag=in-progress --with-dependencies`,
{ stdio: 'pipe' } { stdio: 'pipe' }
); );
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(timeout);
// Verify the move was successful // Verify the move was successful
const tasksAfter = JSON.parse(fs.readFileSync(tasksPath, 'utf8')); const tasksAfter = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
// Verify all tasks in the dependency chain were moved
for (let i = 1; i <= 25; i++) {
expect(tasksAfter.master.tasks.find((t) => t.id === i)).toBeUndefined();
expect( expect(
tasksAfter['in-progress'].tasks.find((t) => t.id === 50) tasksAfter['in-progress'].tasks.find((t) => t.id === i)
).toBeDefined(); ).toBeDefined();
}
// Verify in-progress still has its original tasks (26-50)
for (let i = 26; i <= 50; i++) {
expect(
tasksAfter['in-progress'].tasks.find((t) => t.id === i)
).toBeDefined();
}
// Final count check
expect(tasksAfter['in-progress'].tasks).toHaveLength(50); // 25 moved + 25 original
}); });
}); });

View File

@@ -1,5 +1,6 @@
import { jest } from '@jest/globals';
import { PromptManager } from '../../../scripts/modules/prompt-manager.js'; import { PromptManager } from '../../../scripts/modules/prompt-manager.js';
import { ExpandTaskResponseSchema } from '../../../src/schemas/expand-task.js';
import { SubtaskSchema } from '../../../src/schemas/base-schemas.js';
describe('expand-task prompt template', () => { describe('expand-task prompt template', () => {
let promptManager; let promptManager;
@@ -74,30 +75,25 @@ describe('expand-task prompt template', () => {
expect(userPrompt).toContain(`Current details: ${testTask.details}`); expect(userPrompt).toContain(`Current details: ${testTask.details}`);
// Also includes the expansion prompt // Also includes the expansion prompt
expect(userPrompt).toContain('Expansion Guidance:');
expect(userPrompt).toContain(params.expansionPrompt); expect(userPrompt).toContain(params.expansionPrompt);
expect(userPrompt).toContain(params.complexityReasoningContext); expect(userPrompt).toContain(params.complexityReasoningContext);
}); });
test('all variants request JSON format with subtasks array', () => { test('ExpandTaskResponseSchema defines required subtask fields', () => {
const variants = ['default', 'research', 'complexity-report']; // Test the schema definition directly instead of weak substring matching
const schema = ExpandTaskResponseSchema;
const subtasksSchema = schema.shape.subtasks;
const subtaskSchema = subtasksSchema.element;
variants.forEach((variant) => { // Verify the schema has the required fields
const params = expect(subtaskSchema).toBe(SubtaskSchema);
variant === 'complexity-report' expect(SubtaskSchema.shape).toHaveProperty('id');
? { ...baseParams, expansionPrompt: 'test' } expect(SubtaskSchema.shape).toHaveProperty('title');
: baseParams; expect(SubtaskSchema.shape).toHaveProperty('description');
expect(SubtaskSchema.shape).toHaveProperty('dependencies');
const { systemPrompt, userPrompt } = promptManager.loadPrompt( expect(SubtaskSchema.shape).toHaveProperty('details');
'expand-task', expect(SubtaskSchema.shape).toHaveProperty('status');
params, expect(SubtaskSchema.shape).toHaveProperty('testStrategy');
variant
);
const combined = systemPrompt + userPrompt;
expect(combined.toLowerCase()).toContain('subtasks');
expect(combined).toContain('JSON');
});
}); });
test('complexity-report variant fails without task context regression test', () => { test('complexity-report variant fails without task context regression test', () => {

View 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'
];
// Map banned phrases to contexts where they're allowed
const allowedContexts = {
'respond only with': ['Use markdown formatting for better readability'],
'return only the': ['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');
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];
const isAllowed =
allowedInContext &&
allowedInContext.some((context) =>
lowerContent.includes(context.toLowerCase())
);
expect(isAllowed).toBe(
true,
`File ${file} contains banned phrase "${phrase}" without allowed context`
);
}
});
});
});
});

View File

@@ -50,7 +50,7 @@ jest.unstable_mockModule(
() => ({ () => ({
generateObjectService: jest.fn().mockResolvedValue({ generateObjectService: jest.fn().mockResolvedValue({
mainResult: { mainResult: {
tasks: [] complexityAnalysis: []
}, },
telemetryData: { telemetryData: {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@@ -307,10 +307,15 @@ describe('analyzeTaskComplexity', () => {
return { task: task || null, originalSubtaskCount: null }; 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 // Arrange
const options = { const options = {
file: 'tasks/tasks.json', file: 'tasks/tasks.json',
@@ -338,7 +343,7 @@ describe('analyzeTaskComplexity', () => {
'/mock/project/root', '/mock/project/root',
undefined undefined
); );
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object)); expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
expect(mockWriteFileSync).toHaveBeenCalledWith( expect(mockWriteFileSync).toHaveBeenCalledWith(
expect.stringContaining('task-complexity-report.json'), expect.stringContaining('task-complexity-report.json'),
expect.stringContaining('"thresholdScore": 5'), expect.stringContaining('"thresholdScore": 5'),
@@ -369,7 +374,7 @@ describe('analyzeTaskComplexity', () => {
}); });
// Assert // Assert
expect(generateTextService).toHaveBeenCalledWith( expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
role: 'research' // This should be present when research is true role: 'research' // This should be present when research is true
}) })
@@ -454,7 +459,7 @@ describe('analyzeTaskComplexity', () => {
// Assert // Assert
// Check if the prompt sent to AI doesn't include the completed task (id: 3) // Check if the prompt sent to AI doesn't include the completed task (id: 3)
expect(generateTextService).toHaveBeenCalledWith( expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
prompt: expect.not.stringContaining('"id": 3') prompt: expect.not.stringContaining('"id": 3')
}) })
@@ -471,7 +476,7 @@ describe('analyzeTaskComplexity', () => {
}; };
// Force API error // Force API error
generateTextService.mockRejectedValueOnce(new Error('API Error')); generateObjectService.mockRejectedValueOnce(new Error('API Error'));
const mockMcpLog = { const mockMcpLog = {
info: jest.fn(), info: jest.fn(),

View File

@@ -196,9 +196,62 @@ jest.unstable_mockModule(
currency: 'USD' 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: { mainResult: {
object: {
subtasks: [ subtasks: [
{ {
id: 1, id: 1,
@@ -210,7 +263,6 @@ jest.unstable_mockModule(
testStrategy: 'Test strategy' testStrategy: 'Test strategy'
} }
] ]
}
}, },
telemetryData: { telemetryData: {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@@ -223,6 +275,7 @@ jest.unstable_mockModule(
totalCost: 0.012414, totalCost: 0.012414,
currency: 'USD' currency: 'USD'
} }
});
}) })
}) })
); );
@@ -421,9 +474,8 @@ const { readJSON, writeJSON, getTagAwareFilePath } = await import(
'../../../../../scripts/modules/utils.js' '../../../../../scripts/modules/utils.js'
); );
const { generateTextService, streamTextService } = await import( const { generateTextService, generateObjectService, streamTextService } =
'../../../../../scripts/modules/ai-services-unified.js' await import('../../../../../scripts/modules/ai-services-unified.js');
);
// Import the modules under test // Import the modules under test
const { default: analyzeTaskComplexity } = await import( const { default: analyzeTaskComplexity } = await import(

View File

@@ -65,8 +65,8 @@ jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
jest.unstable_mockModule( jest.unstable_mockModule(
'../../../../../scripts/modules/ai-services-unified.js', '../../../../../scripts/modules/ai-services-unified.js',
() => ({ () => ({
generateTextService: jest.fn().mockResolvedValue({ generateObjectService: jest.fn().mockResolvedValue({
mainResult: JSON.stringify({ mainResult: {
subtasks: [ subtasks: [
{ {
id: 1, id: 1,
@@ -101,7 +101,7 @@ jest.unstable_mockModule(
testStrategy: 'UI tests and visual regression testing' testStrategy: 'UI tests and visual regression testing'
} }
] ]
}), },
telemetryData: { telemetryData: {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
userId: '1234567890', userId: '1234567890',
@@ -213,7 +213,7 @@ const {
findProjectRoot findProjectRoot
} = await import('../../../../../scripts/modules/utils.js'); } = await import('../../../../../scripts/modules/utils.js');
const { generateTextService } = await import( const { generateObjectService } = await import(
'../../../../../scripts/modules/ai-services-unified.js' '../../../../../scripts/modules/ai-services-unified.js'
); );
@@ -373,7 +373,7 @@ describe('expandTask', () => {
'/mock/project/root', '/mock/project/root',
undefined undefined
); );
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object)); expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
expect(writeJSON).toHaveBeenCalledWith( expect(writeJSON).toHaveBeenCalledWith(
tasksPath, tasksPath,
expect.objectContaining({ expect.objectContaining({
@@ -458,7 +458,7 @@ describe('expandTask', () => {
); );
// Assert // Assert
expect(generateTextService).toHaveBeenCalledWith( expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
role: 'research', role: 'research',
commandName: expect.any(String) commandName: expect.any(String)
@@ -496,7 +496,7 @@ describe('expandTask', () => {
telemetryData: expect.any(Object) telemetryData: expect.any(Object)
}) })
); );
expect(generateTextService).toHaveBeenCalled(); expect(generateObjectService).toHaveBeenCalled();
}); });
}); });
@@ -660,25 +660,38 @@ describe('expandTask', () => {
// Act // Act
await expandTask(tasksPath, taskId, 3, false, '', context, false); await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should append to existing subtasks with proper ID increments // Assert - Verify generateObjectService was called correctly
expect(writeJSON).toHaveBeenCalledWith( expect(generateObjectService).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({ expect.objectContaining({
tasks: expect.arrayContaining([ role: 'main',
expect.objectContaining({ commandName: 'expand-task',
id: 4, objectName: 'subtasks'
subtasks: expect.arrayContaining([ })
// Should contain both existing and new subtasks );
expect.any(Object),
expect.any(Object), // Assert - Verify data was written with appended subtasks
expect.any(Object), expect(writeJSON).toHaveBeenCalled();
expect.any(Object) // 1 existing + 3 new = 4 total const writeCall = writeJSON.mock.calls[0];
]) const savedData = writeCall[1]; // Second argument is the data
const task4 = savedData.tasks.find((t) => t.id === 4);
// Should have 4 subtasks total (1 existing + 3 new)
expect(task4.subtasks).toHaveLength(4);
// Verify existing subtask is preserved at index 0
expect(task4.subtasks[0]).toEqual(
expect.objectContaining({
id: 1,
title: 'Existing subtask'
})
);
// Verify new subtasks were appended (they start with id=1 from AI)
expect(task4.subtasks[1]).toEqual(
expect.objectContaining({
id: 1,
title: 'Set up project structure'
}) })
])
}),
'/mock/project/root',
undefined
); );
}); });
}); });
@@ -743,8 +756,8 @@ describe('expandTask', () => {
// Act // Act
await expandTask(tasksPath, taskId, undefined, false, '', context, false); await expandTask(tasksPath, taskId, undefined, false, '', context, false);
// Assert - generateTextService called with systemPrompt for 5 subtasks // Assert - generateObjectService called with systemPrompt for 5 subtasks
const callArg = generateTextService.mock.calls[0][0]; const callArg = generateObjectService.mock.calls[0][0];
expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks'); expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks');
// Assert - Should use complexity-report variant with expansion prompt // Assert - Should use complexity-report variant with expansion prompt
@@ -831,7 +844,9 @@ describe('expandTask', () => {
projectRoot: '/mock/project/root' projectRoot: '/mock/project/root'
}; };
generateTextService.mockRejectedValueOnce(new Error('AI service error')); generateObjectService.mockRejectedValueOnce(
new Error('AI service error')
);
// Act & Assert // Act & Assert
await expect( await expect(
@@ -841,6 +856,54 @@ describe('expandTask', () => {
expect(writeJSON).not.toHaveBeenCalled(); expect(writeJSON).not.toHaveBeenCalled();
}); });
test('should handle missing mainResult from AI response', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Mock AI service returning response without mainResult
generateObjectService.mockResolvedValueOnce({
telemetryData: { inputTokens: 100, outputTokens: 50 }
// Missing mainResult
});
// Act & Assert
await expect(
expandTask(tasksPath, taskId, 3, false, '', context, false)
).rejects.toThrow('AI response did not include a valid subtasks array.');
expect(writeJSON).not.toHaveBeenCalled();
});
test('should handle invalid subtasks array from AI response', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Mock AI service returning response with invalid subtasks
generateObjectService.mockResolvedValueOnce({
mainResult: {
subtasks: 'not-an-array' // Invalid: should be an array
},
telemetryData: { inputTokens: 100, outputTokens: 50 }
});
// Act & Assert
await expect(
expandTask(tasksPath, taskId, 3, false, '', context, false)
).rejects.toThrow('AI response did not include a valid subtasks array.');
expect(writeJSON).not.toHaveBeenCalled();
});
test('should handle file read errors', async () => { test('should handle file read errors', async () => {
// Arrange // Arrange
const tasksPath = 'tasks/tasks.json'; const tasksPath = 'tasks/tasks.json';
@@ -941,7 +1004,7 @@ describe('expandTask', () => {
await expandTask(tasksPath, taskId, 3, false, '', context, false); await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should work with empty context (but may include project context) // Assert - Should work with empty context (but may include project context)
expect(generateTextService).toHaveBeenCalledWith( expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
prompt: expect.stringMatching(/.*/) // Just ensure prompt exists prompt: expect.stringMatching(/.*/) // Just ensure prompt exists
}) })
@@ -1074,7 +1137,7 @@ describe('expandTask', () => {
// Assert - Should complete successfully // Assert - Should complete successfully
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(generateTextService).toHaveBeenCalled(); expect(generateObjectService).toHaveBeenCalled();
}); });
test('should use dynamic prompting when numSubtasks is 0', async () => { test('should use dynamic prompting when numSubtasks is 0', async () => {
@@ -1095,11 +1158,11 @@ describe('expandTask', () => {
// Act // Act
await expandTask(tasksPath, taskId, 0, false, '', context, false); await expandTask(tasksPath, taskId, 0, false, '', context, false);
// Assert - Verify generateTextService was called // Assert - Verify generateObjectService was called
expect(generateTextService).toHaveBeenCalled(); expect(generateObjectService).toHaveBeenCalled();
// Get the call arguments to verify the system prompt // 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( expect(callArgs.systemPrompt).toContain(
'an appropriate number of specific subtasks' 'an appropriate number of specific subtasks'
); );
@@ -1122,11 +1185,11 @@ describe('expandTask', () => {
// Act // Act
await expandTask(tasksPath, taskId, 5, false, '', context, false); await expandTask(tasksPath, taskId, 5, false, '', context, false);
// Assert - Verify generateTextService was called // Assert - Verify generateObjectService was called
expect(generateTextService).toHaveBeenCalled(); expect(generateObjectService).toHaveBeenCalled();
// Get the call arguments to verify the system prompt // 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'); expect(callArgs.systemPrompt).toContain('5 specific subtasks');
}); });
@@ -1151,8 +1214,8 @@ describe('expandTask', () => {
await expandTask(tasksPath, taskId, -3, false, '', context, false); await expandTask(tasksPath, taskId, -3, false, '', context, false);
// Assert - Should use default value instead of negative // Assert - Should use default value instead of negative
expect(generateTextService).toHaveBeenCalled(); expect(generateObjectService).toHaveBeenCalled();
const callArgs = generateTextService.mock.calls[0][0]; const callArgs = generateObjectService.mock.calls[0][0];
expect(callArgs.systemPrompt).toContain('4 specific subtasks'); expect(callArgs.systemPrompt).toContain('4 specific subtasks');
}); });
@@ -1177,8 +1240,8 @@ describe('expandTask', () => {
await expandTask(tasksPath, taskId, undefined, false, '', context, false); await expandTask(tasksPath, taskId, undefined, false, '', context, false);
// Assert - Should use default value // Assert - Should use default value
expect(generateTextService).toHaveBeenCalled(); expect(generateObjectService).toHaveBeenCalled();
const callArgs = generateTextService.mock.calls[0][0]; const callArgs = generateObjectService.mock.calls[0][0];
expect(callArgs.systemPrompt).toContain('6 specific subtasks'); expect(callArgs.systemPrompt).toContain('6 specific subtasks');
}); });
@@ -1203,8 +1266,8 @@ describe('expandTask', () => {
await expandTask(tasksPath, taskId, null, false, '', context, false); await expandTask(tasksPath, taskId, null, false, '', context, false);
// Assert - Should use default value // Assert - Should use default value
expect(generateTextService).toHaveBeenCalled(); expect(generateObjectService).toHaveBeenCalled();
const callArgs = generateTextService.mock.calls[0][0]; const callArgs = generateObjectService.mock.calls[0][0];
expect(callArgs.systemPrompt).toContain('7 specific subtasks'); expect(callArgs.systemPrompt).toContain('7 specific subtasks');
}); });
}); });

View File

@@ -43,7 +43,23 @@ jest.unstable_mockModule(
() => ({ () => ({
generateTextService: jest generateTextService: jest
.fn() .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: {}
})
}) })
); );
@@ -120,3 +136,206 @@ describe('updateTaskById validation', () => {
expect(log).toHaveBeenCalled(); expect(log).toHaveBeenCalled();
}); });
}); });
describe('updateTaskById success path with generateObjectService', () => {
let fs;
let generateObjectService;
beforeEach(async () => {
jest.clearAllMocks();
jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
fs = await import('fs');
const aiServices = await import(
'../../../../../scripts/modules/ai-services-unified.js'
);
generateObjectService = aiServices.generateObjectService;
});
test('successfully updates task with all fields from generateObjectService', async () => {
fs.existsSync.mockReturnValue(true);
readJSON.mockReturnValue({
tag: 'master',
tasks: [
{
id: 1,
title: 'Original Task',
description: 'Original description',
status: 'pending',
dependencies: [],
priority: 'low',
details: null,
testStrategy: null,
subtasks: []
}
]
});
const updatedTaskData = {
id: 1,
title: 'Updated Task',
description: 'Updated description',
status: 'pending',
dependencies: [2],
priority: 'high',
details: 'New implementation details',
testStrategy: 'Unit tests required',
subtasks: [
{
id: 1,
title: 'Subtask 1',
description: 'First subtask',
status: 'pending',
dependencies: []
}
]
};
generateObjectService.mockResolvedValue({
mainResult: {
task: updatedTaskData
},
telemetryData: {
model: 'claude-3-5-sonnet-20241022',
inputTokens: 100,
outputTokens: 200
}
});
const result = await updateTaskById(
'tasks/tasks.json',
1,
'Update task with new requirements',
false,
{ tag: 'master' },
'json'
);
// Verify generateObjectService was called (not generateTextService)
expect(generateObjectService).toHaveBeenCalled();
const callArgs = generateObjectService.mock.calls[0][0];
// Verify correct arguments were passed
expect(callArgs).toMatchObject({
role: 'main',
commandName: 'update-task',
objectName: 'task'
});
expect(callArgs.schema).toBeDefined();
expect(callArgs.systemPrompt).toContain(
'update a software development task'
);
expect(callArgs.prompt).toContain('Update task with new requirements');
// Verify the returned task contains all expected fields
expect(result).toEqual({
updatedTask: expect.objectContaining({
id: 1,
title: 'Updated Task',
description: 'Updated description',
status: 'pending',
dependencies: [2],
priority: 'high',
details: 'New implementation details',
testStrategy: 'Unit tests required',
subtasks: expect.arrayContaining([
expect.objectContaining({
id: 1,
title: 'Subtask 1',
description: 'First subtask',
status: 'pending'
})
])
}),
telemetryData: expect.objectContaining({
model: 'claude-3-5-sonnet-20241022',
inputTokens: 100,
outputTokens: 200
}),
tagInfo: undefined
});
});
test('handles generateObjectService with malformed mainResult', async () => {
fs.existsSync.mockReturnValue(true);
readJSON.mockReturnValue({
tag: 'master',
tasks: [
{
id: 1,
title: 'Task',
description: 'Description',
status: 'pending',
dependencies: [],
priority: 'medium',
details: null,
testStrategy: null,
subtasks: []
}
]
});
generateObjectService.mockResolvedValue({
mainResult: {
task: null // Malformed: task is null
},
telemetryData: {}
});
await expect(
updateTaskById(
'tasks/tasks.json',
1,
'Update task',
false,
{ tag: 'master' },
'json'
)
).rejects.toThrow('Received invalid task object from AI');
});
test('handles generateObjectService with missing required fields', async () => {
fs.existsSync.mockReturnValue(true);
readJSON.mockReturnValue({
tag: 'master',
tasks: [
{
id: 1,
title: 'Task',
description: 'Description',
status: 'pending',
dependencies: [],
priority: 'medium',
details: null,
testStrategy: null,
subtasks: []
}
]
});
generateObjectService.mockResolvedValue({
mainResult: {
task: {
id: 1,
// Missing title and description
status: 'pending',
dependencies: [],
priority: 'medium'
}
},
telemetryData: {}
});
await expect(
updateTaskById(
'tasks/tasks.json',
1,
'Update task',
false,
{ tag: 'master' },
'json'
)
).rejects.toThrow('Updated task missing required fields');
});
});

View File

@@ -30,6 +30,12 @@ jest.unstable_mockModule(
generateTextService: jest.fn().mockResolvedValue({ generateTextService: jest.fn().mockResolvedValue({
mainResult: '[]', // mainResult is the text string directly mainResult: '[]', // mainResult is the text string directly
telemetryData: {} 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' '../../../../../scripts/modules/utils.js'
); );
const { generateTextService } = await import( const { generateObjectService } = await import(
'../../../../../scripts/modules/ai-services-unified.js' '../../../../../scripts/modules/ai-services-unified.js'
); );
@@ -154,7 +160,9 @@ describe('updateTasks', () => {
]; ];
const mockApiResponse = { const mockApiResponse = {
mainResult: JSON.stringify(mockUpdatedTasks), // mainResult is the JSON string directly mainResult: {
tasks: mockUpdatedTasks // generateObject returns structured data
},
telemetryData: {} telemetryData: {}
}; };
@@ -164,7 +172,7 @@ describe('updateTasks', () => {
tag: 'master', tag: 'master',
_rawTaggedData: mockInitialTasks _rawTaggedData: mockInitialTasks
}); });
generateTextService.mockResolvedValue(mockApiResponse); generateObjectService.mockResolvedValue(mockApiResponse);
// Act // Act
const result = await updateTasks( const result = await updateTasks(
@@ -185,7 +193,7 @@ describe('updateTasks', () => {
); );
// 2. AI Service called with correct args // 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 // 3. Write JSON called with correctly merged tasks
expect(writeJSON).toHaveBeenCalledWith( expect(writeJSON).toHaveBeenCalledWith(
@@ -252,7 +260,7 @@ describe('updateTasks', () => {
'/mock/path', '/mock/path',
'master' 'master'
); );
expect(generateTextService).not.toHaveBeenCalled(); expect(generateObjectService).not.toHaveBeenCalled();
expect(writeJSON).not.toHaveBeenCalled(); expect(writeJSON).not.toHaveBeenCalled();
expect(log).toHaveBeenCalledWith( expect(log).toHaveBeenCalledWith(
'info', 'info',
@@ -327,8 +335,10 @@ describe('updateTasks', () => {
_rawTaggedData: mockTaggedData _rawTaggedData: mockTaggedData
}); });
generateTextService.mockResolvedValue({ generateObjectService.mockResolvedValue({
mainResult: JSON.stringify(mockUpdatedTasks), mainResult: {
tasks: mockUpdatedTasks
},
telemetryData: { commandName: 'update-tasks', totalCost: 0.05 } telemetryData: { commandName: 'update-tasks', totalCost: 0.05 }
}); });