diff --git a/src/db/repositories/drizzle/task.ts b/src/db/repositories/drizzle/task.ts index 1745859..f3f5e4c 100644 --- a/src/db/repositories/drizzle/task.ts +++ b/src/db/repositories/drizzle/task.ts @@ -7,7 +7,7 @@ import { eq, asc } from 'drizzle-orm'; import { nanoid } from 'nanoid'; import type { DrizzleDatabase } from '../../index.js'; -import { tasks, type Task } from '../../schema.js'; +import { tasks, taskDependencies, type Task } from '../../schema.js'; import type { TaskRepository, CreateTaskData, @@ -85,4 +85,16 @@ export class DrizzleTaskRepository implements TaskRepository { await this.db.delete(tasks).where(eq(tasks.id, id)); } + + async createDependency(taskId: string, dependsOnTaskId: string): Promise { + const id = nanoid(); + const now = new Date(); + + await this.db.insert(taskDependencies).values({ + id, + taskId, + dependsOnTaskId, + createdAt: now, + }); + } } diff --git a/src/db/repositories/task-repository.ts b/src/db/repositories/task-repository.ts index 1ae8e72..f1898f8 100644 --- a/src/db/repositories/task-repository.ts +++ b/src/db/repositories/task-repository.ts @@ -58,4 +58,11 @@ export interface TaskRepository { * Throws if task not found. */ delete(id: string): Promise; + + /** + * Create a dependency between two tasks. + * The task identified by taskId will depend on dependsOnTaskId. + * Both tasks must exist. + */ + createDependency(taskId: string, dependsOnTaskId: string): Promise; } diff --git a/src/trpc/router.ts b/src/trpc/router.ts index d11dda7..f54b2ef 100644 --- a/src/trpc/router.ts +++ b/src/trpc/router.ts @@ -16,7 +16,7 @@ import type { PhaseRepository } from '../db/repositories/phase-repository.js'; import type { PlanRepository } from '../db/repositories/plan-repository.js'; import type { DispatchManager } from '../dispatch/types.js'; import type { CoordinationManager } from '../coordination/types.js'; -import type { Phase, Plan } from '../db/schema.js'; +import type { Phase, Plan, Task } from '../db/schema.js'; import { buildDiscussPrompt, buildBreakdownPrompt } from '../agent/prompts.js'; /** @@ -928,6 +928,67 @@ export const appRouter = router({ return repo.update(id, data); }), + /** + * Create tasks from decomposition agent output. + * Creates all tasks in order, then creates dependencies from number mappings. + */ + createTasksFromDecomposition: publicProcedure + .input(z.object({ + planId: z.string().min(1), + tasks: z.array(z.object({ + number: z.number().int().positive(), + name: z.string().min(1), + description: z.string(), + type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).default('auto'), + dependencies: z.array(z.number().int().positive()).optional(), + })), + })) + .mutation(async ({ ctx, input }) => { + const taskRepo = requireTaskRepository(ctx); + const planRepo = requirePlanRepository(ctx); + + // Verify plan exists + const plan = await planRepo.findById(input.planId); + if (!plan) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Plan '${input.planId}' not found`, + }); + } + + // Create tasks in order, building number-to-ID map + const numberToId = new Map(); + const created: Task[] = []; + + for (const taskInput of input.tasks) { + const task = await taskRepo.create({ + planId: input.planId, + name: taskInput.name, + description: taskInput.description, + type: taskInput.type, + order: taskInput.number, + status: 'pending', + }); + numberToId.set(taskInput.number, task.id); + created.push(task); + } + + // Create dependencies after all tasks exist + for (const taskInput of input.tasks) { + if (taskInput.dependencies && taskInput.dependencies.length > 0) { + const taskId = numberToId.get(taskInput.number)!; + for (const depNumber of taskInput.dependencies) { + const dependsOnTaskId = numberToId.get(depNumber); + if (dependsOnTaskId) { + await taskRepo.createDependency(taskId, dependsOnTaskId); + } + } + } + } + + return created; + }), + // =========================================================================== // Architect Spawn Procedures // ===========================================================================