diff --git a/apps/server/trpc/routers/phase-dispatch.ts b/apps/server/trpc/routers/phase-dispatch.ts index 4524390..ede1831 100644 --- a/apps/server/trpc/routers/phase-dispatch.ts +++ b/apps/server/trpc/routers/phase-dispatch.ts @@ -4,10 +4,35 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; -import type { Task } from '../../db/schema.js'; +import type { Phase, Task } from '../../db/schema.js'; import type { ProcedureBuilder } from '../trpc.js'; import { requirePhaseDispatchManager, requirePhaseRepository, requireTaskRepository } from './_helpers.js'; +const INTEGRATION_PHASE_NAME = 'Integration'; + +const INTEGRATION_TASK_DESCRIPTION = `Verify that all phase branches integrate correctly after merging into the initiative branch. + +Steps: +1. Build the project — fix any compilation errors +2. Run the full test suite — fix any failing tests +3. Run type checking and linting — fix any errors +4. Review cross-phase imports and shared interfaces for compatibility +5. Smoke test key user flows affected by the merged changes + +Only fix integration issues (type mismatches, conflicting exports, broken tests). Do not refactor or improve existing code.`; + +/** + * Find phase IDs that have no dependents (no other phase depends on them). + * These are the "end" / "leaf" phases in the dependency graph. + */ +function findEndPhaseIds( + phases: Phase[], + edges: Array<{ phaseId: string; dependsOnPhaseId: string }>, +): string[] { + const dependedOn = new Set(edges.map((e) => e.dependsOnPhaseId)); + return phases.filter((p) => !dependedOn.has(p.id)).map((p) => p.id); +} + export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) { return { queuePhase: publicProcedure @@ -23,7 +48,40 @@ export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) { .mutation(async ({ ctx, input }) => { const phaseDispatchManager = requirePhaseDispatchManager(ctx); const phaseRepo = requirePhaseRepository(ctx); - const phases = await phaseRepo.findByInitiativeId(input.initiativeId); + const taskRepo = requireTaskRepository(ctx); + + let phases = await phaseRepo.findByInitiativeId(input.initiativeId); + const edges = await phaseRepo.findDependenciesByInitiativeId(input.initiativeId); + + // Auto-create Integration phase if multiple end phases exist + const existingIntegration = phases.find((p) => p.name === INTEGRATION_PHASE_NAME); + if (!existingIntegration) { + const endPhaseIds = findEndPhaseIds(phases, edges); + if (endPhaseIds.length > 1) { + const integrationPhase = await phaseRepo.create({ + initiativeId: input.initiativeId, + name: INTEGRATION_PHASE_NAME, + status: 'approved', + }); + + for (const endPhaseId of endPhaseIds) { + await phaseRepo.createDependency(integrationPhase.id, endPhaseId); + } + + await taskRepo.create({ + phaseId: integrationPhase.id, + initiativeId: input.initiativeId, + name: 'Verify integration', + description: INTEGRATION_TASK_DESCRIPTION, + category: 'verify', + status: 'pending', + }); + + // Re-fetch so the new phase gets queued in the loop below + phases = await phaseRepo.findByInitiativeId(input.initiativeId); + } + } + let queued = 0; for (const phase of phases) { if (phase.status === 'approved') { diff --git a/docs/dispatch-events.md b/docs/dispatch-events.md index 5d1b4e9..9fe59fe 100644 --- a/docs/dispatch-events.md +++ b/docs/dispatch-events.md @@ -93,6 +93,18 @@ InitiativeChangesRequestedEvent { initiativeId, phaseId, taskId } 4. **Auto-queue tasks** — When phase starts (branches confirmed), pending execution tasks are queued (planning-category tasks excluded) 5. **Events** — `phase:queued`, `phase:started`, `phase:completed`, `phase:blocked` +### Auto-Integration Phase + +When `queueAllPhases` is called (i.e. the user clicks "Execute"), it auto-creates an **Integration** phase if the initiative has multiple end phases (leaf nodes with no dependents). This catches cross-phase incompatibilities before the initiative reaches review. + +- **Trigger**: `queueAllPhases` in `apps/server/trpc/routers/phase-dispatch.ts` +- **Guard**: Only created when `endPhaseIds.length > 1` and no existing "Integration" phase +- **Status**: Created as `approved` (same pattern as Finalization in orchestrator.ts) +- **Dependencies**: Integration depends on all end phases — dispatched last +- **Task**: A single `verify` category task instructs the agent to build, run tests, check types, and review cross-phase imports +- **Idempotency**: Name-based check prevents duplicates on re-execution +- **Coexistence**: Independent of the Finalization phase (different purpose, different trigger) + ### PhaseDispatchManager Methods | Method | Purpose |