/** * Quality Review Dispatch Hook * * Intercepts agent:stopped events and, when conditions are met, spawns * a fresh execute-mode agent to run /simplify on changed files before * the task reaches 'completed' status. */ import type { BranchManager } from '../git/branch-manager.js'; import type { AgentManager } from '../agent/types.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 { PhaseRepository } from '../db/repositories/phase-repository.js'; import { phaseBranchName, taskBranchName } from '../git/branch-naming.js'; import type { createModuleLogger } from '../logger/index.js'; type Logger = ReturnType; // --------------------------------------------------------------------------- // computeQualifyingFiles // --------------------------------------------------------------------------- /** * Returns the list of .ts/.tsx/.js files changed between taskBranch and baseBranch, * excluding generated files and dist artifacts. */ export async function computeQualifyingFiles( branchManager: BranchManager, repoPath: string, taskBranch: string, baseBranch: string, ): Promise { try { const entries = await branchManager.diffBranchesStat(repoPath, baseBranch, taskBranch); return entries .map((e) => e.path) .filter( (p) => /\.(ts|tsx|js)$/.test(p) && !p.endsWith('.gen.ts') && !p.startsWith('dist/') && !p.includes('/dist/'), ); } catch { return []; } } // --------------------------------------------------------------------------- // shouldRunQualityReview // --------------------------------------------------------------------------- interface ShouldRunParams { agentId: string; taskId: string; stopReason: string; agentRepository: AgentRepository; taskRepository: TaskRepository; initiativeRepository: InitiativeRepository; phaseRepository: PhaseRepository; branchManager: BranchManager; repoPath: string; } /** * Evaluates whether a quality review should be run for the stopped agent. * Returns `{ run: true, qualifyingFiles }` only when all conditions pass. * Short-circuits on first false condition. */ export async function shouldRunQualityReview( params: ShouldRunParams, ): Promise<{ run: boolean; qualifyingFiles: string[] }> { const { agentId, taskId, stopReason, agentRepository, taskRepository, initiativeRepository, phaseRepository, branchManager, repoPath, } = params; const NO = { run: false, qualifyingFiles: [] }; // 1. Only act on task_complete stops if (stopReason !== 'task_complete') return NO; // 2. Agent must be in execute mode (guards against errand agents) const agent = await agentRepository.findById(agentId); if (!agent || agent.mode !== 'execute') return NO; // 3. Task must be in_progress; quality_review is the recursion guard 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 !== true) return NO; // 6. Compute branch names from task context if (!task.phaseId) return NO; const phase = await phaseRepository.findById(task.phaseId); if (!phase) return NO; const initBranch = initiative.branch; if (!initBranch) return NO; const base = phaseBranchName(initBranch, phase.name); const branch = taskBranchName(initBranch, task.id); // 7. Must have qualifying files in the changeset const qualifyingFiles = await computeQualifyingFiles(branchManager, repoPath, branch, base); if (qualifyingFiles.length === 0) return NO; return { run: true, qualifyingFiles }; } // --------------------------------------------------------------------------- // runQualityReview // --------------------------------------------------------------------------- interface RunQualityReviewParams { taskId: string; taskBranch: string; baseBranch: string; initiativeId: string; qualifyingFiles: string[]; taskRepository: TaskRepository; agentManager: AgentManager; log: Logger; } /** * Transitions the task to quality_review and spawns a fresh execute-mode * agent to run /simplify on the changed files. * * On spawn failure: marks task completed and returns (never throws). */ export async function runQualityReview(params: RunQualityReviewParams): Promise { const { taskId, taskBranch, baseBranch, initiativeId, qualifyingFiles, taskRepository, agentManager, log } = params; // 1. Transition BEFORE spawning await taskRepository.update(taskId, { status: 'quality_review' }); // 2. Build prompt const fileList = qualifyingFiles.map((f) => `- ${f}`).join('\n'); const reviewPrompt = `Run /simplify to review and fix code quality in this branch.\n\nFiles changed in this task:\n${fileList}`; // 3. Spawn fresh execute-mode agent on the same task branch try { const reviewAgent = await agentManager.spawn({ taskId, initiativeId, prompt: reviewPrompt, mode: 'execute', baseBranch, branchName: taskBranch, }); // 4. Log success log.info({ taskId, reviewAgentId: reviewAgent.id }, 'quality review agent spawned'); } catch (err) { // 5. On spawn failure: mark completed and return — never block task completion log.error({ taskId, err }, 'quality review spawn failed; marking task completed'); await taskRepository.update(taskId, { status: 'completed' }); } }