UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

889 lines (772 loc) 35.3 kB
/** * Greenfield Handler - Story 12.13 * * Epic 12: Bob Full Integration — Completando o PRD v2.0 * * Orchestrates the complete Greenfield Workflow (greenfield-fullstack.yaml) * for users starting a brand new project from scratch. * * Features: * - AC1: Detects Greenfield state (no package.json, .git, docs/) * - AC2-5: Orchestrates 4 phases (Bootstrap → Discovery → Sharding → Dev Cycle) * - AC6-10: Integrates with all Epic 11 modules * - AC11-14: Surface decisions between phases with PAUSE/resume * - AC15-17: Error handling (Retry/Skip/Abort) and idempotency * * @module core/orchestration/greenfield-handler * @version 1.0.0 */ 'use strict'; const fs = require('fs'); const path = require('path'); const EventEmitter = require('events'); // ═══════════════════════════════════════════════════════════════════════════════════ // GREENFIELD CONSTANTS // ═══════════════════════════════════════════════════════════════════════════════════ /** * Default indicators that signal a project is NOT greenfield * @constant {string[]} */ const DEFAULT_GREENFIELD_INDICATORS = [ 'package.json', '.git', 'docs/', 'src/', '.aios-core/', ]; /** * Greenfield workflow phases * @enum {string} */ const GreenfieldPhase = { DETECTION: 'detection', BOOTSTRAP: 'phase_0_bootstrap', DISCOVERY: 'phase_1_discovery', SHARDING: 'phase_2_sharding', DEV_CYCLE: 'phase_3_dev_cycle', COMPLETE: 'complete', }; /** * Phase 1 agent sequence (Discovery & Planning) * @constant {Array<{agent: string, task: string, creates: string}>} */ const PHASE_1_SEQUENCE = [ { agent: '@analyst', task: 'project-brief', creates: 'docs/project-brief.md' }, { agent: '@pm', task: 'create-prd', creates: 'docs/prd.md' }, { agent: '@ux-design-expert', task: 'front-end-spec', creates: 'docs/front-end-spec.md' }, { agent: '@architect', task: 'architecture', creates: 'docs/fullstack-architecture.md' }, { agent: '@po', task: 'validate-artifacts', creates: null }, ]; /** * Phase failure action * @enum {string} */ const PhaseFailureAction = { RETRY: 'retry', SKIP: 'skip', ABORT: 'abort', }; // ═══════════════════════════════════════════════════════════════════════════════════ // GREENFIELD HANDLER CLASS // ═══════════════════════════════════════════════════════════════════════════════════ /** * GreenfieldHandler - Orchestrates the complete Greenfield Workflow */ class GreenfieldHandler extends EventEmitter { /** * @param {string} projectRoot - Project root path * @param {Object} [options] - Configuration options * @param {boolean} [options.debug=false] - Enable debug logging * @param {Object} [options.workflowExecutor] - WorkflowExecutor instance * @param {Object} [options.surfaceChecker] - SurfaceChecker instance * @param {Object} [options.sessionState] - SessionState instance * @param {string[]} [options.indicators] - Custom greenfield indicators */ constructor(projectRoot, options = {}) { super(); if (!projectRoot || typeof projectRoot !== 'string') { throw new Error('projectRoot is required and must be a string'); } this.projectRoot = projectRoot; this.options = { debug: false, ...options, }; // Configurable indicators (AC1) this.indicators = options.indicators || DEFAULT_GREENFIELD_INDICATORS; // Lazy-loaded dependencies this._workflowExecutor = options.workflowExecutor || null; this._surfaceChecker = options.surfaceChecker || null; this._sessionState = options.sessionState || null; // Workflow path this.workflowPath = path.join( projectRoot, '.aios-core/development/workflows/greenfield-fullstack.yaml', ); // Phase progress tracking this.phaseProgress = {}; this._log('GreenfieldHandler initialized'); } // ═══════════════════════════════════════════════════════════════════════════════════ // LAZY DEPENDENCY LOADING // ═══════════════════════════════════════════════════════════════════════════════════ /** @private */ _getWorkflowExecutor() { if (!this._workflowExecutor) { try { const { WorkflowExecutor } = require('./workflow-executor'); this._workflowExecutor = new WorkflowExecutor(this.projectRoot, { debug: this.options.debug, }); } catch (error) { this._log(`WorkflowExecutor not available: ${error.message}`, 'warn'); } } return this._workflowExecutor; } /** @private */ _getSurfaceChecker() { if (!this._surfaceChecker) { try { const { SurfaceChecker } = require('./surface-checker'); this._surfaceChecker = new SurfaceChecker(); } catch (error) { this._log(`SurfaceChecker not available: ${error.message}`, 'warn'); } } return this._surfaceChecker; } /** @private */ _getSessionState() { if (!this._sessionState) { try { const { SessionState } = require('./session-state'); this._sessionState = new SessionState(this.projectRoot, { debug: this.options.debug, }); } catch (error) { this._log(`SessionState not available: ${error.message}`, 'warn'); } } return this._sessionState; } // ═══════════════════════════════════════════════════════════════════════════════════ // GREENFIELD DETECTION (AC1, AC16) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Detects if a project path is a greenfield project (AC1) * * Greenfield = NONE of the configured indicators exist. * * @param {string} [projectPath] - Path to check (defaults to projectRoot) * @returns {boolean} True if greenfield */ isGreenfield(projectPath) { const targetPath = projectPath || this.projectRoot; return this.indicators.every((indicator) => { const fullPath = path.join(targetPath, indicator); return !fs.existsSync(fullPath); }); } /** * Checks if Phase 0 (Bootstrap) can be skipped (AC16) * * Phase 0 is unnecessary if project already has package.json + .git * * @param {string} [projectPath] - Path to check (defaults to projectRoot) * @returns {boolean} True if Phase 0 should be skipped */ shouldSkipBootstrap(projectPath) { const targetPath = projectPath || this.projectRoot; const hasPackageJson = fs.existsSync(path.join(targetPath, 'package.json')); const hasGit = fs.existsSync(path.join(targetPath, '.git')); return hasPackageJson && hasGit; } // ═══════════════════════════════════════════════════════════════════════════════════ // MAIN HANDLER (AC2-5) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Main entry point - handles greenfield workflow orchestration * * @param {Object} context - Execution context * @param {string} [context.userGoal] - User's project description * @param {number} [context.resumeFromPhase] - Phase to resume from (0-3) * @returns {Promise<Object>} Handler result */ async handle(context = {}) { this._log('Greenfield handler invoked'); // Check for resume (AC14) const resumePhase = context.resumeFromPhase; if (typeof resumePhase === 'number' && resumePhase >= 0) { return this._executeFromPhase(resumePhase, context); } // Determine starting phase const skipBootstrap = this.shouldSkipBootstrap(); const startPhase = skipBootstrap ? 1 : 0; if (skipBootstrap) { this._log('Phase 0 skipped: package.json + .git already exist (AC16)'); } // Start orchestration from the appropriate phase return this._executeFromPhase(startPhase, context); } /** * Executes workflow starting from a specific phase * * @param {number} phaseNumber - Phase to start from (0-3) * @param {Object} context - Execution context * @returns {Promise<Object>} Execution result * @private */ async _executeFromPhase(phaseNumber, context) { this._log(`Starting from Phase ${phaseNumber}`); // Record starting phase in session state (AC9) await this._recordPhase(this._getPhaseEnum(phaseNumber), context); switch (phaseNumber) { case 0: return this._executePhase0(context); case 1: return this._executePhase1(context); case 2: return this._executePhase2(context); case 3: return this._executePhase3(context); default: return { action: 'greenfield_error', error: `Invalid phase number: ${phaseNumber}`, }; } } // ═══════════════════════════════════════════════════════════════════════════════════ // PHASE 0: BOOTSTRAP (AC2) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Phase 0: Spawn @devops for environment bootstrap (AC2) * * @param {Object} context - Execution context * @returns {Promise<Object>} Phase result with surface prompt for Phase 1 * @private */ async _executePhase0(context) { this._log('Phase 0: Environment Bootstrap'); this.phaseProgress[GreenfieldPhase.BOOTSTRAP] = { status: 'in_progress', startTime: Date.now() }; this.emit('phaseStart', { phase: GreenfieldPhase.BOOTSTRAP, context }); // AC2: Spawn @devops for environment-bootstrap const spawnResult = await this._spawnAgent('@devops', 'environment-bootstrap', { instructions: 'Execute *environment-bootstrap to set up the development environment', creates: ['.aios/config.yaml', '.aios/environment-report.json', '.gitignore', 'README.md', 'package.json'], }); this.phaseProgress[GreenfieldPhase.BOOTSTRAP] = { status: spawnResult.success ? 'complete' : 'failed', endTime: Date.now(), result: spawnResult, }; this.emit('phaseComplete', { phase: GreenfieldPhase.BOOTSTRAP, result: spawnResult, context }); if (!spawnResult.success) { return this._handlePhaseFailure(GreenfieldPhase.BOOTSTRAP, spawnResult.error, context); } // AC11: Surface between Phase 0 → Phase 1 // Ask user to describe what they want to build return this._surfaceBetweenPhases(0, 1, { message: 'Ambiente configurado. Descreva o que quer construir.', promptType: 'text_input', context: { ...context, phase0Result: spawnResult }, }); } // ═══════════════════════════════════════════════════════════════════════════════════ // PHASE 1: DISCOVERY & PLANNING (AC3) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Phase 1: Spawn agents sequentially for Discovery & Planning (AC3) * * Sequence: @analyst@pm@ux-design-expert → @architect@po * * @param {Object} context - Execution context * @returns {Promise<Object>} Phase result with surface prompt for Phase 2 * @private */ async _executePhase1(context) { this._log('Phase 1: Discovery & Planning'); this.phaseProgress[GreenfieldPhase.DISCOVERY] = { status: 'in_progress', startTime: Date.now() }; this.emit('phaseStart', { phase: GreenfieldPhase.DISCOVERY, context }); const stepResults = []; // Execute each agent in sequence for (const step of PHASE_1_SEQUENCE) { this._log(`Phase 1 step: ${step.agent} → ${step.task}`); const spawnResult = await this._spawnAgent(step.agent, step.task, { instructions: `Execute ${step.task} for greenfield project`, creates: step.creates ? [step.creates] : [], previousResults: stepResults, }); stepResults.push({ agent: step.agent, task: step.task, creates: step.creates, success: spawnResult.success, result: spawnResult, }); // If a step fails, offer Retry/Skip/Abort (AC15) if (!spawnResult.success) { return this._handlePhaseFailure( `${GreenfieldPhase.DISCOVERY}:${step.agent}`, spawnResult.error || `Agent ${step.agent} failed on ${step.task}`, { ...context, failedStep: step, completedSteps: stepResults }, ); } // Check idempotency — if artifact already exists, it was updated not duplicated (AC17) if (step.creates) { this._checkAndLogIdempotency(step.creates); } } this.phaseProgress[GreenfieldPhase.DISCOVERY] = { status: 'complete', endTime: Date.now(), stepResults, }; this.emit('phaseComplete', { phase: GreenfieldPhase.DISCOVERY, result: stepResults, context }); // Build artifacts summary for surface prompt const artifactsSummary = this._buildArtifactsSummary(stepResults); // AC12: Surface between Phase 1 → Phase 2 // Show artifacts summary and ask GO/PAUSE return this._surfaceBetweenPhases(1, 2, { message: `Artefatos de planejamento criados:\n${artifactsSummary}\n\nDeseja continuar com o sharding dos documentos?`, promptType: 'go_pause', context: { ...context, phase1Results: stepResults }, }); } // ═══════════════════════════════════════════════════════════════════════════════════ // PHASE 2: DOCUMENT SHARDING (AC4) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Phase 2: Spawn @po for document sharding (AC4) * * @param {Object} context - Execution context * @returns {Promise<Object>} Phase result with surface prompt for Phase 3 * @private */ async _executePhase2(context) { this._log('Phase 2: Document Sharding'); this.phaseProgress[GreenfieldPhase.SHARDING] = { status: 'in_progress', startTime: Date.now() }; this.emit('phaseStart', { phase: GreenfieldPhase.SHARDING, context }); // AC4: Spawn @po for document sharding const spawnResult = await this._spawnAgent('@po', 'shard-documents', { instructions: 'Shard docs/prd.md and docs/fullstack-architecture.md into development-ready chunks', creates: ['docs/prd/', 'docs/architecture/'], requires: ['docs/prd.md', 'docs/front-end-spec.md', 'docs/fullstack-architecture.md'], }); this.phaseProgress[GreenfieldPhase.SHARDING] = { status: spawnResult.success ? 'complete' : 'failed', endTime: Date.now(), result: spawnResult, }; this.emit('phaseComplete', { phase: GreenfieldPhase.SHARDING, result: spawnResult, context }); if (!spawnResult.success) { return this._handlePhaseFailure(GreenfieldPhase.SHARDING, spawnResult.error, context); } // AC13: Surface between Phase 2 → Phase 3 // Show created stories and ask GO return this._surfaceBetweenPhases(2, 3, { message: 'Documentos fragmentados para desenvolvimento. Stories criadas. Deseja iniciar o ciclo de desenvolvimento?', promptType: 'go_pause', context: { ...context, phase2Result: spawnResult }, }); } // ═══════════════════════════════════════════════════════════════════════════════════ // PHASE 3: DEVELOPMENT CYCLE (AC5) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Phase 3: Transition to Development Cycle (AC5) * * Reuses WorkflowExecutor from Story 11.3 * * @param {Object} context - Execution context * @returns {Promise<Object>} Phase result * @private */ async _executePhase3(context) { this._log('Phase 3: Development Cycle'); this.phaseProgress[GreenfieldPhase.DEV_CYCLE] = { status: 'in_progress', startTime: Date.now() }; this.emit('phaseStart', { phase: GreenfieldPhase.DEV_CYCLE, context }); // AC5: Transition to Development Cycle via WorkflowExecutor const workflowExecutor = this._getWorkflowExecutor(); this.phaseProgress[GreenfieldPhase.DEV_CYCLE] = { status: 'complete', endTime: Date.now(), }; this.emit('phaseComplete', { phase: GreenfieldPhase.DEV_CYCLE, context }); return { action: 'greenfield_dev_cycle', phase: GreenfieldPhase.DEV_CYCLE, data: { message: 'Greenfield workflow completo! Entrando no ciclo de desenvolvimento.', nextStep: 'development_cycle', workflowExecutorAvailable: !!workflowExecutor, handoff: 'Use @sm → *create-story para iniciar o ciclo de stories', context, }, }; } // ═══════════════════════════════════════════════════════════════════════════════════ // AGENT SPAWNING (AC6, AC7) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Spawns an agent via TerminalSpawner (AC7) with ExecutorAssignment (AC6) * * @param {string} agent - Agent ID (e.g., '@devops') * @param {string} task - Task to execute * @param {Object} spawnContext - Spawn context * @returns {Promise<Object>} Spawn result * @private */ async _spawnAgent(agent, task, spawnContext = {}) { this._log(`Spawning ${agent} for ${task}`); this.emit('agentSpawn', { agent, task, context: spawnContext }); try { // Try TerminalSpawner first (AC7) const TerminalSpawner = require('./terminal-spawner'); if (TerminalSpawner.isSpawnerAvailable()) { const agentId = agent.replace('@', ''); const result = await TerminalSpawner.spawnAgent(agentId, task, { context: { instructions: spawnContext.instructions || `Execute ${task}`, creates: spawnContext.creates || [], requires: spawnContext.requires || [], metadata: spawnContext.previousResults || {}, }, timeout: 7200000, // 2 hours debug: this.options.debug, }); if (result.pid) { this.emit('terminalSpawn', { agent, pid: result.pid, task }); } return { success: result.success !== false, pid: result.pid, output: result.output, outputFile: result.outputFile, }; } // Fallback: Return instructions for manual execution this._log(`TerminalSpawner not available, returning manual instructions for ${agent}`); return { success: true, manual: true, instructions: `Spawn ${agent} manually and execute: *${task}`, }; } catch (error) { this._log(`Failed to spawn ${agent}: ${error.message}`, 'error'); return { success: false, error: error.message, }; } } // ═══════════════════════════════════════════════════════════════════════════════════ // SURFACE DECISIONS (AC8, AC11-14) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Creates a surface decision point between phases (AC8, AC11-14) * * @param {number} fromPhase - Phase just completed * @param {number} toPhase - Next phase to execute * @param {Object} surfaceConfig - Surface configuration * @param {string} surfaceConfig.message - Message to display * @param {string} surfaceConfig.promptType - 'text_input' | 'go_pause' * @param {Object} surfaceConfig.context - Context to carry forward * @returns {Object} Surface result * @private */ _surfaceBetweenPhases(fromPhase, toPhase, surfaceConfig) { const surfaceChecker = this._getSurfaceChecker(); // Build options based on prompt type const options = surfaceConfig.promptType === 'go_pause' ? ['GO', 'PAUSE'] : ['continue']; const surfaceResult = surfaceChecker ? surfaceChecker.shouldSurface({ valid_options_count: options.length, options_with_tradeoffs: surfaceConfig.message, }) : { should_surface: true }; return { action: 'greenfield_surface', phase: this._getPhaseEnum(fromPhase), nextPhase: toPhase, data: { message: surfaceConfig.message, promptType: surfaceConfig.promptType, options, surfaceResult, resumeFromPhase: toPhase, context: surfaceConfig.context, }, }; } /** * Handles user response to a surface decision * * @param {string} decision - User decision ('GO', 'PAUSE', or text input) * @param {number} nextPhase - Phase to execute if GO * @param {Object} context - Execution context * @returns {Promise<Object>} Next step result */ async handleSurfaceDecision(decision, nextPhase, context = {}) { this._log(`Surface decision: ${decision}, next phase: ${nextPhase}`); const normalizedDecision = decision.toUpperCase().trim(); // AC14: PAUSE saves current state for resume if (normalizedDecision === 'PAUSE') { await this._savePhaseForResume(nextPhase, context); return { action: 'greenfield_paused', data: { message: `Workflow pausado. Resume da Phase ${nextPhase} quando quiser continuar.`, savedPhase: nextPhase, context, }, }; } // GO or text input: continue to next phase if (normalizedDecision === 'GO' || normalizedDecision === 'CONTINUE') { return this._executeFromPhase(nextPhase, context); } // Text input (likely project description from Phase 0 → 1 surface) return this._executeFromPhase(nextPhase, { ...context, userGoal: decision, }); } // ═══════════════════════════════════════════════════════════════════════════════════ // ERROR HANDLING (AC15) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Handles phase failure with Retry/Skip/Abort options (AC15) * * @param {string} phase - Failed phase identifier * @param {string} errorMessage - Error description * @param {Object} context - Execution context * @returns {Object} Failure result with options * @private */ _handlePhaseFailure(phase, errorMessage, context) { this._log(`Phase failed: ${phase} - ${errorMessage}`, 'error'); this.emit('phaseError', { phase, error: errorMessage, context }); return { action: 'greenfield_phase_failure', phase, error: errorMessage, options: [ { action: PhaseFailureAction.RETRY, label: '1. Tentar novamente' }, { action: PhaseFailureAction.SKIP, label: '2. Pular esta fase' }, { action: PhaseFailureAction.ABORT, label: '3. Cancelar workflow' }, ], context, }; } /** * Handles phase failure action from user (AC15) * * @param {string} phase - Failed phase * @param {string} action - User chosen action (retry/skip/abort) * @param {Object} context - Execution context * @returns {Promise<Object>} Next step result */ async handlePhaseFailureAction(phase, action, context = {}) { this._log(`Handling phase failure: action=${action}, phase=${phase}`); switch (action) { case PhaseFailureAction.RETRY: { const phaseNumber = this._getPhaseNumber(phase); if (phaseNumber >= 0) { return this._executeFromPhase(phaseNumber, context); } // Retry at the step level within Phase 1 if (context.failedStep) { return this._executePhase1(context); } return { action: 'retry_failed', error: `Cannot determine phase number for: ${phase}` }; } case PhaseFailureAction.SKIP: { this._log(`Skipping phase: ${phase}`); this.phaseProgress[phase] = { status: 'skipped' }; const nextPhase = this._getNextPhaseNumber(phase); if (nextPhase <= 3) { return this._executeFromPhase(nextPhase, context); } return { action: 'greenfield_complete', data: { message: 'Workflow completo (com fases puladas).' } }; } case PhaseFailureAction.ABORT: this._log(`Aborting workflow at phase: ${phase}`); return { action: 'greenfield_aborted', data: { message: 'Greenfield workflow cancelado.', lastPhase: phase, progress: this.phaseProgress, }, }; default: return { action: 'invalid_action', error: `Unknown action: ${action}` }; } } // ═══════════════════════════════════════════════════════════════════════════════════ // IDEMPOTENCY (AC17) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Checks if an artifact exists and logs idempotency info (AC17) * * @param {string} artifactPath - Relative path to artifact * @returns {Object} Idempotency info */ checkIdempotency(artifactPath) { const fullPath = path.join(this.projectRoot, artifactPath); const exists = fs.existsSync(fullPath); if (exists) { this._log(`Updating existing ${artifactPath} (idempotent re-run)`); return { exists: true, path: fullPath, action: 'update' }; } return { exists: false, path: fullPath, action: 'create' }; } /** * Logs idempotency check for an artifact * @param {string} artifactPath - Relative path * @private */ _checkAndLogIdempotency(artifactPath) { const result = this.checkIdempotency(artifactPath); if (result.exists) { this._log(`Idempotent: ${artifactPath} already exists, will be updated`); } } // ═══════════════════════════════════════════════════════════════════════════════════ // SESSION STATE (AC9, AC14) // ═══════════════════════════════════════════════════════════════════════════════════ /** * Records phase progress in session state (AC9) * * @param {string} phase - Current phase * @param {Object} context - Execution context * @private */ async _recordPhase(phase, context) { const sessionState = this._getSessionState(); if (!sessionState) { return; } try { const exists = await sessionState.exists(); if (exists) { await sessionState.loadSessionState(); await sessionState.recordPhaseChange(`greenfield_${phase}`, 'greenfield-fullstack', '@pm'); this._log(`Phase recorded in session state: ${phase}`); } } catch (error) { this._log(`Failed to record phase: ${error.message}`, 'warn'); } } /** * Saves current phase for resume (AC14) * * @param {number} phaseNumber - Phase number to resume from * @param {Object} context - Context to save * @private */ async _savePhaseForResume(phaseNumber, context) { const sessionState = this._getSessionState(); if (!sessionState) { return; } try { const exists = await sessionState.exists(); if (exists) { await sessionState.loadSessionState(); await sessionState.updateSessionState({ workflow: { current_phase: `greenfield_phase_${phaseNumber}`, greenfield_resume: { phaseNumber, savedAt: new Date().toISOString(), context, }, }, }); this._log(`Phase ${phaseNumber} saved for resume`); } } catch (error) { this._log(`Failed to save phase for resume: ${error.message}`, 'warn'); } } // ═══════════════════════════════════════════════════════════════════════════════════ // HELPERS // ═══════════════════════════════════════════════════════════════════════════════════ /** * Builds a summary of created artifacts * @param {Array} stepResults - Phase 1 step results * @returns {string} Formatted summary * @private */ _buildArtifactsSummary(stepResults) { return stepResults .filter((s) => s.creates && s.success) .map((s) => `- ${s.creates} (${s.agent})`) .join('\n'); } /** * Gets the GreenfieldPhase enum for a phase number * @param {number} phaseNumber - Phase number (0-3) * @returns {string} GreenfieldPhase value * @private */ _getPhaseEnum(phaseNumber) { const map = { 0: GreenfieldPhase.BOOTSTRAP, 1: GreenfieldPhase.DISCOVERY, 2: GreenfieldPhase.SHARDING, 3: GreenfieldPhase.DEV_CYCLE, }; return map[phaseNumber] || GreenfieldPhase.DETECTION; } /** * Gets phase number from phase string * @param {string} phase - Phase string * @returns {number} Phase number (-1 if not found) * @private */ _getPhaseNumber(phase) { if (phase.includes('phase_0') || phase.includes('bootstrap')) return 0; if (phase.includes('phase_1') || phase.includes('discovery')) return 1; if (phase.includes('phase_2') || phase.includes('sharding')) return 2; if (phase.includes('phase_3') || phase.includes('dev_cycle')) return 3; return -1; } /** * Gets the next phase number after a given phase * @param {string} phase - Current phase * @returns {number} Next phase number * @private */ _getNextPhaseNumber(phase) { const current = this._getPhaseNumber(phase); return current >= 0 ? current + 1 : 4; } /** * Debug logger * @param {string} message - Log message * @param {string} [level='info'] - Log level * @private */ _log(message, level = 'info') { if (this.options.debug || level === 'error' || level === 'warn') { const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : '🌱'; console.log(`[GreenfieldHandler] ${prefix} ${message}`); } } } // ═══════════════════════════════════════════════════════════════════════════════════ // EXPORTS // ═══════════════════════════════════════════════════════════════════════════════════ module.exports = { GreenfieldHandler, GreenfieldPhase, PhaseFailureAction, DEFAULT_GREENFIELD_INDICATORS, PHASE_1_SEQUENCE, };