Work packages completed: - WP1: packages/forge — pipeline runner, stage adapter, board tasks, brief classifier, persona loader with project-level overrides. 89 tests, 95.62% coverage. - WP2: packages/macp — credential resolver, gate runner, event emitter, protocol types. 65 tests, 96.24% coverage. Full Python-to-TS port preserving all behavior. - WP3: plugins/mosaic-framework — OC rails injection plugin (before_agent_start + subagent_spawning hooks for Mosaic contract enforcement). - WP4: profiles/ (domains, tech-stacks, workflows), guides/ (17 docs), skills/ (5 universal skills), forge pipeline assets (48 markdown files). Board deliberation: docs/reviews/consolidation-board-memo.md Brief: briefs/monorepo-consolidation.md Consolidates mosaic/stack (forge, MACP, bootstrap framework) into mosaic/mosaic-stack. 154 new tests total. Zero Python — all TypeScript/ESM.
183 lines
4.9 KiB
TypeScript
183 lines
4.9 KiB
TypeScript
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<string>();
|
|
const result: string[] = [];
|
|
for (const item of items) {
|
|
if (!seen.has(item)) {
|
|
seen.add(item);
|
|
result.push(item);
|
|
}
|
|
}
|
|
return result;
|
|
}
|