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:
199
packages/forge/__tests__/board-tasks.test.ts
Normal file
199
packages/forge/__tests__/board-tasks.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
import {
|
||||
buildPersonaBrief,
|
||||
writePersonaBrief,
|
||||
personaResultPath,
|
||||
synthesisResultPath,
|
||||
generateBoardTasks,
|
||||
synthesizeReviews,
|
||||
} from '../src/board-tasks.js';
|
||||
import type { BoardPersona, PersonaReview } from '../src/types.js';
|
||||
|
||||
const testPersonas: BoardPersona[] = [
|
||||
{ name: 'CEO', slug: 'ceo', description: 'The CEO sets direction.', path: 'agents/board/ceo.md' },
|
||||
{
|
||||
name: 'CTO',
|
||||
slug: 'cto',
|
||||
description: 'The CTO evaluates feasibility.',
|
||||
path: 'agents/board/cto.md',
|
||||
},
|
||||
];
|
||||
|
||||
describe('buildPersonaBrief', () => {
|
||||
it('includes persona name and description', () => {
|
||||
const brief = buildPersonaBrief('Build feature X', testPersonas[0]!);
|
||||
expect(brief).toContain('# Board Evaluation: CEO');
|
||||
expect(brief).toContain('The CEO sets direction.');
|
||||
expect(brief).toContain('Build feature X');
|
||||
expect(brief).toContain('"persona": "CEO"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('writePersonaBrief', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-board-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('writes brief file to disk', () => {
|
||||
const briefPath = writePersonaBrief(tmpDir, 'BOARD', testPersonas[0]!, 'Test brief');
|
||||
expect(fs.existsSync(briefPath)).toBe(true);
|
||||
const content = fs.readFileSync(briefPath, 'utf-8');
|
||||
expect(content).toContain('Board Evaluation: CEO');
|
||||
});
|
||||
});
|
||||
|
||||
describe('personaResultPath', () => {
|
||||
it('builds correct path', () => {
|
||||
const p = personaResultPath('/run/abc', 'BOARD-ceo');
|
||||
expect(p).toContain('01-board/results/BOARD-ceo.board.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('synthesisResultPath', () => {
|
||||
it('builds correct path', () => {
|
||||
const p = synthesisResultPath('/run/abc', 'BOARD-SYNTHESIS');
|
||||
expect(p).toContain('01-board/results/BOARD-SYNTHESIS.board.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateBoardTasks', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-board-tasks-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('generates one task per persona plus synthesis', () => {
|
||||
const tasks = generateBoardTasks('Test brief', testPersonas, tmpDir);
|
||||
expect(tasks).toHaveLength(3); // 2 personas + 1 synthesis
|
||||
});
|
||||
|
||||
it('persona tasks have no dependsOn', () => {
|
||||
const tasks = generateBoardTasks('Test brief', testPersonas, tmpDir);
|
||||
expect(tasks[0]!.dependsOn).toBeUndefined();
|
||||
expect(tasks[1]!.dependsOn).toBeUndefined();
|
||||
});
|
||||
|
||||
it('synthesis task depends on all persona tasks', () => {
|
||||
const tasks = generateBoardTasks('Test brief', testPersonas, tmpDir);
|
||||
const synthesis = tasks[tasks.length - 1]!;
|
||||
expect(synthesis.id).toBe('BOARD-SYNTHESIS');
|
||||
expect(synthesis.dependsOn).toEqual(['BOARD-ceo', 'BOARD-cto']);
|
||||
expect(synthesis.dependsOnPolicy).toBe('all_terminal');
|
||||
});
|
||||
|
||||
it('persona tasks have correct metadata', () => {
|
||||
const tasks = generateBoardTasks('Test brief', testPersonas, tmpDir);
|
||||
expect(tasks[0]!.metadata['personaName']).toBe('CEO');
|
||||
expect(tasks[0]!.metadata['personaSlug']).toBe('ceo');
|
||||
});
|
||||
|
||||
it('uses custom base task ID', () => {
|
||||
const tasks = generateBoardTasks('Brief', testPersonas, tmpDir, 'CUSTOM');
|
||||
expect(tasks[0]!.id).toBe('CUSTOM-ceo');
|
||||
expect(tasks[tasks.length - 1]!.id).toBe('CUSTOM-SYNTHESIS');
|
||||
});
|
||||
|
||||
it('writes persona brief files to disk', () => {
|
||||
generateBoardTasks('Test brief', testPersonas, tmpDir);
|
||||
const briefDir = path.join(tmpDir, '01-board', 'briefs');
|
||||
expect(fs.existsSync(briefDir)).toBe(true);
|
||||
const files = fs.readdirSync(briefDir);
|
||||
expect(files).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('synthesizeReviews', () => {
|
||||
const makeReview = (
|
||||
persona: string,
|
||||
verdict: PersonaReview['verdict'],
|
||||
confidence: number,
|
||||
): PersonaReview => ({
|
||||
persona,
|
||||
verdict,
|
||||
confidence,
|
||||
concerns: [`${persona} concern`],
|
||||
recommendations: [`${persona} rec`],
|
||||
keyRisks: [`${persona} risk`],
|
||||
});
|
||||
|
||||
it('returns approve when all approve', () => {
|
||||
const result = synthesizeReviews([
|
||||
makeReview('CEO', 'approve', 0.8),
|
||||
makeReview('CTO', 'approve', 0.9),
|
||||
]);
|
||||
expect(result.verdict).toBe('approve');
|
||||
expect(result.confidence).toBe(0.85);
|
||||
expect(result.persona).toBe('Board Synthesis');
|
||||
});
|
||||
|
||||
it('returns reject when any reject', () => {
|
||||
const result = synthesizeReviews([
|
||||
makeReview('CEO', 'approve', 0.8),
|
||||
makeReview('CTO', 'reject', 0.7),
|
||||
]);
|
||||
expect(result.verdict).toBe('reject');
|
||||
});
|
||||
|
||||
it('returns conditional when any conditional (no reject)', () => {
|
||||
const result = synthesizeReviews([
|
||||
makeReview('CEO', 'approve', 0.8),
|
||||
makeReview('CTO', 'conditional', 0.6),
|
||||
]);
|
||||
expect(result.verdict).toBe('conditional');
|
||||
});
|
||||
|
||||
it('merges and deduplicates concerns', () => {
|
||||
const reviews = [makeReview('CEO', 'approve', 0.8), makeReview('CTO', 'approve', 0.9)];
|
||||
const result = synthesizeReviews(reviews);
|
||||
expect(result.concerns).toEqual(['CEO concern', 'CTO concern']);
|
||||
expect(result.recommendations).toEqual(['CEO rec', 'CTO rec']);
|
||||
});
|
||||
|
||||
it('deduplicates identical items', () => {
|
||||
const r1: PersonaReview = {
|
||||
persona: 'CEO',
|
||||
verdict: 'approve',
|
||||
confidence: 0.8,
|
||||
concerns: ['shared concern'],
|
||||
recommendations: [],
|
||||
keyRisks: [],
|
||||
};
|
||||
const r2: PersonaReview = {
|
||||
persona: 'CTO',
|
||||
verdict: 'approve',
|
||||
confidence: 0.8,
|
||||
concerns: ['shared concern'],
|
||||
recommendations: [],
|
||||
keyRisks: [],
|
||||
};
|
||||
const result = synthesizeReviews([r1, r2]);
|
||||
expect(result.concerns).toEqual(['shared concern']);
|
||||
});
|
||||
|
||||
it('includes original reviews', () => {
|
||||
const reviews = [makeReview('CEO', 'approve', 0.8)];
|
||||
const result = synthesizeReviews(reviews);
|
||||
expect(result.reviews).toEqual(reviews);
|
||||
});
|
||||
|
||||
it('handles empty reviews', () => {
|
||||
const result = synthesizeReviews([]);
|
||||
expect(result.verdict).toBe('approve');
|
||||
expect(result.confidence).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user