UNPKG

aios-core

Version:

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

816 lines (744 loc) 30.1 kB
// SYN-14: Boot time captured before ANY require — measures cold start const _BOOT_TIME = process.hrtime.bigint(); /** * Unified Activation Pipeline - Single Entry Point for All 12 Agents * * Story ACT-6: Eliminates divergence between Path A (9 agents) and Path B (3 agents) * by providing a single activation pipeline with identical context richness for ALL agents. * * Story ACT-11: Pipeline Performance Optimization & Loader Prioritization * - Tiered loading: Critical > High > Best-effort (instead of flat Promise.all) * - Per-loader profiling via _profileLoader() with context.metrics output * - Partial greeting support (between full and fallback) * - Configurable timeout budgets via core-config.yaml and env vars * - CoreConfig shared with GreetingBuilder to eliminate double read * * Story ACT-12: Native Language Delegation * - Language handling delegated to Claude Code's native `language` setting in settings.json * - Removed language extraction/propagation from pipeline * - FALLBACK_PHRASES simplified to single English FALLBACK_PHRASE constant * * Architecture (ACT-11 Tiered): * Phase 0: Load CoreConfig (shared, fast) * Phase 1 (Critical, 80ms): AgentConfigLoader — greeting is broken without this * Phase 2 (High, parallel with remaining budget): PermissionMode + GitConfigDetector * Phase 3 (Best-effort, parallel with remaining budget): SessionContext + ProjectStatus * Sequential: GreetingPreferenceManager, ContextDetector, WorkflowNavigator * Final: GreetingBuilder.buildGreeting(enrichedContext) * * Performance Targets (ACT-11): * - Pipeline p50 (warm): <150ms * - Pipeline p95 (cold): <250ms * - Fallback rate: <5% * * @module development/scripts/unified-activation-pipeline * @see greeting-builder.js - Core greeting class * @see generate-greeting.js - CLI wrapper (now thin wrapper around this pipeline) */ 'use strict'; const path = require('path'); const fs = require('fs').promises; const fsSync = require('fs'); const yaml = require('js-yaml'); const GreetingBuilder = require('./greeting-builder'); const { AgentConfigLoader } = require('./agent-config-loader'); const SessionContextLoader = require('../../core/session/context-loader'); // NOG-18: loadProjectStatus removed — gitStatus is native in Claude Code system prompt. // const { loadProjectStatus } = require('../../infrastructure/scripts/project-status-loader'); const GitConfigDetector = require('../../infrastructure/scripts/git-config-detector'); const { PermissionMode } = require('../../core/permissions'); const GreetingPreferenceManager = require('./greeting-preference-manager'); const ContextDetector = require('../../core/session/context-detector'); const WorkflowNavigator = require('./workflow-navigator'); const { atomicWriteSync } = require('../../core/synapse/utils/atomic-write'); // BUG-1 fix (INS-1): Graceful degradation when pro-detector is not available // In installed projects, bin/utils/pro-detector.js does not exist let isProAvailable, loadProModule; try { ({ isProAvailable, loadProModule } = require('../../../bin/utils/pro-detector')); } catch { isProAvailable = () => false; loadProModule = () => null; } /** * ACT-11: Loader importance tiers with per-tier timeout budgets. * Tier 1 (Critical) must complete for a meaningful greeting. * Tier 2 (High) adds important visual elements (badge, branch). * Tier 3 (Best-effort) adds optional context (status, session). * @type {Object} */ const LOADER_TIERS = { critical: { loaders: ['agentConfig'], timeout: 80, description: 'Agent identity — greeting is broken without this', }, high: { loaders: ['permissionMode', 'gitConfig'], timeout: 120, description: 'Permission badge + branch name — visually degraded without these', }, bestEffort: { loaders: ['sessionContext'], timeout: 180, description: 'Session awareness — greeting works fine without this. NOG-18: projectStatus removed (native gitStatus)', }, }; /** * Default total pipeline timeout (ms). * Can be overridden via core-config.yaml pipeline.timeout_ms or AIOS_PIPELINE_TIMEOUT env var. * @type {number} */ const DEFAULT_PIPELINE_TIMEOUT_MS = 500; /** * All 12 supported agent IDs. * @type {string[]} */ const ALL_AGENT_IDS = [ 'dev', 'qa', 'architect', 'pm', 'po', 'sm', 'analyst', 'data-engineer', 'ux-design-expert', 'devops', 'aios-master', 'squad-creator', ]; /** * ACT-12: Fallback phrase for minimal greeting (English-only safety net). * Language handling is delegated to Claude Code's native `language` setting in settings.json. * @type {string} */ const FALLBACK_PHRASE = 'Type `*help` to see available commands.'; class UnifiedActivationPipeline { constructor(options = {}) { this.projectRoot = options.projectRoot || process.cwd(); this.greetingBuilder = options.greetingBuilder || new GreetingBuilder(); this.preferenceManager = options.preferenceManager || new GreetingPreferenceManager(); this.contextDetector = options.contextDetector || new ContextDetector(); this.workflowNavigator = options.workflowNavigator || new WorkflowNavigator(); this.gitConfigDetector = options.gitConfigDetector || new GitConfigDetector(); } /** * Static convenience method — creates instance and activates. * Allows both `UnifiedActivationPipeline.activate('dev')` and * `new UnifiedActivationPipeline().activate('dev')`. * * @param {string} agentId - Agent identifier (e.g., 'dev', 'qa', 'pm') * @param {Object} [options] - Activation options * @returns {Promise<{greeting: string, context: Object, duration: number, quality: string, metrics: Object}>} */ static activate(agentId, options = {}) { return new UnifiedActivationPipeline().activate(agentId, options); } /** * Activate an agent through the unified pipeline. * * ACT-11: Uses tiered loading with graceful degradation. * Returns 'full', 'partial', or 'fallback' quality level instead of boolean. * * @param {string} agentId - Agent identifier (e.g., 'dev', 'qa', 'pm') * @param {Object} [options] - Activation options * @param {Array} [options.conversationHistory] - Conversation history for context detection * @returns {Promise<{greeting: string, context: Object, duration: number, quality: string, metrics: Object}>} * greeting - Formatted greeting string ready for display * context - The enriched context object assembled by the pipeline * duration - Total activation time in ms * quality - 'full' | 'partial' | 'fallback' * metrics - Loader timing data */ async activate(agentId, options = {}) { const startTime = Date.now(); try { // ACT-11: Load config early for timeout settings const coreConfig = await this._loadCoreConfig(); const pipelineTimeout = this._resolvePipelineTimeout(coreConfig); // Race: full pipeline vs timeout (clear timer to prevent leak) const { promise: timeoutPromise, timerId } = this._timeoutFallback(agentId, pipelineTimeout); const result = await Promise.race([ this._runPipeline(agentId, options, coreConfig, startTime), timeoutPromise, ]); clearTimeout(timerId); result.duration = Date.now() - startTime; return result; } catch (error) { console.warn(`[UnifiedActivationPipeline] Activation failed for ${agentId}:`, error.message); const fallbackGreeting = this._generateFallbackGreeting(agentId); return { greeting: fallbackGreeting, context: this._getDefaultContext(agentId), duration: Date.now() - startTime, quality: 'fallback', // ACT-11: backward compat — keep fallback field fallback: true, metrics: { loaders: {} }, }; } } /** * ACT-11: Run the tiered activation pipeline. * * Instead of flat Promise.all() for all 5 loaders, uses tiered execution: * - Tier 1 (Critical): AgentConfigLoader must complete * - Tier 2 (High): PermissionMode + GitConfigDetector, best-effort * - Tier 3 (Best-effort): SessionContext + ProjectStatus, optional * * After each tier, remaining budget is checked. Slow lower-tier loaders * cannot prevent the greeting from being built with available context. * * @private * @param {string} agentId - Agent identifier * @param {Object} options - Activation options * @param {Object} coreConfig - Pre-loaded core config (shared, not read again) * @returns {Promise<{greeting: string, context: Object, quality: string, metrics: Object}>} */ async _runPipeline(agentId, options = {}, coreConfig = {}, startTime = Date.now()) { const pipelineStart = Date.now(); const metrics = { loaders: {} }; // --- Tier 1: Critical (AgentConfig) --- const tier1Budget = LOADER_TIERS.critical.timeout; const agentComplete = await this._profileLoader('agentConfig', metrics, tier1Budget, () => { const loader = new AgentConfigLoader(agentId); return loader.loadComplete(coreConfig); }); // If Tier 1 failed, we can still build a minimal greeting but mark as fallback const agentDefinition = this._buildAgentDefinition(agentId, agentComplete); if (!agentComplete) { // Tier 1 failure: return fallback greeting const greeting = this._generateFallbackGreeting(agentId); return { greeting, context: this._getDefaultContext(agentId), quality: 'fallback', fallback: true, metrics, }; } // --- Tier 2: High (PermissionMode + GitConfig) — parallel --- const tier2Budget = LOADER_TIERS.high.timeout; const elapsedAfterT1 = Date.now() - pipelineStart; const tier2Remaining = Math.max(tier2Budget - elapsedAfterT1, 20); const [permissionData, gitConfig] = await Promise.all([ this._profileLoader('permissionMode', metrics, tier2Remaining, async () => { const mode = new PermissionMode(this.projectRoot); await mode.load(); return { mode: mode.currentMode, badge: mode.getBadge() }; }), this._profileLoader('gitConfig', metrics, tier2Remaining, () => { return this.gitConfigDetector.get(); }), ]); // --- Memory Loader (MIS-6): Load agent memories if pro available --- let memories = []; try { if (isProAvailable()) { const MemoryLoader = loadProModule('memory/memory-loader'); if (MemoryLoader) { // Check feature gate for memory.extended const featureGate = loadProModule('license/feature-gate'); const isMemoryEnabled = featureGate?.featureGate?.isAvailable('pro.memory.extended') ?? false; if (isMemoryEnabled) { const memoryBudget = agentComplete?.config?.memoryBudget || 2000; const memoryTimeout = 500; // 500ms timeout for memory load memories = await this._profileLoader('memories', metrics, memoryTimeout, async () => { const loader = new MemoryLoader(this.projectRoot); const result = await loader.loadForAgent(agentId, { budget: memoryBudget }); return result?.memories || []; }); } } } } catch (error) { // Graceful degradation: log error but continue with empty memories if (metrics?.loaders?.memories) { metrics.loaders.memories.error = error.message; } memories = []; } // --- Tier 3: Best-effort (SessionContext + ProjectStatus) — parallel --- const tier3Budget = LOADER_TIERS.bestEffort.timeout; const elapsedAfterT2 = Date.now() - pipelineStart; const tier3Remaining = Math.max(tier3Budget - elapsedAfterT2, 20); // NOG-18: projectStatus loader removed — gitStatus is native in Claude Code system prompt. // The loadProjectStatus() function ran 5+ git commands (~76ms) duplicating native features. // GreetingBuilder handles projectStatus: null gracefully (null-checks everywhere). const [sessionContext] = await Promise.all([ this._profileLoader('sessionContext', metrics, tier3Remaining, () => { const loader = new SessionContextLoader(); return loader.loadContext(agentId); }), ]); const projectStatus = null; // --- Sequential steps with data dependencies --- // Step 6: Greeting preference (sync, fast) // ACT-11: Share coreConfig with GreetingBuilder to avoid double resolveConfig() const userProfile = this.greetingBuilder.loadUserProfile(coreConfig); const preference = this._resolvePreference(agentDefinition, userProfile); // Step 7: Session type detection const sessionType = this._detectSessionType(sessionContext, options); // Step 8: Workflow state detection const workflowState = this._detectWorkflowState(sessionContext, sessionType); // --- Assemble enriched context --- const enrichedContext = { agent: agentDefinition, config: agentComplete?.config || {}, session: sessionContext || this._getDefaultSessionContext(), projectStatus: projectStatus || null, gitConfig: gitConfig || { configured: false, type: null, branch: null }, permissions: permissionData || { mode: 'ask', badge: '[Ask]' }, preference, sessionType, workflowState, userProfile, // MIS-6: Agent memories from progressive retrieval memories: memories || [], // ACT-11: Share coreConfig with GreetingBuilder to eliminate double resolveConfig() _coreConfig: coreConfig, // Legacy context fields for backward compatibility with GreetingBuilder conversationHistory: options.conversationHistory || [], lastCommands: sessionContext?.lastCommands || [], previousAgent: sessionContext?.previousAgent || null, sessionMessage: sessionContext?.message || null, workflowActive: sessionContext?.workflowActive || null, sessionStory: sessionContext?.currentStory || null, }; // --- Build greeting via GreetingBuilder --- const greeting = await this.greetingBuilder.buildGreeting(agentDefinition, enrichedContext); // ACT-11: Determine quality level based on what loaded successfully const quality = this._determineQuality(metrics); // SYN-13: Write active agent to SYNAPSE session (fire-and-forget, 20ms budget) this._writeSynapseSession(agentId, quality, metrics); // SYN-14: Persist UAP metrics for diagnostics (fire-and-forget) this._persistUapMetrics(agentId, quality, metrics, Date.now() - startTime); return { greeting, context: enrichedContext, quality, // ACT-11: backward compat — fallback is false unless quality is 'fallback' fallback: quality === 'fallback', metrics, }; } /** * ACT-11: Profile a loader with timing and status tracking. * * Wraps a loader function with: * - Start/end/duration timing in milliseconds * - Status: 'ok' | 'timeout' | 'error' * - Per-loader timeout with graceful null return * * Results are recorded in metrics.loaders[name]. * * @private * @param {string} name - Loader name for metrics * @param {Object} metrics - Metrics object to populate * @param {number} timeoutMs - Timeout budget for this loader * @param {Function} loaderFn - Async function that performs the load * @returns {Promise<*>} Loaded data or null on failure/timeout */ async _profileLoader(name, metrics, timeoutMs, loaderFn) { const start = Date.now(); let timer; try { const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${name} timeout (${timeoutMs}ms)`)), timeoutMs); }); const result = await Promise.race([ loaderFn(), timeoutPromise, ]); clearTimeout(timer); const duration = Date.now() - start; metrics.loaders[name] = { duration, status: 'ok', start, end: start + duration }; return result; } catch (error) { clearTimeout(timer); const duration = Date.now() - start; const status = error.message.includes('timeout') ? 'timeout' : 'error'; metrics.loaders[name] = { duration, status, start, end: start + duration, error: error.message }; console.warn(`[UnifiedActivationPipeline] ${name} ${status}: ${error.message}`); return null; } } /** * ACT-11: Determine greeting quality based on loader results. * * - 'full': All loaders succeeded (or at least Tier 1 + Tier 2) * - 'partial': Tier 1 succeeded but some Tier 2/3 loaders failed * - 'fallback': Tier 1 (agentConfig) failed * * @private * @param {Object} metrics - Metrics object with loader results * @returns {string} 'full' | 'partial' | 'fallback' */ _determineQuality(metrics) { const loaders = metrics.loaders; // Tier 1 failure = fallback if (!loaders.agentConfig || loaders.agentConfig.status !== 'ok') { return 'fallback'; } // Check if any loader failed const allLoaderNames = Object.keys(loaders); const failedLoaders = allLoaderNames.filter(name => loaders[name].status !== 'ok'); if (failedLoaders.length === 0) { return 'full'; } return 'partial'; } /** * Load core configuration from YAML. * ACT-11: This is read once and shared with GreetingBuilder and all loaders. * @private * @returns {Promise<Object>} Core config object */ async _loadCoreConfig() { try { const configPath = path.join(this.projectRoot, '.aios-core', 'core-config.yaml'); const content = await fs.readFile(configPath, 'utf8'); return yaml.load(content); } catch (error) { console.warn('[UnifiedActivationPipeline] Failed to load core config:', error.message); return {}; } } /** * ACT-11: Resolve pipeline timeout from config hierarchy. * Priority: AIOS_PIPELINE_TIMEOUT env > core-config.yaml pipeline.timeout_ms > default * @private * @param {Object} coreConfig - Core config object * @returns {number} Pipeline timeout in ms */ _resolvePipelineTimeout(coreConfig) { // Env var override (for CI/testing) const envTimeout = process.env.AIOS_PIPELINE_TIMEOUT; if (envTimeout) { const parsed = parseInt(envTimeout, 10); if (!isNaN(parsed) && parsed > 0) { return parsed; } } // Config override const configTimeout = coreConfig?.pipeline?.timeout_ms; if (configTimeout && typeof configTimeout === 'number' && configTimeout > 0) { return configTimeout; } return DEFAULT_PIPELINE_TIMEOUT_MS; } /** * Build agent definition from loaded config data. * @private * @param {string} agentId - Agent ID * @param {Object|null} agentComplete - Data from AgentConfigLoader.loadComplete() * @returns {Object} Agent definition object suitable for GreetingBuilder */ _buildAgentDefinition(agentId, agentComplete) { if (agentComplete && agentComplete.agent) { return { ...agentComplete.agent, id: agentComplete.agent.id || agentId, persona_profile: agentComplete.persona_profile || agentComplete.definition?.persona_profile, persona: agentComplete.definition?.persona || agentComplete.persona, commands: agentComplete.commands || agentComplete.definition?.commands || [], }; } // Fallback: minimal agent definition return { id: agentId, name: agentId, icon: this._getDefaultIcon(agentId), persona_profile: { greeting_levels: { minimal: `${this._getDefaultIcon(agentId)} ${agentId} Agent ready`, named: `${this._getDefaultIcon(agentId)} ${agentId} ready`, archetypal: `${this._getDefaultIcon(agentId)} ${agentId} ready`, }, }, persona: { role: agentId }, commands: [], }; } /** * Resolve greeting preference, accounting for bob mode and PM agent. * @private * @param {Object} agentDefinition - Agent definition * @param {string} userProfile - User profile ('bob' | 'advanced') * @returns {string} Effective preference */ _resolvePreference(agentDefinition, userProfile) { // PM agent bypasses bob mode restriction (PM is primary interface in bob mode) const effectiveProfile = (userProfile === 'bob' && agentDefinition.id === 'pm') ? 'advanced' : userProfile; return this.preferenceManager.getPreference(effectiveProfile); } /** * Detect session type from session context. * @private * @param {Object|null} sessionContext - Session context data * @param {Object} options - Activation options * @returns {string} 'new' | 'existing' | 'workflow' */ _detectSessionType(sessionContext, options) { try { // If conversation history provided, prefer that if (options.conversationHistory && options.conversationHistory.length > 0) { return this.contextDetector.detectSessionType(options.conversationHistory); } // Use pre-detected session type from SessionContextLoader if (sessionContext && sessionContext.sessionType) { return sessionContext.sessionType; } // Fallback to file-based detection return this.contextDetector.detectSessionType([]); } catch (error) { console.warn('[UnifiedActivationPipeline] Session type detection failed:', error.message); return 'new'; } } /** * Detect workflow state from session context and session type. * Story ACT-5: Relaxed trigger - now detects workflows for any non-new session. * @private * @param {Object|null} sessionContext - Session context data * @param {string} sessionType - Detected session type * @returns {Object|null} Workflow state or null */ _detectWorkflowState(sessionContext, sessionType) { try { if (sessionType === 'new' || !sessionContext) { return null; } const commandHistory = sessionContext.lastCommands || []; if (commandHistory.length === 0) { return null; } return this.workflowNavigator.detectWorkflowState(commandHistory, sessionContext); } catch (error) { console.warn('[UnifiedActivationPipeline] Workflow detection failed:', error.message); return null; } } /** * Create a timeout promise that resolves with a fallback greeting. * ACT-12: Language delegated to Claude Code native settings.json. * @private * @param {string} agentId - Agent ID * @param {number} timeoutMs - Timeout in milliseconds * @returns {{promise: Promise, timerId: NodeJS.Timeout}} Promise and timer ID */ _timeoutFallback(agentId, timeoutMs) { let timerId; const promise = new Promise((resolve) => { timerId = setTimeout(() => { console.warn(`[UnifiedActivationPipeline] Pipeline timeout (${timeoutMs}ms) for ${agentId}`); resolve({ greeting: this._generateFallbackGreeting(agentId), context: this._getDefaultContext(agentId), quality: 'fallback', fallback: true, metrics: { loaders: {} }, }); }, timeoutMs); }); return { promise, timerId }; } /** * Generate fallback greeting when pipeline fails. * ACT-12: Language delegated to Claude Code native settings.json. * Fallback is English-only safety net — Claude Code translates natively. * @private * @param {string} agentId - Agent ID * @returns {string} Simple fallback greeting */ _generateFallbackGreeting(agentId) { const icon = this._getDefaultIcon(agentId); return `${icon} ${agentId} Agent ready\n\n${FALLBACK_PHRASE}`; } /** * Get default icon for agent. * @private * @param {string} agentId - Agent ID * @returns {string} Default icon emoji */ _getDefaultIcon(agentId) { const icons = { 'dev': '\uD83D\uDCBB', 'qa': '\uD83D\uDD0D', 'architect': '\uD83C\uDFD7\uFE0F', 'pm': '\uD83D\uDCCA', 'po': '\uD83D\uDCCB', 'sm': '\uD83C\uDFC3', 'analyst': '\uD83D\uDD2C', 'data-engineer': '\uD83D\uDDC4\uFE0F', 'ux-design-expert': '\uD83C\uDFA8', 'devops': '\u2699\uFE0F', 'aios-master': '\uD83D\uDC51', 'squad-creator': '\uD83D\uDC65', }; return icons[agentId] || '\uD83E\uDD16'; } /** * Get default session context when loader fails. * @private * @returns {Object} Default session context */ _getDefaultSessionContext() { return { sessionType: 'new', message: null, previousAgent: null, lastCommands: [], workflowActive: null, currentStory: null, }; } /** * Get default enriched context when pipeline fails. * @private * @param {string} agentId - Agent ID * @returns {Object} Default context */ _getDefaultContext(agentId) { return { agent: { id: agentId, name: agentId, icon: this._getDefaultIcon(agentId) }, config: {}, session: this._getDefaultSessionContext(), projectStatus: null, gitConfig: { configured: false, type: null, branch: null }, permissions: { mode: 'ask', badge: '[Ask]' }, preference: 'auto', sessionType: 'new', workflowState: null, userProfile: 'advanced', // MIS-6: Include memories field in fallback context memories: [], conversationHistory: [], lastCommands: [], previousAgent: null, sessionMessage: null, workflowActive: null, sessionStory: null, }; } /** * SYN-13: Write active agent to SYNAPSE session bridge file. * * Writes `.synapse/sessions/_active-agent.json` as a singleton file. * Uses fs.writeFileSync directly (not updateSession) to avoid prompt_count * side effects. Fire-and-forget with try/catch — never blocks activation. * * @private * @param {string} agentId - Agent ID being activated * @param {string} quality - Activation quality ('full'|'partial'|'fallback') * @param {Object} metrics - Metrics object for profiling */ _writeSynapseSession(agentId, quality, metrics) { const start = Date.now(); try { const sessionsDir = path.join(this.projectRoot, '.synapse', 'sessions'); if (!fsSync.existsSync(path.join(this.projectRoot, '.synapse'))) { // .synapse/ does not exist — project may not have SYNAPSE installed const duration = Date.now() - start; metrics.loaders.synapseSession = { duration, status: 'skipped', start, end: start + duration }; return; } if (!fsSync.existsSync(sessionsDir)) { fsSync.mkdirSync(sessionsDir, { recursive: true }); } const bridgeData = { id: agentId, activated_at: new Date().toISOString(), activation_quality: quality, source: 'uap', }; const bridgePath = path.join(sessionsDir, '_active-agent.json'); atomicWriteSync(bridgePath, JSON.stringify(bridgeData, null, 2)); const duration = Date.now() - start; metrics.loaders.synapseSession = { duration, status: 'ok', start, end: start + duration }; } catch (error) { const duration = Date.now() - start; metrics.loaders.synapseSession = { duration, status: 'error', start, end: start + duration, error: error.message }; console.warn(`[UnifiedActivationPipeline] SYNAPSE session write failed: ${error.message}`); } } /** * SYN-14: Persist UAP metrics to .synapse/metrics/uap-metrics.json. * Fire-and-forget — never blocks activation pipeline. * * @private * @param {string} agentId - Agent ID * @param {string} quality - Activation quality ('full'|'partial'|'fallback') * @param {Object} metrics - Metrics object with loader timings * @param {number} totalDuration - Total activation duration in ms */ _persistUapMetrics(agentId, quality, metrics, totalDuration) { try { const synapsePath = path.join(this.projectRoot, '.synapse'); if (!fsSync.existsSync(synapsePath)) return; const metricsDir = path.join(synapsePath, 'metrics'); if (!fsSync.existsSync(metricsDir)) { fsSync.mkdirSync(metricsDir, { recursive: true }); } const requireChainMs = typeof _BOOT_TIME !== 'undefined' ? Number(process.hrtime.bigint() - _BOOT_TIME) / 1e6 : 0; const data = { agentId, quality, totalDuration, requireChainMs, loaders: {}, timestamp: new Date().toISOString(), }; for (const [name, info] of Object.entries(metrics.loaders || {})) { data.loaders[name] = { duration: info.duration || 0, status: info.status || 'unknown', }; } atomicWriteSync( path.join(metricsDir, 'uap-metrics.json'), JSON.stringify(data, null, 2), ); } catch { // Fire-and-forget: never block the activation pipeline } } /** * Get list of all supported agent IDs. * @returns {string[]} Array of agent IDs */ static getAllAgentIds() { return [...ALL_AGENT_IDS]; } /** * Validate that an agent ID is supported. * @param {string} agentId - Agent ID to validate * @returns {boolean} True if valid */ static isValidAgentId(agentId) { return ALL_AGENT_IDS.includes(agentId); } } module.exports = { UnifiedActivationPipeline, ALL_AGENT_IDS, // ACT-11: Export for testing LOADER_TIERS, DEFAULT_PIPELINE_TIMEOUT_MS, // ACT-12: Single English fallback (language delegated to Claude Code settings.json) FALLBACK_PHRASE, }; // CLI entrypoint: `node unified-activation-pipeline.js <agentId>` if (require.main === module) { const agentId = process.argv[2]; if (!agentId || !ALL_AGENT_IDS.includes(agentId)) { console.error(`Usage: node unified-activation-pipeline.js <agentId>\nValid agents: ${ALL_AGENT_IDS.join(', ')}`); process.exit(1); } UnifiedActivationPipeline.activate(agentId) .then(result => { console.log(result.greeting); process.exit(0); }) .catch(err => { console.error(`Activation error: ${err.message}`); process.exit(1); }); }