feat: auto-create Integration phase for multi-leaf initiatives

When an initiative has multiple end phases (leaf nodes with no
dependents), queueAllPhases now auto-creates an Integration phase
that depends on all of them. This catches cross-phase incompatibilities
(type mismatches, conflicting exports, broken tests) before review.
This commit is contained in:
Lukas May
2026-03-06 21:31:20 +01:00
parent 094b7e6307
commit 0f53930610
2 changed files with 72 additions and 2 deletions

View File

@@ -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') {

View File

@@ -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 |