diff --git a/packages/core/src/services/trackerService.ts b/packages/core/src/services/trackerService.ts index f50eb711c1..37028fdd18 100644 --- a/packages/core/src/services/trackerService.ts +++ b/packages/core/src/services/trackerService.ts @@ -6,7 +6,8 @@ import fs from 'node:fs/promises'; import path from 'node:path'; -import type { TrackerTask } from './trackerTypes.js'; +import { randomBytes } from 'node:crypto'; +import { TrackerTaskSchema, type TrackerTask } from './trackerTypes.js'; export class TrackerService { private readonly trackerDir: string; @@ -28,7 +29,7 @@ export class TrackerService { * Generates a 6-character hex ID. */ private generateId(): string { - return Math.random().toString(16).substring(2, 8).padEnd(6, '0'); + return randomBytes(3).toString('hex'); } /** @@ -53,8 +54,8 @@ export class TrackerService { const taskPath = path.join(this.tasksDir, `${id}.json`); try { const content = await fs.readFile(taskPath, 'utf8'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return JSON.parse(content) as TrackerTask; + const data: unknown = JSON.parse(content); + return TrackerTaskSchema.parse(data); } catch (error) { if ( error && @@ -82,8 +83,8 @@ export class TrackerService { path.join(this.tasksDir, f), 'utf8', ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return JSON.parse(content) as TrackerTask; + const data: unknown = JSON.parse(content); + return TrackerTaskSchema.parse(data); }), ); return tasks; @@ -159,10 +160,17 @@ export class TrackerService { private async validateNoCircularDependencies( task: TrackerTask, ): Promise { + const allTasks = await this.listTasks(); + const taskMap = new Map( + allTasks.map((t) => [t.id, t]), + ); + // Ensure the current (possibly unsaved) task state is used + taskMap.set(task.id, task); + const visited = new Set(); const stack = new Set(); - const check = async (currentId: string) => { + const check = (currentId: string) => { if (stack.has(currentId)) { throw new Error( `Circular dependency detected involving task ${currentId}.`, @@ -175,17 +183,16 @@ export class TrackerService { visited.add(currentId); stack.add(currentId); - const currentTask = - currentId === task.id ? task : await this.getTask(currentId); + const currentTask = taskMap.get(currentId); if (currentTask) { for (const depId of currentTask.dependencies) { - await check(depId); + check(depId); } } stack.delete(currentId); }; - await check(task.id); + check(task.id); } } diff --git a/packages/core/src/services/trackerTypes.ts b/packages/core/src/services/trackerTypes.ts index 06509fc770..2e5d4e36b9 100644 --- a/packages/core/src/services/trackerTypes.ts +++ b/packages/core/src/services/trackerTypes.ts @@ -4,18 +4,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -export type TaskType = 'epic' | 'task' | 'bug'; +import { z } from 'zod'; -export type TaskStatus = 'open' | 'in_progress' | 'blocked' | 'closed'; +export const TaskTypeSchema = z.enum(['epic', 'task', 'bug']); +export type TaskType = z.infer; -export interface TrackerTask { - id: string; - title: string; - description: string; - type: TaskType; - status: TaskStatus; - parentId?: string; - dependencies: string[]; - subagentSessionId?: string; - metadata?: Record; -} +export const TaskStatusSchema = z.enum([ + 'open', + 'in_progress', + 'blocked', + 'closed', +]); +export type TaskStatus = z.infer; + +export const TrackerTaskSchema = z.object({ + id: z.string().length(6), + title: z.string(), + description: z.string(), + type: TaskTypeSchema, + status: TaskStatusSchema, + parentId: z.string().optional(), + dependencies: z.array(z.string()), + subagentSessionId: z.string().optional(), + metadata: z.record(z.unknown()).optional(), +}); + +export type TrackerTask = z.infer;