Layer 2: Orchestrator
The Intelligent Coordinator
The Orchestrator is the central brain of the CLI. It coordinates the entire execution, resolving dependencies, managing VFS lifecycle, and ensuring blueprints execute in the correct order.
Purpose & Responsibilities
The Orchestrator Layer sits between the CLI command interface and the blueprint execution engine.
Core Responsibilities:
- Dependency Resolution - Order modules using topological sort
- Blueprint Loading - Load blueprints from marketplace
- Configuration Merging - Merge defaults with user parameters
- VFS Lifecycle Management - Create and manage VFS per blueprint
- Execution Coordination - Call preprocessor β executor for each blueprint
- Error Aggregation - Collect and report errors across all blueprints
Implementation
File Location
src/agents/orchestrator-agent.tsClass Structure
export class OrchestratorAgent {
private projectManager: ProjectManager;
private blueprintLoader: BlueprintLoader;
private blueprintPreprocessor: BlueprintPreprocessor;
private blueprintExecutor: BlueprintExecutor;
private dependencyResolver: DependencyResolver;
constructor(projectManager: ProjectManager) {
this.projectManager = projectManager;
this.blueprintLoader = new BlueprintLoader();
this.blueprintPreprocessor = new BlueprintPreprocessor();
this.blueprintExecutor = new BlueprintExecutor(
projectManager.getProjectRoot()
);
this.dependencyResolver = new DependencyResolver();
}
async executeRecipe(genome: Genome): Promise<ExecutionResult> {
// Main orchestration logic
}
}Execution Flow
Step 1: Dependency Resolution
Implementation:
// Build dependency graph from modules
const dependencyGraph = new DependencyGraph();
for (const module of genome.modules) {
// Load blueprint to get prerequisites
const blueprint = await this.blueprintLoader.load(module.id);
// Add to graph
dependencyGraph.addNode(module.id);
// Add edges for prerequisites
blueprint.requires?.forEach(prereq => {
dependencyGraph.addEdge(prereq, module.id);
});
}
// Topological sort
const executionOrder = dependencyGraph.topologicalSort();
// Result: Adapters β Connectors β FeaturesCritical design: Topological sort automatically ensures adapters run before connectors.
Example order:
1. framework/nextjs (no prerequisites)
2. database/drizzle (no prerequisites)
3. ui/shadcn (requires: framework/nextjs)
4. connector/drizzle-nextjs (requires: framework/nextjs, database/drizzle)Step 2: Module Classification
The Orchestrator classifies modules by type:
function classifyModule(moduleId: string): ModuleType {
if (moduleId.startsWith('framework/')) return 'framework';
if (moduleId.startsWith('adapter/')) return 'adapter';
if (moduleId.startsWith('connector/')) return 'connector';
if (moduleId.startsWith('feature/')) return 'feature';
// Fallback: infer from category
const category = extractCategoryFromId(moduleId);
return category;
}Why classify?: Different module types may have different execution strategies (future enhancement).
Step 3: Configuration Merging
For each module, merge blueprint defaults with user parameters:
function mergeConfiguration(
module: GenomeModule,
blueprint: Blueprint
): MergedConfiguration {
return {
module: {
id: module.id,
version: module.version || 'latest',
defaults: blueprint.defaults || {},
parameters: deepmerge(
blueprint.defaults || {},
module.parameters || {}
)
},
project: this.projectManager.getContext(),
activeFeatures: Object.keys(module.features || {})
.filter(key => module.features[key] === true)
};
}Example:
// Blueprint defaults:
{
typescript: true,
tailwind: false,
appRouter: true
}
// User parameters:
{
tailwind: true // Override
}
// Merged result:
{
typescript: true, // Default
tailwind: true, // User override
appRouter: true // Default
}Step 4: Blueprint Execution Loop
The core orchestration loop:
async executeRecipe(genome: Genome, verbose: boolean, logger: EnhancedLogger) {
// Resolve execution order
const executionOrder = await this.resolveExecutionOrder(genome.modules);
const results: BlueprintResult[] = [];
// Execute each blueprint in order
for (const module of executionOrder) {
logger.startPhase(`Executing ${module.id}`);
try {
// 1. Load blueprint from marketplace
const blueprint = await this.blueprintLoader.load(module.id);
// 2. Merge configuration
const mergedConfig = this.mergeConfiguration(module, blueprint);
// 3. Create dedicated VFS for this blueprint
const vfs = new VirtualFileSystem(
`blueprint-${blueprint.id}`,
this.projectManager.getProjectRoot()
);
// 4. Preprocess blueprint (dynamic β static)
const preprocessResult = await this.blueprintPreprocessor.processBlueprint(
blueprint,
mergedConfig
);
if (!preprocessResult.success) {
throw new Error(`Preprocessing failed: ${preprocessResult.error}`);
}
// 5. Execute blueprint actions
const execResult = await this.blueprintExecutor.executeActions(
preprocessResult.actions,
mergedConfig,
vfs
);
if (!execResult.success) {
throw new Error(`Execution failed: ${execResult.errors.join(', ')}`);
}
// 6. Flush VFS to disk (atomic commit)
await vfs.flushToDisk();
logger.completePhase(`β
${module.id} completed`);
results.push({ success: true, module: module.id });
} catch (error) {
// Fail-fast: Stop execution on first error
logger.error(`β ${module.id} failed: ${error.message}`);
return {
success: false,
errors: [`Module ${module.id}: ${error.message}`],
completedModules: results
};
}
}
return {
success: true,
completedModules: results
};
}Key Design Decisions
1. Per-Blueprint VFS
Decision: Create new VFS for each blueprint
// NOT this (shared VFS):
const vfs = new VirtualFileSystem(...);
for (const blueprint of blueprints) {
await execute(blueprint, vfs); // Shared VFS
}
// But THIS (per-blueprint VFS):
for (const blueprint of blueprints) {
const vfs = new VirtualFileSystem(...); // New VFS
await execute(blueprint, vfs);
await vfs.flushToDisk(); // Commit per blueprint
}Rationale:
- β Isolation: Blueprint A can't corrupt Blueprint B's changes
- β Atomicity: Each blueprint is an atomic unit
- β Pre-population: Each VFS pre-loaded with only files that blueprint needs
- β Rollback: Blueprint fails β VFS discarded, no disk corruption
Trade-off: Memory overhead (multiple VFS instances), but safety worth it.
2. Fail-Fast Execution
Decision: Stop on first blueprint failure
if (!execResult.success) {
// Immediately return, don't continue
return { success: false, errors: [...] };
}Rationale:
- β Prevents cascading failures: Later blueprints might depend on earlier ones
- β Clear error reporting: One error at a time
- β No partial projects: Project is complete or not generated
Trade-off: Can't do "best effort" generation, but better UX.
3. Topological Sort for Execution Order
Decision: Use graph-based dependency resolution
const graph = new DependencyGraph();
// Add nodes and edges
modules.forEach(m => {
graph.addNode(m.id);
m.requires.forEach(req => graph.addEdge(req, m.id));
});
// Sort
const order = graph.topologicalSort();Ensures:
- Framework installed before UI library
- Database installed before database connector
- Auth installed before auth connector
Rationale: Explicit > implicit. Don't rely on module order in genome.
Error Handling
Error Types Handled
try {
await executeBlueprint(...);
} catch (error) {
if (error instanceof PrerequisiteError) {
// Missing dependency
logger.error(`Missing prerequisite: ${error.prerequisite}`);
suggestInstallation(error.prerequisite);
} else if (error instanceof BlueprintError) {
// Blueprint execution failed
logger.error(`Blueprint failed: ${error.message}`);
showBlueprintContext(error);
} else {
// Unknown error
logger.error(`Unexpected error: ${error.message}`);
}
}Error Recovery
Current: No recovery, fail-fast
Future consideration: Checkpoint system
// Proposed:
const checkpoint = createCheckpoint();
for (const blueprint of blueprints) {
try {
await execute(blueprint);
checkpoint.save(blueprint.id);
} catch (error) {
checkpoint.rollbackTo(lastSuccessful);
throw error;
}
}Integration with Other Systems
With CLI Command Layer
// src/commands/new.ts calls orchestrator
const orchestrator = new OrchestratorAgent(projectManager);
const result = await orchestrator.executeRecipe(genome, verbose, logger);With Blueprint Preprocessor
// Orchestrator calls preprocessor for each blueprint
const preprocessResult = await this.blueprintPreprocessor.processBlueprint(
blueprintModule,
mergedConfig
);With Blueprint Executor
// Orchestrator calls executor with preprocessed actions
const execResult = await this.blueprintExecutor.executeActions(
preprocessResult.actions,
mergedConfig,
vfs
);Logging & Progress Tracking
Phase-Based Logging
logger.startPhase('Validating Genome');
// ... validation
logger.completePhase('β
Validated');
logger.startPhase('Resolving Dependencies');
// ... resolution
logger.completePhase(`β
${modules.length} modules ordered`);
logger.startPhase('Executing Blueprints');
for (const module of modules) {
logger.logProgress(`[${i}/${total}] ${module.id}`);
// ... execution
}
logger.completePhase('β
All blueprints executed');Output:
π Validating Genome...
β
Completed in 45ms
π Resolving Dependencies...
β
8 modules ordered
ποΈ Executing Blueprints...
[1/8] framework/nextjs
β
framework/nextjs
[2/8] database/drizzle
β
database/drizzle
...Future Enhancements
1. Parallel Execution
Current: Sequential Proposed: Parallel execution for independent blueprints
// Group blueprints by dependency level
const levels = dependencyGraph.getLevels();
// Execute each level in parallel
for (const level of levels) {
await Promise.all(
level.map(blueprint => executeBlueprint(blueprint))
);
}Benefit: 2-3x faster for large genomes
2. Incremental Execution
Current: All-or-nothing Proposed: Resume from checkpoint
const completed = loadCheckpoint();
const remaining = modules.filter(m => !completed.includes(m.id));
for (const module of remaining) {
await execute(module);
saveCheckpoint(module.id);
}Benefit: User can resume after fixing errors
3. Dry-Run Mode Enhancement
Current: Basic preview Proposed: Full simulation with VFS
// Execute in dry-run mode
const result = await executeWithDryRun(genome);
// Show what would be created
result.files.forEach(file => console.log(`Would create: ${file}`));
result.modifications.forEach(mod => console.log(`Would modify: ${mod}`));Next Steps
- Layer 3: Preprocessor β - Dynamic blueprint handling
- Layer 4: Executor β - Action execution
- Dependency Resolution β - Deep dive into topological sort
The Orchestrator is the conductor. Understanding it is key to understanding the CLI.