fix: remove .default() from Zod schemas for OpenAI strict JSON schema validation

Fixes #1552

OpenAI's structured outputs API requires all properties to be in the
'required' array of JSON Schema. Zod's .default() makes fields optional,
causing codex-cli provider to fail with 'Missing dependencies' error.

Changes:
- Removed .default() from SubtaskSchema, BaseTaskSchema, UpdatedTaskSchema
- Added application-level default handling in expand-task, update-task-by-id,
  update-tasks, and parse-prd-streaming
- Ensures all schema properties are marked as required for OpenAI compatibility
- Maintains backward compatibility by applying defaults when AI doesn't provide values

Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
claude[bot]
2025-12-29 08:50:15 +00:00
committed by Ralph Khreish
parent ba923a1e19
commit bba4be390b
7 changed files with 58 additions and 13 deletions

View File

@@ -0,0 +1,7 @@
---
"task-master-ai": patch
---
fix: Remove .default() from Zod schemas to satisfy OpenAI strict JSON schema validation
This fixes an issue where codex-cli provider (using OpenAI API) would fail with "Missing 'dependencies'" error during task expansion. OpenAI's structured outputs require all properties to be in the 'required' array, but Zod's .default() makes fields optional. The fix removes .default() from schemas and applies defaults at the application level instead.

View File

@@ -331,7 +331,12 @@ async function expandTask(
if (!mainResult || !Array.isArray(mainResult.subtasks)) {
throw new Error('AI response did not include a valid subtasks array.');
}
generatedSubtasks = mainResult.subtasks;
generatedSubtasks = mainResult.subtasks.map((subtask) => ({
...subtask,
dependencies: subtask.dependencies ?? [],
status: subtask.status ?? 'pending',
testStrategy: subtask.testStrategy ?? null
}));
logger.info(`Received ${generatedSubtasks.length} subtasks from AI.`);
} catch (error) {
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);

View File

@@ -570,6 +570,17 @@ async function processWithGenerateObject(context, logger) {
// Extract tasks from the result (handle both direct tasks and mainResult.tasks)
const tasks = result?.mainResult || result;
// Apply defaults to ensure all required fields are present
if (tasks && Array.isArray(tasks.tasks)) {
tasks.tasks = tasks.tasks.map((task) => ({
...task,
dependencies: task.dependencies ?? [],
priority: task.priority ?? null,
details: task.details ?? null,
testStrategy: task.testStrategy ?? null
}));
}
// Process the generated tasks
if (tasks && Array.isArray(tasks.tasks)) {
// Update progress tracker with final tasks

View File

@@ -422,7 +422,13 @@ async function updateTaskById(
}
// Full update mode: Use structured data directly
const updatedTask = aiServiceResponse.mainResult.task;
const updatedTask = {
...aiServiceResponse.mainResult.task,
dependencies: aiServiceResponse.mainResult.task.dependencies ?? [],
priority: aiServiceResponse.mainResult.task.priority ?? null,
details: aiServiceResponse.mainResult.task.details ?? null,
testStrategy: aiServiceResponse.mainResult.task.testStrategy ?? null
};
// --- Task Validation/Correction (Keep existing logic) ---
if (!updatedTask || typeof updatedTask !== 'object')
@@ -465,7 +471,8 @@ async function updateTaskById(
depId < currentSubtaskId
)
: [],
status: subtask.status || 'pending'
status: subtask.status || 'pending',
testStrategy: subtask.testStrategy ?? null
};
currentSubtaskId++;
return correctedSubtask;

View File

@@ -228,7 +228,23 @@ async function updateTasks(
stopLoadingIndicator(loadingIndicator, 'AI update complete.');
// With generateObject, we get structured data directly
const parsedUpdatedTasks = aiServiceResponse.mainResult.tasks;
const parsedUpdatedTasks = aiServiceResponse.mainResult.tasks.map(
(task) => ({
...task,
dependencies: task.dependencies ?? [],
priority: task.priority ?? null,
details: task.details ?? null,
testStrategy: task.testStrategy ?? null,
subtasks: task.subtasks
? task.subtasks.map((subtask) => ({
...subtask,
dependencies: subtask.dependencies ?? [],
status: subtask.status ?? 'pending',
testStrategy: subtask.testStrategy ?? null
}))
: null
})
);
// --- Update Tasks Data (Updated writeJSON call) ---
if (!Array.isArray(parsedUpdatedTasks)) {

View File

@@ -26,13 +26,12 @@ export const BaseTaskSchema = z
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([]),
dependencies: z.array(z.union([z.number().int(), z.string()])),
priority: z
.enum(['low', 'medium', 'high', 'critical'])
.nullable()
.default(null),
details: z.string().nullable().default(null),
testStrategy: z.string().nullable().default(null)
.nullable(),
details: z.string().nullable(),
testStrategy: z.string().nullable()
})
.strict();
@@ -41,9 +40,9 @@ export const SubtaskSchema = z
id: z.number().int().positive(),
title: z.string().min(5).max(200),
description: z.string().min(10),
dependencies: z.array(z.number().int()).default([]),
dependencies: z.array(z.number().int()),
details: z.string().min(20),
status: z.enum(['pending', 'done', 'completed']).default('pending'),
testStrategy: z.string().nullable().default(null)
status: z.enum(['pending', 'done', 'completed']),
testStrategy: z.string().nullable()
})
.strict();

View File

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