Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions .aiox-core/core/config/config-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ const performanceMetrics = {
};

/**
* Checks if cache is still valid
* Checks if the configuration cache is still valid based on TTL
*
* @returns {boolean} True if cache exists and has not expired
*/
function isCacheValid() {
if (!configCache.lastLoad) return false;
Expand All @@ -89,7 +91,13 @@ function isCacheValid() {
}

/**
* Loads full config file (used for initial load or cache refresh)
* Loads the full core-config.yaml file from disk
*
* Used for initial load or cache refresh. The result is cached internally
* and subsequent calls within the TTL window return the cached version.
*
* @returns {Promise<Object>} Parsed configuration object
* @throws {Error} If the config file cannot be read or parsed
*/
async function loadFullConfig() {
const configPath = path.join('.aiox-core', 'core-config.yaml');
Expand Down Expand Up @@ -121,8 +129,13 @@ async function loadFullConfig() {
/**
* Loads specific config sections on demand
*
* If the cache is valid, returns sections from cache. Otherwise performs
* a full config reload and extracts the requested sections.
* Sections that do not exist in the config are silently omitted.
*
* @param {string[]} sections - Array of section names to load
* @returns {Promise<Object>} Config object with requested sections
* @returns {Promise<Object>} Config object containing only the requested sections
* @throws {Error} If config file cannot be read or parsed (on cache miss)
*/
async function loadConfigSections(sections) {
const startTime = Date.now();
Expand Down Expand Up @@ -160,10 +173,18 @@ async function loadConfigSections(sections) {
}

/**
* Loads config for specific agent with lazy loading
* Loads config for a specific agent with lazy loading
*
* Only loads the configuration sections required by the given agent,
* as defined in the agentRequirements mapping. Falls back to
* ALWAYS_LOADED sections for unknown agent IDs.
*
* @param {string} agentId - Agent ID (e.g., 'dev', 'qa', 'po')
* @returns {Promise<Object>} Config object with sections needed by agent
* @param {string} agentId - Agent ID (e.g., 'dev', 'qa', 'po', 'architect')
* @returns {Promise<Object>} Config object with only the sections needed by the agent
* @throws {Error} If config file cannot be read or parsed
* @example
* const config = await loadAgentConfig('dev');
* // Returns { frameworkDocsLocation, projectDocsLocation, toolConfigurations, ... }
*/
async function loadAgentConfig(agentId) {
const startTime = Date.now();
Expand All @@ -186,16 +207,25 @@ async function loadAgentConfig(agentId) {
}

/**
* Loads always-loaded sections (minimal config)
* Loads only the always-loaded (minimal) configuration sections
*
* Returns the lightweight subset of config needed by all agents:
* frameworkDocsLocation, projectDocsLocation, devLoadAlwaysFiles, and lazyLoading.
*
* @returns {Promise<Object>} Minimal config with always-loaded sections
* @throws {Error} If config file cannot be read or parsed
*/
async function loadMinimalConfig() {
return await loadConfigSections(ALWAYS_LOADED);
}

/**
* Preloads config into cache (useful for startup optimization)
* Preloads the full configuration into cache for startup optimization
*
* Call this during application initialization to avoid cold cache
* misses on the first config access.
*
* @returns {Promise<void>}
*/
async function preloadConfig() {
console.log('🔄 Preloading config into cache...');
Expand All @@ -204,7 +234,12 @@ async function preloadConfig() {
}

/**
* Clears config cache (useful for testing or forcing reload)
* Clears the entire config cache, forcing a full reload on next access
*
* Useful for testing scenarios or when the config file has been
* modified externally and a fresh load is required.
*
* @returns {void}
*/
function clearCache() {
configCache.full = null;
Expand All @@ -229,10 +264,15 @@ function getPerformanceMetrics() {
}

/**
* Validates that required sections exist in config
* Validates that all required config sections exist for a given agent
*
* Loads the full config and checks that every section listed in the
* agent's requirements is present. Returns a detailed validation result
* including any missing sections.
*
* @param {string} agentId - Agent ID to validate
* @returns {Promise<Object>} Validation result
* @param {string} agentId - Agent ID to validate (e.g., 'dev', 'qa')
* @returns {Promise<{valid: boolean, agentId: string, requiredSections: string[], missingSections: string[], availableSections: string[]}>} Validation result
* @throws {Error} If config file cannot be read or parsed
*/
async function validateAgentConfig(agentId) {
const requiredSections = agentRequirements[agentId] || ALWAYS_LOADED;
Expand All @@ -253,10 +293,15 @@ async function validateAgentConfig(agentId) {
}

/**
* Gets config section on demand (async lazy load)
* Gets a single config section on demand via async lazy load
*
* Leverages the caching layer so repeated calls for the same section
* within the TTL window are served from cache.
*
* @param {string} sectionName - Section to load
* @returns {Promise<any>} Section content
* @param {string} sectionName - Section name to load (e.g., 'toolConfigurations')
* @returns {Promise<*>} Section content, or undefined if the section does not exist
* @example
* const tools = await getConfigSection('toolConfigurations');
*/
async function getConfigSection(sectionName) {
const config = await loadConfigSections([sectionName]);
Expand Down
19 changes: 19 additions & 0 deletions .aiox-core/core/events/dashboard-emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,29 @@ const EMIT_TIMEOUT_MS = 500;

/**
* DashboardEmitter - Singleton for emitting events to monitor-server
*
* Sends high-level CLI events (agent activation, command execution,
* story status changes, session lifecycle) to the monitor-server via
* non-blocking HTTP POST requests with 500ms timeout.
*
* Falls back to writing events to a JSONL file when the monitor-server
* is unreachable. Automatically disabled in test environments.
*
* @example
* const emitter = DashboardEmitter.getInstance();
* emitter.setAgent('dev');
* await emitter.emitCommandStart('*develop', ['--story', '3.1']);
*/
class DashboardEmitter {
static instance = null;

/**
* Create a new DashboardEmitter instance
*
* Uses the CLAUDE_CODE_SESSION_ID environment variable if available,
* otherwise generates a random UUID for session tracking.
* Automatically disabled when NODE_ENV is 'test'.
*/
constructor() {
this.sessionId = process.env.CLAUDE_CODE_SESSION_ID || randomUUID();
this.projectRoot = process.cwd();
Expand Down
13 changes: 11 additions & 2 deletions .aiox-core/core/orchestration/gate-evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,12 @@ class GateEvaluator {
}

/**
* Clear all results
* Clear all stored gate results and logs
*
* Resets the evaluator to its initial state, removing all
* previously recorded gate evaluations and log entries.
*
* @returns {void}
*/
clear() {
this.results = [];
Expand All @@ -469,14 +474,18 @@ class GateEvaluator {
/**
* Log message
* @private
* @param {string} message - Log message
* @param {string} [level='info'] - Log level (info, warn, error)
*/
_log(message, level = 'info') {
const timestamp = new Date().toISOString();
this.logs.push({ timestamp, level, message });
}

/**
* Get logs
* Get all evaluation logs
*
* @returns {Array<{timestamp: string, level: string, message: string}>} Copy of all log entries
*/
getLogs() {
return [...this.logs];
Expand Down
64 changes: 55 additions & 9 deletions .aiox-core/core/orchestration/master-orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -934,13 +934,23 @@ class MasterOrchestrator extends EventEmitter {

/**
* Start dashboard monitoring (Story 0.8 - AC1)
*
* Begins periodic state updates to the dashboard integration layer.
* Must be called after initialization to enable real-time observability.
*
* @returns {Promise<void>}
*/
async startDashboard() {
await this.dashboardIntegration.start();
}

/**
* Stop dashboard monitoring (Story 0.8)
*
* Stops periodic dashboard updates and cleans up any pending timers.
* Safe to call multiple times.
*
* @returns {void}
*/
stopDashboard() {
this.dashboardIntegration.stop();
Expand Down Expand Up @@ -1060,8 +1070,12 @@ class MasterOrchestrator extends EventEmitter {

/**
* Save current state to disk (AC1, AC3)
* Called after each epic completion and state transition
* @returns {Promise<boolean>} Success status
*
* Persists the full orchestration state including epic progress,
* timestamps, tech stack profile, recovery tracking, errors, and insights.
* Called automatically after each epic completion and state transition.
*
* @returns {Promise<boolean>} True if state was saved successfully, false on failure
*/
async saveState() {
try {
Expand Down Expand Up @@ -1287,8 +1301,12 @@ class MasterOrchestrator extends EventEmitter {
}

/**
* Clear saved state for current story
* @returns {Promise<boolean>} Success status
* Clear saved state for the current story
*
* Removes the persisted state file from disk, allowing a fresh
* orchestration run without resuming from previous progress.
*
* @returns {Promise<boolean>} True if state file was deleted, false if not found or on error
*/
async clearState() {
try {
Expand All @@ -1304,8 +1322,12 @@ class MasterOrchestrator extends EventEmitter {
}

/**
* List all saved states
* @returns {Promise<Array>} List of state summaries
* List all saved orchestration states
*
* Scans the master-orchestrator state directory and returns a summary
* for each state file, sorted by most recently updated first.
*
* @returns {Promise<Array<{storyId: string, workflowId: string, status: string, progress: number, updatedAt: string, resumable: boolean}>>} List of state summaries
*/
async listSavedStates() {
const stateDir = path.join(this.projectRoot, '.aiox', 'master-orchestrator');
Expand Down Expand Up @@ -1369,8 +1391,17 @@ class MasterOrchestrator extends EventEmitter {

/**
* Finalize pipeline execution and generate summary
* @param {Object} pipelineResult - Raw pipeline result
* @returns {Object} Finalized result
*
* Produces a comprehensive result object with human-readable duration,
* tech stack summary, epic execution details, and accumulated insights.
*
* @param {Object} [pipelineResult={}] - Raw pipeline result from executeFullPipeline
* @param {boolean} [pipelineResult.success] - Whether the pipeline succeeded
* @param {number[]} [pipelineResult.epicsExecuted] - Epic numbers that executed
* @param {number[]} [pipelineResult.epicsFailed] - Epic numbers that failed
* @param {number[]} [pipelineResult.epicsSkipped] - Epic numbers that were skipped
* @param {number} [pipelineResult.duration] - Duration in milliseconds
* @returns {Object} Finalized result with workflowId, status, duration, and epic breakdown
*/
finalize(pipelineResult = {}) {
const duration =
Expand Down Expand Up @@ -1508,15 +1539,30 @@ class MasterOrchestrator extends EventEmitter {

/**
* Stub Epic Executor - placeholder for Story 0.3
* Real executors will be implemented in Story 0.3
*
* Provides a no-op executor for epics that do not yet have a real
* implementation. Returns a minimal success result so the pipeline
* can continue execution during development.
*/
class StubEpicExecutor {
/**
* Create a new StubEpicExecutor
*
* @param {MasterOrchestrator} orchestrator - Parent orchestrator instance
* @param {number} epicNum - Epic number this executor handles
*/
constructor(orchestrator, epicNum) {
this.orchestrator = orchestrator;
this.epicNum = epicNum;
this.config = EPIC_CONFIG[epicNum];
}

/**
* Execute the stub epic (no-op placeholder)
*
* @param {Object} _context - Execution context (ignored by stub)
* @returns {Promise<Object>} Minimal result with status 'stub'
*/
async execute(_context) {
console.log(chalk.yellow(` ⚠️ Using stub executor for Epic ${this.epicNum}`));
console.log(chalk.gray(` Real executor (${this.config.executor}) not yet implemented`));
Expand Down
Loading
Loading