/** * Quality Review Service * * Decides whether to run a quality review after a task agent completes, * and orchestrates spawning the review agent. * * All dependencies are passed as function parameters (hexagonal DI pattern). */ import type { BranchManager } from '../git/branch-manager.js'; import type { AgentRepository } from '../db/repositories/agent-repository.js'; import type { TaskRepository } from '../db/repositories/task-repository.js'; import type { InitiativeRepository } from '../db/repositories/initiative-repository.js'; import type { AgentManager } from '../agent/types.js'; import type { Logger } from 'pino'; // --------------------------------------------------------------------------- // computeQualifyingFiles // --------------------------------------------------------------------------- /** * Compute source files in the diff between taskBranch and baseBranch that * qualify for a quality review (excludes *.gen.ts and dist/ paths). * * Returns [] if the diff throws (treated as no qualifying files). */ export async function computeQualifyingFiles( repoPath: string, taskBranch: string, baseBranch: string, branchManager: BranchManager, ): Promise { try { const entries = await branchManager.diffBranchesStat(repoPath, baseBranch, taskBranch); return entries .map((e) => e.path) .filter((p) => !p.endsWith('.gen.ts')) .filter((p) => !p.startsWith('dist/') && !p.includes('/dist/')); } catch { return []; } } // --------------------------------------------------------------------------- // shouldRunQualityReview // --------------------------------------------------------------------------- export interface QualityReviewCheckParams { agentId: string; taskId: string; stopReason: string; repoPath: string; taskBranch: string; baseBranch: string; agentRepository: AgentRepository; taskRepository: TaskRepository; initiativeRepository: InitiativeRepository; branchManager: BranchManager; } /** * Determine whether a quality review should be run for the given agent stop event. * * Returns { run: true, qualifyingFiles } only when all six conditions pass: * 1. stopReason === 'task_complete' * 2. Agent mode is 'execute' * 3. Task status is 'in_progress' (not 'quality_review' — recursion guard) * 4. task.initiativeId is non-null * 5. initiative.qualityReview === true * 6. computeQualifyingFiles() returns a non-empty array */ export async function shouldRunQualityReview( params: QualityReviewCheckParams, ): Promise<{ run: boolean; qualifyingFiles: string[] }> { const { agentId, taskId, stopReason, repoPath, taskBranch, baseBranch, agentRepository, taskRepository, initiativeRepository, branchManager } = params; const NO = { run: false, qualifyingFiles: [] }; // 1. Only fire on task_complete if (stopReason !== 'task_complete') return NO; // 2. Agent mode must be 'execute' const agent = await agentRepository.findById(agentId); if (!agent || agent.mode !== 'execute') return NO; // 3. Task status must be 'in_progress' (recursion guard: skip if already quality_review) const task = await taskRepository.findById(taskId); if (!task) return NO; if (task.status === 'quality_review') return NO; if (task.status !== 'in_progress') return NO; // 4. Task must belong to an initiative if (!task.initiativeId) return NO; // 5. Initiative must have qualityReview enabled const initiative = await initiativeRepository.findById(task.initiativeId); if (!initiative || !initiative.qualityReview) return NO; // 6. Must have qualifying files in the changeset const qualifyingFiles = await computeQualifyingFiles(repoPath, taskBranch, baseBranch, branchManager); if (qualifyingFiles.length === 0) return NO; return { run: true, qualifyingFiles }; } // --------------------------------------------------------------------------- // runQualityReview // --------------------------------------------------------------------------- export interface QualityReviewRunParams { taskId: string; taskBranch: string; baseBranch: string; initiativeId: string; qualifyingFiles: string[]; taskRepository: TaskRepository; agentManager: AgentManager; log: Logger; } /** * Spawn a quality review agent on the task branch. * * 1. Transitions task to 'quality_review' * 2. Builds /simplify prompt with qualifying files * 3. Spawns execute-mode agent on the same task branch * 4. Logs the review agent ID * 5. On spawn error: logs and transitions task to 'completed' — never throws */ export async function runQualityReview(params: QualityReviewRunParams): Promise { const { taskId, taskBranch, baseBranch, initiativeId, qualifyingFiles, taskRepository, agentManager, log } = params; await taskRepository.update(taskId, { status: 'quality_review' }); const fileList = qualifyingFiles.join('\n'); const prompt = `Run /simplify to review and fix code quality in this branch.\n\n${fileList}`; try { const reviewAgent = await agentManager.spawn({ taskId, initiativeId, prompt, mode: 'execute', baseBranch, branchName: taskBranch, }); log.info({ taskId, reviewAgentId: reviewAgent.id }, 'quality review agent spawned'); } catch (err) { log.error({ taskId, err: err instanceof Error ? err.message : String(err) }, 'quality review agent spawn failed'); await taskRepository.update(taskId, { status: 'completed' }); } }