feat: monorepo consolidation — forge pipeline, MACP protocol, framework plugin, profiles/guides/skills
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.
This commit is contained in:
182
packages/forge/src/board-tasks.ts
Normal file
182
packages/forge/src/board-tasks.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user