Merge branch 'u/anj/task-tracker-phase-1' into u/anj/task-tracker-phase-2

This commit is contained in:
Anjali Sridhar
2026-02-18 15:45:43 -08:00
2 changed files with 42 additions and 24 deletions
+18 -11
View File
@@ -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<void> {
const allTasks = await this.listTasks();
const taskMap = new Map<string, TrackerTask>(
allTasks.map((t) => [t.id, t]),
);
// Ensure the current (possibly unsaved) task state is used
taskMap.set(task.id, task);
const visited = new Set<string>();
const stack = new Set<string>();
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);
}
}
+24 -13
View File
@@ -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<typeof TaskTypeSchema>;
export interface TrackerTask {
id: string;
title: string;
description: string;
type: TaskType;
status: TaskStatus;
parentId?: string;
dependencies: string[];
subagentSessionId?: string;
metadata?: Record<string, unknown>;
}
export const TaskStatusSchema = z.enum([
'open',
'in_progress',
'blocked',
'closed',
]);
export type TaskStatus = z.infer<typeof TaskStatusSchema>;
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<typeof TrackerTaskSchema>;