import fs from 'node:fs'; import path from 'node:path'; import type { BoardPersona, BoardSynthesis, ForgeTask, PersonaReview } from './types.js'; /** * Build the brief content for a persona's board evaluation. */ export function buildPersonaBrief(brief: string, persona: BoardPersona): string { return [ `# Board Evaluation: ${persona.name}`, '', '## Your Role', persona.description, '', '## Brief Under Review', brief.trim(), '', '## Instructions', 'Evaluate this brief from your perspective. Output a JSON object:', '{', ` "persona": "${persona.name}",`, ' "verdict": "approve|reject|conditional",', ' "confidence": 0.0-1.0,', ' "concerns": ["..."],', ' "recommendations": ["..."],', ' "key_risks": ["..."]', '}', '', ].join('\n'); } /** * Write a persona brief to the run directory and return the path. */ export function writePersonaBrief( runDir: string, baseTaskId: string, persona: BoardPersona, brief: string, ): string { const briefDir = path.join(runDir, '01-board', 'briefs'); fs.mkdirSync(briefDir, { recursive: true }); const briefPath = path.join(briefDir, `${baseTaskId}-${persona.slug}.md`); fs.writeFileSync(briefPath, buildPersonaBrief(brief, persona), 'utf-8'); return briefPath; } /** * Get the result path for a persona's board review. */ export function personaResultPath(runDir: string, taskId: string): string { return path.join(runDir, '01-board', 'results', `${taskId}.board.json`); } /** * Get the result path for the board synthesis. */ export function synthesisResultPath(runDir: string, taskId: string): string { return path.join(runDir, '01-board', 'results', `${taskId}.board.json`); } /** * Generate one ForgeTask per board persona plus one synthesis task. * * Persona tasks run independently (no depends_on). * The synthesis task depends on all persona tasks with 'all_terminal' policy. */ export function generateBoardTasks( brief: string, personas: BoardPersona[], runDir: string, baseTaskId = 'BOARD', ): ForgeTask[] { const tasks: ForgeTask[] = []; const personaTaskIds: string[] = []; const personaResultPaths: string[] = []; for (const persona of personas) { const taskId = `${baseTaskId}-${persona.slug}`; personaTaskIds.push(taskId); const briefPath = writePersonaBrief(runDir, baseTaskId, persona, brief); const resultRelPath = personaResultPath(runDir, taskId); personaResultPaths.push(resultRelPath); tasks.push({ id: taskId, title: `Board review: ${persona.name}`, description: `Independent board evaluation for ${persona.name}.`, type: 'review', dispatch: 'exec', status: 'pending', briefPath, resultPath: resultRelPath, timeoutSeconds: 120, qualityGates: ['true'], metadata: { personaName: persona.name, personaSlug: persona.slug, personaPath: persona.path, resultOutputPath: resultRelPath, }, }); } // Synthesis task — merges all persona reviews const synthesisId = `${baseTaskId}-SYNTHESIS`; const synthesisResult = synthesisResultPath(runDir, synthesisId); tasks.push({ id: synthesisId, title: 'Board synthesis', description: 'Merge independent board reviews into a single recommendation.', type: 'review', dispatch: 'exec', status: 'pending', briefPath: '', resultPath: synthesisResult, timeoutSeconds: 120, dependsOn: personaTaskIds, dependsOnPolicy: 'all_terminal', qualityGates: ['true'], metadata: { resultOutputPath: synthesisResult, inputResultPaths: personaResultPaths, }, }); return tasks; } /** * Merge multiple persona reviews into a board synthesis. */ export function synthesizeReviews(reviews: PersonaReview[]): BoardSynthesis { const verdicts = reviews.map((r) => r.verdict); let mergedVerdict: PersonaReview['verdict']; if (verdicts.includes('reject')) { mergedVerdict = 'reject'; } else if (verdicts.includes('conditional')) { mergedVerdict = 'conditional'; } else { mergedVerdict = 'approve'; } const confidenceValues = reviews.map((r) => r.confidence); const avgConfidence = confidenceValues.length > 0 ? Math.round((confidenceValues.reduce((a, b) => a + b, 0) / confidenceValues.length) * 1000) / 1000 : 0; const concerns = unique(reviews.flatMap((r) => r.concerns)); const recommendations = unique(reviews.flatMap((r) => r.recommendations)); const keyRisks = unique(reviews.flatMap((r) => r.keyRisks)); return { persona: 'Board Synthesis', verdict: mergedVerdict, confidence: avgConfidence, concerns, recommendations, keyRisks, reviews, }; } /** Deduplicate while preserving order. */ function unique(items: string[]): string[] { const seen = new Set(); const result: string[] = []; for (const item of items) { if (!seen.has(item)) { seen.add(item); result.push(item); } } return result; }