/** * Task Router — CRUD, approval, listing by parent/initiative/phase */ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import type { ProcedureBuilder } from '../trpc.js'; import { requireTaskRepository, requireInitiativeRepository, requirePhaseRepository, requireDispatchManager, } from './_helpers.js'; export function taskProcedures(publicProcedure: ProcedureBuilder) { return { listTasks: publicProcedure .input(z.object({ parentTaskId: z.string().min(1) })) .query(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); return taskRepository.findByParentTaskId(input.parentTaskId); }), getTask: publicProcedure .input(z.object({ id: z.string().min(1) })) .query(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); const task = await taskRepository.findById(input.id); if (!task) { throw new TRPCError({ code: 'NOT_FOUND', message: `Task '${input.id}' not found`, }); } return task; }), updateTaskStatus: publicProcedure .input(z.object({ id: z.string().min(1), status: z.enum(['pending_approval', 'pending', 'in_progress', 'completed', 'blocked']), })) .mutation(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); const existing = await taskRepository.findById(input.id); if (!existing) { throw new TRPCError({ code: 'NOT_FOUND', message: `Task '${input.id}' not found`, }); } return taskRepository.update(input.id, { status: input.status }); }), createInitiativeTask: publicProcedure .input(z.object({ initiativeId: z.string().min(1), name: z.string().min(1), description: z.string().optional(), category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).optional(), requiresApproval: z.boolean().nullable().optional(), })) .mutation(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); const initiativeRepo = requireInitiativeRepository(ctx); const initiative = await initiativeRepo.findById(input.initiativeId); if (!initiative) { throw new TRPCError({ code: 'NOT_FOUND', message: `Initiative '${input.initiativeId}' not found`, }); } return taskRepository.create({ initiativeId: input.initiativeId, name: input.name, description: input.description ?? null, category: input.category ?? 'execute', type: input.type ?? 'auto', requiresApproval: input.requiresApproval ?? null, status: 'pending', }); }), createPhaseTask: publicProcedure .input(z.object({ phaseId: z.string().min(1), name: z.string().min(1), description: z.string().optional(), category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).optional(), requiresApproval: z.boolean().nullable().optional(), })) .mutation(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); const phaseRepo = requirePhaseRepository(ctx); const phase = await phaseRepo.findById(input.phaseId); if (!phase) { throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found`, }); } return taskRepository.create({ phaseId: input.phaseId, name: input.name, description: input.description ?? null, category: input.category ?? 'execute', type: input.type ?? 'auto', requiresApproval: input.requiresApproval ?? null, status: 'pending', }); }), listPendingApprovals: publicProcedure .input(z.object({ initiativeId: z.string().optional(), phaseId: z.string().optional(), category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), }).optional()) .query(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); return taskRepository.findPendingApproval(input); }), listInitiativeTasks: publicProcedure .input(z.object({ initiativeId: z.string().min(1) })) .query(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); const tasks = await taskRepository.findByInitiativeId(input.initiativeId); return tasks; }), listPhaseTasks: publicProcedure .input(z.object({ phaseId: z.string().min(1) })) .query(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); const tasks = await taskRepository.findByPhaseId(input.phaseId); return tasks.filter((t) => t.category !== 'detail'); }), deleteTask: publicProcedure .input(z.object({ id: z.string().min(1) })) .mutation(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); await taskRepository.delete(input.id); return { success: true }; }), listPhaseTaskDependencies: publicProcedure .input(z.object({ phaseId: z.string().min(1) })) .query(async ({ ctx, input }) => { const taskRepo = requireTaskRepository(ctx); const tasks = await taskRepo.findByPhaseId(input.phaseId); const edges: Array<{ taskId: string; dependsOn: string[] }> = []; for (const t of tasks) { const deps = await taskRepo.getDependencies(t.id); if (deps.length > 0) edges.push({ taskId: t.id, dependsOn: deps }); } return edges; }), approveTask: publicProcedure .input(z.object({ taskId: z.string().min(1) })) .mutation(async ({ ctx, input }) => { const dispatchManager = requireDispatchManager(ctx); await dispatchManager.approveTask(input.taskId); return { success: true }; }), }; }