diff --git a/scripts/modules/task-manager/parse-prd/parse-prd-helpers.js b/scripts/modules/task-manager/parse-prd/parse-prd-helpers.js index 565b229c..0668794f 100644 --- a/scripts/modules/task-manager/parse-prd/parse-prd-helpers.js +++ b/scripts/modules/task-manager/parse-prd/parse-prd-helpers.js @@ -120,19 +120,21 @@ export function validateFileOperations({ } /** - * Process and transform tasks with ID remapping - * @param {Array} rawTasks - Raw tasks from AI - * @param {number} startId - Starting ID for new tasks - * @param {Array} existingTasks - Existing tasks for dependency validation - * @param {string} defaultPriority - Default priority for tasks - * @returns {Array} Processed tasks with remapped IDs - */ + * Transform raw PRD tasks into normalized tasks with reassigned sequential IDs. + * @param {Array} rawTasks - Tasks parsed from a PRD; each task must include an integer `id`. + * @param {number} startId - ID to assign to the first processed task. + * @param {Array} existingTasks - Existing tasks used to validate dependency references. + * @param {string} defaultPriority - Priority to apply when a task has no priority. + * @returns {Array} An array of tasks with reassigned sequential IDs, normalized fields (status, priority, title, description, details, testStrategy, subtasks), and dependencies remapped to the new IDs. export function processTasks( rawTasks, startId, existingTasks, defaultPriority ) { + // Runtime guard: ensure PRD task IDs are unique and sequential (1..N). + validateSequentialTaskIds(rawTasks, startId); + let currentId = startId; const taskMap = new Map(); @@ -172,6 +174,54 @@ export function processTasks( return processedTasks; } +/** + * Validate that an array of PRD tasks uses unique, positive integer IDs that form a contiguous sequence. + * + * Validates that each task has an integer `id` >= 1, that IDs are unique, and that the sorted IDs form + * a contiguous sequence starting at either `1` or the provided `expectedStartId`. + * + * @param {Array<{id: number}>} rawTasks - Array of task objects containing an `id` property. If not an array or empty, no validation is performed. + * @param {number} [expectedStartId=1] - Allowed alternative starting ID for the contiguous sequence. + * @throws {Error} If any `id` is not an integer >= 1: "PRD tasks must use sequential positive integer IDs starting at 1." + * @throws {Error} If IDs are not unique: "PRD task IDs must be unique and sequential starting at 1." + * @throws {Error} If the sequence does not start at `1` or `expectedStartId`: "PRD task IDs must start at 1 or {expectedStartId} and be sequential." + * @throws {Error} If IDs are not contiguous: "PRD task IDs must be a contiguous sequence starting at {startId}." + */ +function validateSequentialTaskIds(rawTasks, expectedStartId = 1) { + if (!Array.isArray(rawTasks) || rawTasks.length === 0) { + return; + } + + const ids = rawTasks.map((task) => task.id); + + if (ids.some((id) => !Number.isInteger(id) || id < 1)) { + throw new Error( + 'PRD tasks must use sequential positive integer IDs starting at 1.' + ); + } + + const uniqueIds = new Set(ids); + if (uniqueIds.size !== ids.length) { + throw new Error('PRD task IDs must be unique and sequential starting at 1.'); + } + + const sortedIds = [...uniqueIds].sort((a, b) => a - b); + const startId = sortedIds[0]; + if (startId !== 1 && startId !== expectedStartId) { + throw new Error( + `PRD task IDs must start at 1 or ${expectedStartId} and be sequential.` + ); + } + + for (let index = 0; index < sortedIds.length; index += 1) { + if (sortedIds[index] !== startId + index) { + throw new Error( + `PRD task IDs must be a contiguous sequence starting at ${startId}.` + ); + } + } +} + /** * Save tasks to file with tag support * @param {string} tasksPath - Path to save tasks @@ -380,4 +430,4 @@ export function displayNonStreamingCliOutput({ if (aiServiceResponse?.telemetryData) { displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); } -} +} \ No newline at end of file diff --git a/scripts/modules/task-manager/scope-adjustment.js b/scripts/modules/task-manager/scope-adjustment.js index 66c44c21..0951f1f0 100644 --- a/scripts/modules/task-manager/scope-adjustment.js +++ b/scripts/modules/task-manager/scope-adjustment.js @@ -157,14 +157,22 @@ function getCurrentComplexityScore(taskId, context) { } /** - * Regenerates subtasks for a task based on new complexity while preserving completed work - * @param {Object} task - The updated task object - * @param {string} tasksPath - Path to tasks.json - * @param {Object} context - Context containing projectRoot, tag, session - * @param {string} direction - Direction of scope change (up/down) for logging - * @param {string} strength - Strength level ('light', 'regular', 'heavy') - * @param {number|null} originalComplexity - Original complexity score for smarter adjustments - * @returns {Promise} Object with updated task and regeneration info + * Regenerates a task's pending subtasks to match a changed complexity while preserving subtasks with work already done. + * + * Generates new pending subtasks (via the configured AI service) to reach a target subtask count based on direction and strength, preserves subtasks whose status indicates work should be kept, and remaps IDs/dependencies so generated subtasks follow preserved ones. + * + * @param {Object} task - The task object to update; its subtasks array will be replaced with preserved and newly generated subtasks in the returned `updatedTask`. + * @param {string} tasksPath - Filesystem path to tasks.json (used for context; not modified by this function). + * @param {Object} context - Execution context containing { projectRoot, tag, session, ... } used for AI calls and logging. + * @param {string} direction - Scope change direction: 'up' to increase complexity or 'down' to decrease complexity. + * @param {string} [strength='regular'] - Adjustment strength: 'light', 'regular', or 'heavy'. + * @param {number|null} [originalComplexity=null] - Original complexity score (1-10) used to bias how aggressive the adjustment should be. + * @returns {Promise} An object with: + * - updatedTask: the task object with preserved and regenerated subtasks, + * - regenerated: `true` if new subtasks were generated, `false` otherwise, + * - preserved: number of subtasks kept, + * - generated: number of subtasks newly generated, + * - error?: string when regeneration failed (present only on failure). */ async function regenerateSubtasksForComplexity( task, @@ -378,6 +386,7 @@ Ensure the JSON is valid and properly formatted.`; }); const generatedSubtasks = aiResult.mainResult.subtasks || []; + ensureSequentialSubtaskIds(generatedSubtasks); // Post-process generated subtasks to ensure defaults const processedGeneratedSubtasks = generatedSubtasks.map((subtask) => ({ @@ -441,6 +450,38 @@ Ensure the JSON is valid and properly formatted.`; } } +/** + * Validate that subtasks have unique, positive integer IDs forming a sequential series starting at 1. + * + * @param {Array} subtasks - Array of subtask objects; each object is expected to have an `id` property. + * @throws {Error} If any `id` is not a positive integer ("Generated subtask ids must be positive integers"). + * @throws {Error} If `id` values are not unique ("Generated subtasks must have unique ids"). + * @throws {Error} If `id` values are not a contiguous sequence starting at 1 ("Generated subtask ids must be sequential starting from 1"). + */ +function ensureSequentialSubtaskIds(subtasks) { + if (!Array.isArray(subtasks) || subtasks.length === 0) { + return; + } + + const ids = subtasks.map((subtask) => subtask.id); + if (ids.some((id) => !Number.isInteger(id) || id < 1)) { + throw new Error('Generated subtask ids must be positive integers'); + } + const uniqueIds = new Set(ids); + if (uniqueIds.size !== ids.length) { + throw new Error('Generated subtasks must have unique ids'); + } + + const sortedIds = [...uniqueIds].sort((a, b) => a - b); + for (let index = 0; index < sortedIds.length; index += 1) { + if (sortedIds[index] !== index + 1) { + throw new Error( + 'Generated subtask ids must be sequential starting from 1' + ); + } + } +} + /** * Generates AI prompt for scope adjustment * @param {Object} task - The task to adjust @@ -872,4 +913,4 @@ export async function scopeDownTask( updatedTasks, telemetryData: combinedTelemetryData }; -} +} \ No newline at end of file