Merge branch 'main' into cw/continuous-code-quality-conflict-1772832123778

# Conflicts:
#	apps/server/drizzle/meta/0037_snapshot.json
#	apps/server/drizzle/meta/_journal.json
This commit is contained in:
Lukas May
2026-03-06 22:30:21 +01:00
39 changed files with 5291 additions and 419 deletions

View File

@@ -475,8 +475,8 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) {
const uniqueTaskIds = [...new Set(filteredAgents.map(a => a.taskId).filter(Boolean) as string[])];
const uniqueInitiativeIds = [...new Set(filteredAgents.map(a => a.initiativeId).filter(Boolean) as string[])];
const [chunks, messageCounts, taskResults, initiativeResults] = await Promise.all([
logChunkRepo.findByAgentIds(matchingIds),
const [metrics, messageCounts, taskResults, initiativeResults] = await Promise.all([
logChunkRepo.findMetricsByAgentIds(matchingIds),
conversationRepo.countByFromAgentIds(matchingIds),
Promise.all(uniqueTaskIds.map(id => taskRepo.findById(id))),
Promise.all(uniqueInitiativeIds.map(id => initiativeRepo.findById(id))),
@@ -486,37 +486,14 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) {
const taskMap = new Map(taskResults.filter(Boolean).map(t => [t!.id, t!.name]));
const initiativeMap = new Map(initiativeResults.filter(Boolean).map(i => [i!.id, i!.name]));
const messagesMap = new Map(messageCounts.map(m => [m.agentId, m.count]));
// Group chunks by agentId
const chunksByAgent = new Map<string, typeof chunks>();
for (const chunk of chunks) {
const existing = chunksByAgent.get(chunk.agentId);
if (existing) {
existing.push(chunk);
} else {
chunksByAgent.set(chunk.agentId, [chunk]);
}
}
const metricsMap = new Map(metrics.map(m => [m.agentId, m]));
// Build result rows
return filteredAgents.map(agent => {
const agentChunks = chunksByAgent.get(agent.id) ?? [];
let questionsCount = 0;
let subagentsCount = 0;
let compactionsCount = 0;
for (const chunk of agentChunks) {
try {
const parsed = JSON.parse(chunk.content);
if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') {
questionsCount += parsed.input?.questions?.length ?? 0;
} else if (parsed.type === 'tool_use' && parsed.name === 'Agent') {
subagentsCount++;
} else if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') {
compactionsCount++;
}
} catch { /* skip malformed */ }
}
const agentMetrics = metricsMap.get(agent.id);
const questionsCount = agentMetrics?.questionsCount ?? 0;
const subagentsCount = agentMetrics?.subagentsCount ?? 0;
const compactionsCount = agentMetrics?.compactionsCount ?? 0;
return {
id: agent.id,

View File

@@ -337,6 +337,14 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) {
});
}
// Clean up orphaned pending/in_progress detail tasks from previous failed attempts
const phaseTasks = await taskRepo.findByPhaseId(input.phaseId);
for (const t of phaseTasks) {
if (t.category === 'detail' && (t.status === 'pending' || t.status === 'in_progress')) {
await taskRepo.update(t.id, { status: 'completed', summary: 'Superseded by retry' });
}
}
const detailTaskName = input.taskName ?? `Detail: ${phase.name}`;
const task = await taskRepo.create({
phaseId: phase.id,

View File

@@ -184,11 +184,11 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
.input(z.object({
projectId: z.string().optional(),
status: z.enum(ErrandStatusValues).optional(),
}))
}).optional())
.query(async ({ ctx, input }) => {
return requireErrandRepository(ctx).findAll({
projectId: input.projectId,
status: input.status,
projectId: input?.projectId,
status: input?.status,
});
}),

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