From 184bb5e68e88cdecb4efb0552cbbb2dbb4a1e6ba Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Sun, 3 Aug 2025 15:07:27 +0300 Subject: [PATCH] feat: improve scope up and down command & parse-prd improvements --- .changeset/fuzzy-words-count.md | 8 +++ .changeset/tender-trams-refuse.md | 8 +++ .changeset/vast-sites-leave.md | 11 ++++ .github/scripts/tag-extension.mjs | 12 +++-- .../src/core/direct-functions/scope-down.js | 10 ++-- .../src/core/direct-functions/scope-up.js | 10 ++-- package-lock.json | 16 ++++-- package.json | 1 + scripts/modules/commands.js | 6 ++- scripts/modules/task-manager/parse-prd.js | 21 +++++--- .../modules/task-manager/scope-adjustment.js | 39 +++++++++----- src/ai-providers/base-provider.js | 51 +++++++++++++++++-- src/ai-providers/perplexity.js | 17 +++++++ 13 files changed, 167 insertions(+), 43 deletions(-) create mode 100644 .changeset/fuzzy-words-count.md create mode 100644 .changeset/tender-trams-refuse.md create mode 100644 .changeset/vast-sites-leave.md diff --git a/.changeset/fuzzy-words-count.md b/.changeset/fuzzy-words-count.md new file mode 100644 index 00000000..fd054e48 --- /dev/null +++ b/.changeset/fuzzy-words-count.md @@ -0,0 +1,8 @@ +--- +"task-master-ai": patch +--- + +Fix scope-up/down prompts to include all required fields for better AI model compatibility + +- Added missing `priority` field to scope adjustment prompts to prevent validation errors with Claude-code and other models +- Ensures generated JSON includes all fields required by the schema diff --git a/.changeset/tender-trams-refuse.md b/.changeset/tender-trams-refuse.md new file mode 100644 index 00000000..7d2804ea --- /dev/null +++ b/.changeset/tender-trams-refuse.md @@ -0,0 +1,8 @@ +--- +"task-master-ai": patch +--- + +Fix MCP scope-up/down tools not finding tasks + +- Fixed task ID parsing in MCP layer - now correctly converts string IDs to numbers +- scope_up_task and scope_down_task MCP tools now work properly diff --git a/.changeset/vast-sites-leave.md b/.changeset/vast-sites-leave.md new file mode 100644 index 00000000..2d330e55 --- /dev/null +++ b/.changeset/vast-sites-leave.md @@ -0,0 +1,11 @@ +--- +"task-master-ai": patch +--- + +Improve AI provider compatibility for JSON generation + +- Fixed schema compatibility issues between Perplexity and OpenAI o3 models +- Removed nullable/default modifiers from Zod schemas for broader compatibility +- Added automatic JSON repair for malformed AI responses (handles cases like missing array values) +- Perplexity now uses JSON mode for more reliable structured output +- Post-processing handles default values separately from schema validation diff --git a/.github/scripts/tag-extension.mjs b/.github/scripts/tag-extension.mjs index ee723ce3..7282d756 100644 --- a/.github/scripts/tag-extension.mjs +++ b/.github/scripts/tag-extension.mjs @@ -86,19 +86,23 @@ if (gitResult.status !== 0) { console.error('Error:', gitResult.error); console.error('Stderr:', gitResult.stderr); console.error('Command:', `git ls-remote ${repoUrl} ${tag}`); - + // For CI environments, try using origin instead of the full URL if (process.env.CI) { console.log('Retrying with origin remote...'); gitResult = spawnSync('git', ['ls-remote', 'origin', tag], { encoding: 'utf8' }); - + if (gitResult.status !== 0) { - throw new Error(`Failed to check remote for tag ${tag}. Exit code: ${gitResult.status}`); + throw new Error( + `Failed to check remote for tag ${tag}. Exit code: ${gitResult.status}` + ); } } else { - throw new Error(`Failed to check remote for tag ${tag}. Exit code: ${gitResult.status}`); + throw new Error( + `Failed to check remote for tag ${tag}. Exit code: ${gitResult.status}` + ); } } diff --git a/mcp-server/src/core/direct-functions/scope-down.js b/mcp-server/src/core/direct-functions/scope-down.js index e966797d..3bc6df8f 100644 --- a/mcp-server/src/core/direct-functions/scope-down.js +++ b/mcp-server/src/core/direct-functions/scope-down.js @@ -71,8 +71,8 @@ export async function scopeDownDirect(args, log, context = {}) { }; } - // Parse task IDs - const taskIds = id.split(',').map((taskId) => taskId.trim()); + // Parse task IDs - convert to numbers as expected by scopeDownTask + const taskIds = id.split(',').map((taskId) => parseInt(taskId.trim(), 10)); log.info( `Scoping down tasks: ${taskIds.join(', ')}, strength: ${strength}, research: ${research}` @@ -90,10 +90,10 @@ export async function scopeDownDirect(args, log, context = {}) { projectRoot, commandName: 'scope-down', outputType: 'mcp', - tag + tag, + research }, - 'json', // outputFormat - research + 'json' // outputFormat ); // Restore normal logging diff --git a/mcp-server/src/core/direct-functions/scope-up.js b/mcp-server/src/core/direct-functions/scope-up.js index da882b4d..ecfdec14 100644 --- a/mcp-server/src/core/direct-functions/scope-up.js +++ b/mcp-server/src/core/direct-functions/scope-up.js @@ -71,8 +71,8 @@ export async function scopeUpDirect(args, log, context = {}) { }; } - // Parse task IDs - const taskIds = id.split(',').map((taskId) => taskId.trim()); + // Parse task IDs - convert to numbers as expected by scopeUpTask + const taskIds = id.split(',').map((taskId) => parseInt(taskId.trim(), 10)); log.info( `Scoping up tasks: ${taskIds.join(', ')}, strength: ${strength}, research: ${research}` @@ -90,10 +90,10 @@ export async function scopeUpDirect(args, log, context = {}) { projectRoot, commandName: 'scope-up', outputType: 'mcp', - tag + tag, + research }, - 'json', // outputFormat - research + 'json' // outputFormat ); // Restore normal logging diff --git a/package-lock.json b/package-lock.json index 10316745..60ade081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "task-master-ai", - "version": "0.22.1-rc.0", + "version": "0.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "task-master-ai", - "version": "0.22.1-rc.0", + "version": "0.23.0", "license": "MIT WITH Commons-Clause", "workspaces": [ "apps/*", @@ -46,6 +46,7 @@ "helmet": "^8.1.0", "inquirer": "^12.5.0", "jsonc-parser": "^3.3.1", + "jsonrepair": "^3.13.0", "jsonwebtoken": "^9.0.2", "lru-cache": "^10.2.0", "ollama-ai-provider": "^1.2.0", @@ -84,7 +85,7 @@ } }, "apps/extension": { - "version": "0.22.3", + "version": "0.23.0", "devDependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -14942,6 +14943,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonrepair": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.0.tgz", + "integrity": "sha512-5YRzlAQ7tuzV1nAJu3LvDlrKtBFIALHN2+a+I1MGJCt3ldRDBF/bZuvIPzae8Epot6KBXd0awRZZcuoeAsZ/mw==", + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", diff --git a/package.json b/package.json index 57c9877c..67f02fcb 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "helmet": "^8.1.0", "inquirer": "^12.5.0", "jsonc-parser": "^3.3.1", + "jsonrepair": "^3.13.0", "jsonwebtoken": "^9.0.2", "lru-cache": "^10.2.0", "ollama-ai-provider": "^1.2.0", diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index b9cee6d7..18133f95 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -1479,7 +1479,8 @@ function registerCommands(programInstance) { projectRoot: taskMaster.getProjectRoot(), tag, commandName: 'scope-up', - outputType: 'cli' + outputType: 'cli', + research: options.research || false }; const result = await scopeUpTask( @@ -1605,7 +1606,8 @@ function registerCommands(programInstance) { projectRoot: taskMaster.getProjectRoot(), tag, commandName: 'scope-down', - outputType: 'cli' + outputType: 'cli', + research: options.research || false }; const result = await scopeDownTask( diff --git a/scripts/modules/task-manager/parse-prd.js b/scripts/modules/task-manager/parse-prd.js index 33d8bdcb..3db7c84c 100644 --- a/scripts/modules/task-manager/parse-prd.js +++ b/scripts/modules/task-manager/parse-prd.js @@ -23,14 +23,14 @@ import { displayAiUsageSummary } from '../ui.js'; // Define the Zod schema for a SINGLE task object const prdSingleTaskSchema = z.object({ - id: z.number().int().positive(), + id: z.number(), 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() + details: z.string(), + testStrategy: z.string(), + priority: z.enum(['high', 'medium', 'low']), + dependencies: z.array(z.number()), + status: z.string() }); // Define the Zod schema for the ENTIRE expected AI response object @@ -257,10 +257,15 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) { return { ...task, id: newId, - status: 'pending', + status: task.status || 'pending', priority: task.priority || 'medium', dependencies: Array.isArray(task.dependencies) ? task.dependencies : [], - subtasks: [] + subtasks: [], + // Ensure all required fields have values (even if empty strings) + title: task.title || '', + description: task.description || '', + details: task.details || '', + testStrategy: task.testStrategy || '' }; }); diff --git a/scripts/modules/task-manager/scope-adjustment.js b/scripts/modules/task-manager/scope-adjustment.js index 23792fb3..cb1558ba 100644 --- a/scripts/modules/task-manager/scope-adjustment.js +++ b/scripts/modules/task-manager/scope-adjustment.js @@ -337,7 +337,7 @@ ${ } Return a JSON object with a "subtasks" array. Each subtask should have: -- id: Sequential number starting from 1 +- id: Sequential NUMBER starting from 1 (e.g., 1, 2, 3 - NOT "1", "2", "3") - title: Clear, specific title - description: Detailed description - dependencies: Array of dependency IDs as STRINGS (use format ["${task.id}.1", "${task.id}.2"] for siblings, or empty array [] for no dependencies) @@ -345,7 +345,9 @@ Return a JSON object with a "subtasks" array. Each subtask should have: - status: "pending" - testStrategy: Testing approach -IMPORTANT: Dependencies must be strings, not numbers! +IMPORTANT: +- The 'id' field must be a NUMBER, not a string! +- Dependencies must be strings, not numbers! Ensure the JSON is valid and properly formatted.`; @@ -358,14 +360,14 @@ Ensure the JSON is valid and properly formatted.`; description: z.string().min(10), dependencies: z.array(z.string()), details: z.string().min(20), - status: z.string().default('pending'), - testStrategy: z.string().nullable().default('') + status: z.string(), + testStrategy: z.string() }) ) }); const aiResult = await generateObjectService({ - role: 'main', + role: context.research ? 'research' : 'main', session: context.session, systemPrompt, prompt, @@ -377,14 +379,21 @@ Ensure the JSON is valid and properly formatted.`; const generatedSubtasks = aiResult.mainResult.subtasks || []; + // Post-process generated subtasks to ensure defaults + const processedGeneratedSubtasks = generatedSubtasks.map((subtask) => ({ + ...subtask, + status: subtask.status || 'pending', + testStrategy: subtask.testStrategy || '' + })); + // Update task with preserved subtasks + newly generated ones - task.subtasks = [...preservedSubtasks, ...generatedSubtasks]; + task.subtasks = [...preservedSubtasks, ...processedGeneratedSubtasks]; return { updatedTask: task, regenerated: true, preserved: preservedSubtasks.length, - generated: generatedSubtasks.length + generated: processedGeneratedSubtasks.length }; } catch (error) { log( @@ -457,6 +466,7 @@ ADJUSTMENT REQUIREMENTS: - description: Updated task description - details: Updated implementation details - testStrategy: Updated test strategy +- priority: Task priority ('low', 'medium', or 'high') Ensure the JSON is valid and properly formatted.`; @@ -501,14 +511,11 @@ async function adjustTaskComplexity( .string() .min(1) .describe('Updated testing approach for the adjusted scope'), - priority: z - .enum(['low', 'medium', 'high']) - .optional() - .describe('Task priority level') + priority: z.enum(['low', 'medium', 'high']).describe('Task priority level') }); const aiResult = await generateObjectService({ - role: 'main', + role: context.research ? 'research' : 'main', session: context.session, systemPrompt, prompt, @@ -520,10 +527,16 @@ async function adjustTaskComplexity( const updatedTaskData = aiResult.mainResult; + // Ensure priority has a value (in case AI didn't provide one) + const processedTaskData = { + ...updatedTaskData, + priority: updatedTaskData.priority || task.priority || 'medium' + }; + return { updatedTask: { ...task, - ...updatedTaskData + ...processedTaskData }, telemetryData: aiResult.telemetryData }; diff --git a/src/ai-providers/base-provider.js b/src/ai-providers/base-provider.js index bb2be1d1..90e18a1e 100644 --- a/src/ai-providers/base-provider.js +++ b/src/ai-providers/base-provider.js @@ -1,4 +1,12 @@ -import { generateObject, generateText, streamText } from 'ai'; +import { + generateObject, + generateText, + streamText, + zodSchema, + JSONParseError, + NoObjectGeneratedError +} from 'ai'; +import { jsonrepair } from 'jsonrepair'; import { log } from '../../scripts/modules/utils.js'; /** @@ -206,8 +214,8 @@ export class BaseAIProvider { const result = await generateObject({ model: client(params.modelId), messages: params.messages, - schema: params.schema, - mode: 'auto', + schema: zodSchema(params.schema), + mode: params.mode || 'auto', maxTokens: params.maxTokens, temperature: params.temperature }); @@ -226,6 +234,43 @@ export class BaseAIProvider { } }; } catch (error) { + // Check if this is a JSON parsing error that we can potentially fix + if ( + NoObjectGeneratedError.isInstance(error) && + JSONParseError.isInstance(error.cause) && + error.cause.text + ) { + log( + 'warn', + `${this.name} generated malformed JSON, attempting to repair...` + ); + + try { + // Use jsonrepair to fix the malformed JSON + const repairedJson = jsonrepair(error.cause.text); + const parsed = JSON.parse(repairedJson); + + log('info', `Successfully repaired ${this.name} JSON output`); + + // Return in the expected format + return { + object: parsed, + usage: { + // Extract usage information from the error if available + inputTokens: error.usage?.promptTokens || 0, + outputTokens: error.usage?.completionTokens || 0, + totalTokens: error.usage?.totalTokens || 0 + } + }; + } catch (repairError) { + log( + 'error', + `Failed to repair ${this.name} JSON: ${repairError.message}` + ); + // Fall through to handleError with original error + } + } + this.handleError('object generation', error); } } diff --git a/src/ai-providers/perplexity.js b/src/ai-providers/perplexity.js index 06345081..529986d5 100644 --- a/src/ai-providers/perplexity.js +++ b/src/ai-providers/perplexity.js @@ -44,4 +44,21 @@ export class PerplexityAIProvider extends BaseAIProvider { this.handleError('client initialization', error); } } + + /** + * Override generateObject to use JSON mode for Perplexity + * + * NOTE: Perplexity models (especially sonar models) have known issues + * generating valid JSON, particularly with array fields. They often + * generate malformed JSON like "dependencies": , instead of "dependencies": [] + * + * The base provider now handles JSON repair automatically for all providers. + */ + async generateObject(params) { + // Force JSON mode for Perplexity as it may help with reliability + return super.generateObject({ + ...params, + mode: 'json' + }); + } }