diff --git a/src/trpc/router.ts b/src/trpc/router.ts index 8fdbe2b..d11dda7 100644 --- a/src/trpc/router.ts +++ b/src/trpc/router.ts @@ -13,9 +13,10 @@ import type { TaskRepository } from '../db/repositories/task-repository.js'; import type { MessageRepository } from '../db/repositories/message-repository.js'; import type { InitiativeRepository } from '../db/repositories/initiative-repository.js'; 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 } from '../db/schema.js'; +import type { Phase, Plan } from '../db/schema.js'; import { buildDiscussPrompt, buildBreakdownPrompt } from '../agent/prompts.js'; /** @@ -264,6 +265,19 @@ function requirePhaseRepository(ctx: TRPCContext): PhaseRepository { return ctx.phaseRepository; } +/** + * Helper to ensure planRepository is available in context. + */ +function requirePlanRepository(ctx: TRPCContext): PlanRepository { + if (!ctx.planRepository) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Plan repository not available', + }); + } + return ctx.planRepository; +} + // ============================================================================= // Application Router with Procedures // ============================================================================= @@ -841,6 +855,79 @@ export const appRouter = router({ return created; }), + // =========================================================================== + // Plan Procedures + // =========================================================================== + + /** + * Create a new plan for a phase. + * Auto-assigns the next plan number if not provided. + */ + createPlan: publicProcedure + .input(z.object({ + phaseId: z.string().min(1), + number: z.number().int().positive().optional(), + name: z.string().min(1), + description: z.string().optional(), + })) + .mutation(async ({ ctx, input }) => { + const repo = requirePlanRepository(ctx); + const number = input.number ?? await repo.getNextNumber(input.phaseId); + return repo.create({ + phaseId: input.phaseId, + number, + name: input.name, + description: input.description ?? null, + status: 'pending', + }); + }), + + /** + * List plans for a phase. + * Returns plans ordered by number. + */ + listPlans: publicProcedure + .input(z.object({ phaseId: z.string().min(1) })) + .query(async ({ ctx, input }) => { + const repo = requirePlanRepository(ctx); + return repo.findByPhaseId(input.phaseId); + }), + + /** + * Get a plan by ID. + * Throws NOT_FOUND if plan doesn't exist. + */ + getPlan: publicProcedure + .input(z.object({ id: z.string().min(1) })) + .query(async ({ ctx, input }) => { + const repo = requirePlanRepository(ctx); + const plan = await repo.findById(input.id); + if (!plan) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Plan '${input.id}' not found`, + }); + } + return plan; + }), + + /** + * Update a plan. + * Returns the updated plan. + */ + updatePlan: publicProcedure + .input(z.object({ + id: z.string().min(1), + name: z.string().min(1).optional(), + description: z.string().optional(), + status: z.enum(['pending', 'in_progress', 'completed']).optional(), + })) + .mutation(async ({ ctx, input }) => { + const repo = requirePlanRepository(ctx); + const { id, ...data } = input; + return repo.update(id, data); + }), + // =========================================================================== // Architect Spawn Procedures // ===========================================================================