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.
8.2 KiB
WP1: packages/forge — Forge Pipeline Package
Context
Port the Forge progressive refinement pipeline from Python (~/src/mosaic-stack/forge/) to TypeScript as packages/forge in this monorepo. The pipeline markdown assets (stages, agents, personas, rails, gates, templates) are already copied to packages/forge/pipeline/. This task is the TypeScript implementation layer.
Board decisions that constrain this work:
- Abstract TaskExecutor interface — packages/forge must NOT hard-import packages/coord. Define an abstract interface; coord satisfies it.
- Clean index.ts exports, no internal path leakage, no hardcoded paths
- 85% test coverage on TS implementation files (markdown assets excluded)
- Test strategy for non-deterministic AI orchestration: fixture-based integration tests
- OpenBrain is OUT OF SCOPE
- ESM only, zero Python
Dependencies available:
@mosaic/macp(packages/macp) is built and provides: GateEntry, GateResult, Task types, credential resolution, gate running, event emission
Source Files (Python → TypeScript)
1. types.ts
Define all Forge-specific types:
// Stage specification
interface StageSpec {
number: string;
title: string;
dispatch: 'exec' | 'yolo' | 'pi';
type: 'research' | 'review' | 'coding' | 'deploy';
gate: string;
promptFile: string;
qualityGates: (string | GateEntry)[];
}
// Brief classification
type BriefClass = 'strategic' | 'technical' | 'hotfix';
type ClassSource = 'cli' | 'frontmatter' | 'auto';
// Run manifest (persisted to disk)
interface RunManifest {
runId: string;
brief: string;
codebase: string;
briefClass: BriefClass;
classSource: ClassSource;
forceBoard: boolean;
createdAt: string;
updatedAt: string;
currentStage: string;
status: 'in_progress' | 'completed' | 'failed' | 'interrupted' | 'rejected';
stages: Record<string, StageStatus>;
}
// Abstract task executor (decouples from packages/coord)
interface TaskExecutor {
submitTask(task: ForgeTask): Promise<void>;
waitForCompletion(taskId: string, timeoutMs: number): Promise<TaskResult>;
}
// Persona override config
interface ForgeConfig {
board?: {
additionalMembers?: string[];
skipMembers?: string[];
};
specialists?: {
alwaysInclude?: string[];
};
}
2. constants.ts
Source: Top of ~/src/mosaic-stack/forge/lib (ALL_STAGES, LABELS, STAGE_SPECS equivalent) + ~/src/mosaic-stack/forge/pipeline/orchestrator/stage_adapter.py (STAGE_TIMEOUTS)
export const STAGE_SEQUENCE = [
'00-intake',
'00b-discovery',
'01-board',
'01b-brief-analyzer',
'02-planning-1',
'03-planning-2',
'04-planning-3',
'05-coding',
'06-review',
'07-remediate',
'08-test',
'09-deploy',
];
export const STAGE_TIMEOUTS: Record<string, number> = {
'00-intake': 120,
'00b-discovery': 300,
'01-board': 120,
'02-planning-1': 600,
// ... etc
};
export const STAGE_LABELS: Record<string, string> = {
'00-intake': 'INTAKE',
// ... etc
};
Also: STRATEGIC_KEYWORDS, TECHNICAL_KEYWORDS for brief classification.
3. brief-classifier.ts
Source: classify_brief(), parse_brief_frontmatter(), stages_for_class() from ~/src/mosaic-stack/forge/lib
- Auto-classify brief by keyword analysis (strategic vs technical)
- Parse YAML frontmatter for explicit
class:field - CLI flag override
- Return stage list based on classification (strategic = full pipeline, technical = skip board, hotfix = skip board + brief analyzer)
4. stage-adapter.ts
Source: ~/src/mosaic-stack/forge/pipeline/orchestrator/stage_adapter.py
mapStageToTask(): Convert a Forge stage into a task compatible with TaskExecutor- Stage briefs written to
{runDir}/{stageName}/brief.md - Result paths at
{runDir}/{stageName}/result.json - Previous results read from disk at runtime (not baked into brief)
- Per-stage timeouts from STAGE_TIMEOUTS
- depends_on chain built from stage sequence
5. board-tasks.ts
Source: ~/src/mosaic-stack/forge/pipeline/orchestrator/board_tasks.py
loadBoardPersonas(): Read all .md files frompipeline/agents/board/generateBoardTasks(): One task per persona + synthesis task- Synthesis depends on all persona tasks with
depends_on_policy: 'all_terminal' - Persona briefs include role description + brief under review
- Synthesis script merges independent reviews into board memo
6. pipeline-runner.ts
Source: ~/src/mosaic-stack/forge/pipeline/orchestrator/pipeline_runner.py + ~/src/mosaic-stack/forge/lib (cmd_run, cmd_resume, cmd_status)
runPipeline(briefPath, projectRoot, options): Full pipeline execution- Creates run directory at
{projectRoot}/.forge/runs/{runId}/ - Generates tasks for all stages, submits to TaskExecutor
- Tracks manifest.json with stage statuses
resumePipeline(runDir): Pick up from last incomplete stagegetPipelineStatus(runDir): Read manifest and report
Key difference from Python: Run output goes to PROJECT-scoped .forge/runs/, not inside the Forge package.
7. Persona Override System (NEW — not in Python)
- Base personas read from
packages/forge/pipeline/agents/ - Project overrides read from
{projectRoot}/.forge/personas/{role}.md - Merge strategy: project persona content APPENDED to base persona (not replaced)
- Board composition configurable via
{projectRoot}/.forge/config.yaml - If no project config exists, use defaults (all base personas, no overrides)
Package Structure
packages/forge/
├── src/
│ ├── index.ts
│ ├── types.ts
│ ├── constants.ts
│ ├── brief-classifier.ts
│ ├── stage-adapter.ts
│ ├── board-tasks.ts
│ ├── pipeline-runner.ts
│ └── persona-loader.ts
├── pipeline/ # Already copied (WP4) — markdown assets
│ ├── stages/
│ ├── agents/
│ ├── rails/
│ ├── gates/
│ └── templates/
├── __tests__/
│ ├── brief-classifier.test.ts
│ ├── stage-adapter.test.ts
│ ├── board-tasks.test.ts
│ ├── pipeline-runner.test.ts
│ └── persona-loader.test.ts
├── package.json
├── tsconfig.json
└── vitest.config.ts
Package.json
{
"name": "@mosaic/forge",
"version": "0.0.1",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@mosaic/macp": "workspace:*"
},
"devDependencies": {
"vitest": "workspace:*",
"typescript": "workspace:*"
}
}
Only dependency: @mosaic/macp (for gate types, event emission).
Test Strategy (Board requirement)
Deterministic code (brief-classifier, stage-adapter, board-tasks, persona-loader, constants):
- Standard unit tests with known inputs/outputs
- 100% of classification logic, stage mapping, persona loading covered
Non-deterministic code (pipeline-runner):
- Fixture-based integration tests using a mock TaskExecutor
- Mock executor returns pre-recorded results for each stage
- Tests verify: manifest progression, stage ordering, dependency enforcement, resume behavior, error handling
- NO real AI calls in tests
Markdown assets: Excluded from coverage measurement (configure vitest to exclude pipeline/ directory).
ESM Requirements
"type": "module"in package.json- NodeNext module resolution in tsconfig
.jsextensions in all imports- No CommonJS
Key Design: Abstract TaskExecutor
// In packages/forge/src/types.ts
export interface TaskExecutor {
submitTask(task: ForgeTask): Promise<void>;
waitForCompletion(taskId: string, timeoutMs: number): Promise<TaskResult>;
getTaskStatus(taskId: string): Promise<TaskStatus>;
}
// In packages/coord (or wherever the concrete impl lives)
export class CoordTaskExecutor implements TaskExecutor {
// ... uses packages/coord runner
}
This means packages/forge can be tested with a mock executor and deployed with any backend.
Asset Resolution
Pipeline markdown assets (stages, personas, rails) must be resolved relative to the package installation, NOT hardcoded paths:
// Use import.meta.url to find package root
const PACKAGE_ROOT = new URL('..', import.meta.url).pathname;
const PIPELINE_DIR = path.join(PACKAGE_ROOT, 'pipeline');
Project-level overrides resolved relative to projectRoot parameter.