UNPKG

aios-core

Version:

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

328 lines (283 loc) 9.85 kB
/** * Workflow Navigator - Next-Step Suggestions for Workflow State * * Provides intelligent next-step command suggestions based on: * - Current workflow state (detected from command history) * - Workflow transitions (defined in workflow-patterns.yaml) * - Context data (story path, branch, epic) * * Features: * - State detection from successful command completion * - Pre-populated command templates * - Numbered list formatting for user selection */ const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); const WORKFLOW_PATTERNS_PATH = path.join(process.cwd(), '.aios-core', 'data', 'workflow-patterns.yaml'); class WorkflowNavigator { constructor() { this.patterns = this._loadPatterns(); } /** * Detect current workflow state from command history * @param {Array<string>} commandHistory - Recent commands executed * @param {Object} context - Session context (story_path, branch, etc.) * @returns {Object|null} { workflow, state, context } or null if no state detected */ detectWorkflowState(commandHistory, context = {}) { if (!commandHistory || commandHistory.length === 0) { return null; } const lastCommand = commandHistory[commandHistory.length - 1]; // Check each workflow's transitions for (const [workflowName, workflowDef] of Object.entries(this.patterns.workflows || {})) { if (!workflowDef.transitions) { continue; } for (const [stateName, transition] of Object.entries(workflowDef.transitions)) { if (this._matchesTrigger(lastCommand, transition.trigger)) { return { workflow: workflowName, state: stateName, context: this.extractContext(context), }; } } } return null; } /** * Suggest next commands for current workflow state * @param {Object} workflowState - { workflow, state, context } * @returns {Array} Array of suggestions with pre-populated commands */ suggestNextCommands(workflowState) { if (!workflowState || !workflowState.workflow || !workflowState.state) { return []; } const workflow = this.patterns.workflows[workflowState.workflow]; if (!workflow || !workflow.transitions) { return []; } const transition = workflow.transitions[workflowState.state]; if (!transition || !transition.next_steps) { return []; } // Generate suggestions with pre-populated templates const suggestions = transition.next_steps.map(step => { const command = this.populateTemplate(step.args_template, workflowState.context); return { command: `*${step.command}${command ? ' ' + command : ''}`, description: step.description || '', raw_command: step.command, args: command, }; }); return suggestions; } /** * Populate command template with context variables * @param {string} template - Template string (e.g., "${story_path}") * @param {Object} context - Context variables * @returns {string} Populated template */ populateTemplate(template, context) { if (!template) { return ''; } let result = template; // Replace ${variable} with context values const variables = template.match(/\$\{([^}]+)\}/g); if (variables) { variables.forEach(variable => { const key = variable.slice(2, -1); // Remove ${ and } const value = context[key] || ''; result = result.replace(variable, value); }); } return result.trim(); } /** * Format suggestions as numbered list * @param {Array} suggestions - Suggestion objects * @param {string} header - Optional header text * @returns {string} Formatted suggestions */ formatSuggestions(suggestions, header = 'Next steps:') { if (!suggestions || suggestions.length === 0) { return ''; } const lines = [header, '']; suggestions.forEach((suggestion, index) => { const number = index + 1; const desc = suggestion.description ? ` - ${suggestion.description}` : ''; lines.push(`${number}. \`${suggestion.command}\`${desc}`); }); return lines.join('\n'); } /** * Extract context from session/environment * @param {Object} rawContext - Raw context data * @returns {Object} Normalized context */ extractContext(rawContext = {}) { return { story_path: rawContext.story_path || rawContext.currentStory || '', branch: rawContext.branch || rawContext.gitBranch || '', epic: rawContext.epic || rawContext.currentEpic || '', }; } /** * Check if command matches trigger pattern * @private * @param {string} command - Command to check * @param {string} trigger - Trigger pattern * @returns {boolean} True if matches */ _matchesTrigger(command, trigger) { if (!command || !trigger) { return false; } // Simple substring matching for now // Examples: // - "validate-story-draft completed successfully" // - "develop completed" const triggerCommand = trigger.split(' ')[0]; // Get command name return command.includes(triggerCommand); } /** * Load workflow patterns from YAML * @private * @returns {Object} Workflow patterns */ _loadPatterns() { try { if (!fs.existsSync(WORKFLOW_PATTERNS_PATH)) { console.warn('[WorkflowNavigator] Patterns file not found'); return { workflows: {} }; } const content = fs.readFileSync(WORKFLOW_PATTERNS_PATH, 'utf8'); return yaml.load(content) || { workflows: {} }; } catch (error) { console.warn('[WorkflowNavigator] Failed to load patterns:', error.message); return { workflows: {} }; } } /** * Detect workflow state from a state file (GAP-3 integration) * @param {string} stateFilePath - Path to a workflow state YAML file * @returns {Object|null} { workflow, state, context, stateData } or null */ detectWorkflowStateFromFile(stateFilePath) { try { if (!fs.existsSync(stateFilePath)) { return null; } const content = fs.readFileSync(stateFilePath, 'utf8'); const stateData = yaml.load(content); if (!stateData || stateData.status !== 'active') { return null; } // Try to map step index to a semantic state via workflow transitions let semanticState = `step_${stateData.current_step_index}`; const currentStep = Array.isArray(stateData.steps) ? stateData.steps[stateData.current_step_index] : null; if (currentStep && this.patterns.workflows) { const wfDef = this.patterns.workflows[stateData.workflow_id]; if (wfDef && wfDef.transitions) { for (const [stateName, transition] of Object.entries(wfDef.transitions)) { if (transition.trigger && currentStep.agent && transition.trigger.includes(currentStep.agent)) { semanticState = stateName; break; } } } } return { workflow: stateData.workflow_id, state: semanticState, context: { instance_id: stateData.instance_id, current_phase: stateData.current_phase, target_context: stateData.target_context, squad_name: stateData.squad_name, }, stateData, }; } catch (error) { console.warn('[WorkflowNavigator] Failed to load state file:', error.message); return null; } } /** * Suggest next commands based on workflow state file (GAP-3 integration) * @param {Object} state - Workflow state object from state file * @returns {Array} Array of suggestions */ suggestNextCommandsFromState(state) { if (!state || state.status !== 'active') { return []; } if (!Array.isArray(state.steps) || state.current_step_index < 0 || state.current_step_index >= state.steps.length) { return []; } const currentStep = state.steps[state.current_step_index]; if (!currentStep) { return []; } const suggestions = []; // Primary: continue the workflow suggestions.push({ command: `*run-workflow ${state.workflow_id} continue`, description: `Continue workflow — ${currentStep.phase}`, raw_command: 'run-workflow', args: `${state.workflow_id} continue`, }); // If current step has an agent, suggest activating it if (currentStep.agent) { suggestions.push({ command: `@${currentStep.agent}`, description: 'Activate agent for current step', raw_command: currentStep.agent, args: '', }); } // If current step is optional, offer skip if (currentStep.optional) { suggestions.push({ command: `*run-workflow ${state.workflow_id} skip`, description: 'Skip optional step', raw_command: 'run-workflow', args: `${state.workflow_id} skip`, }); } // Status check suggestions.push({ command: `*run-workflow ${state.workflow_id} status`, description: 'View workflow progress', raw_command: 'run-workflow', args: `${state.workflow_id} status`, }); return suggestions; } /** * Get greeting message for workflow state * @param {Object} workflowState - Workflow state * @returns {string} Greeting message */ getGreetingMessage(workflowState) { if (!workflowState || !workflowState.workflow || !workflowState.state) { return ''; } const workflow = this.patterns.workflows[workflowState.workflow]; if (!workflow || !workflow.transitions) { return ''; } const transition = workflow.transitions[workflowState.state]; return transition?.greeting_message || ''; } } module.exports = WorkflowNavigator;