aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
1,405 lines • 50.2 kB
JavaScript
/**
* Greeting Builder - Context-Aware Agent Greeting System (Core Logic)
*
* ARCHITECTURE NOTE:
* This is the CORE CLASS that contains all greeting logic.
* It can be used directly by agents OR via the CLI wrapper (generate-greeting.js).
*
* - This file: Core GreetingBuilder class
* - generate-greeting.js: CLI wrapper that orchestrates context loading
*
* Builds intelligent greetings based on:
* - Session type (new/existing/workflow)
* - Git configuration status
* - Project status (natural language narrative)
* - Command visibility metadata
* - Previous agent handoff context
* - Current story and branch references
*
* Story ACT-7: Context-Aware Greeting Sections
* - Section builders receive full enriched context from UnifiedActivationPipeline
* - Presentation adapts: new=full intro, existing=brief, workflow=focused
* - Role description references current story and branch
* - Project status uses natural language narrative
* - Context section references previous agent handoff intelligently
* - Footer varies by session context
* - Parallelizable sections executed with Promise.all()
* - Fallback to static templates if context loading fails (150ms per section)
*
* Used by: Most agents (direct invocation in STEP 3)
* Also used by: generate-greeting.js (CLI wrapper for @devops, @data-engineer, @ux-design-expert)
*
* @see docs/architecture/greeting-system.md for full architecture documentation
* @see generate-greeting.js for CLI wrapper
*
* Performance: <200ms total (hard limit with timeout protection)
* Fallback: Simple greeting on any error
*/
const ContextDetector = require('../../core/session/context-detector');
const GitConfigDetector = require('../../infrastructure/scripts/git-config-detector');
const WorkflowNavigator = require('./workflow-navigator');
const GreetingPreferenceManager = require('./greeting-preference-manager');
const { loadProjectStatus } = require('../../infrastructure/scripts/project-status-loader');
const { PermissionMode } = require('../../core/permissions');
const { resolveConfig } = require('../../core/config/config-resolver');
const { validateUserProfile } = require('../../infrastructure/scripts/validate-user-profile');
// Story ACT-5: SessionState integration for cross-terminal workflow continuity
const { SessionState } = require('../../core/orchestration/session-state');
// Story ACT-5: SurfaceChecker integration for proactive suggestions
const { SurfaceChecker } = require('../../core/orchestration/surface-checker');
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const GREETING_TIMEOUT = 150; // 150ms hard limit per-section
const _TOTAL_GREETING_TIMEOUT = 200; // 200ms total pipeline budget (Story ACT-7, documented constant)
const SECTION_TIMEOUT = 150; // 150ms per section builder (Story ACT-7 AC8)
// Story ACT-2: Validation now delegated to validate-user-profile.js
const DEFAULT_USER_PROFILE = 'advanced';
const GIT_WARNING_TEMPLATE = `
⚠️ **Git Configuration Needed**
Your project is not connected to a git repository.
Run \`git init\` and \`git remote add origin <url>\` to enable version control.
`;
class GreetingBuilder {
constructor() {
this.contextDetector = new ContextDetector();
this.gitConfigDetector = new GitConfigDetector();
this.workflowNavigator = new WorkflowNavigator();
this.preferenceManager = new GreetingPreferenceManager();
this.config = this._loadConfig();
}
/**
* Load resolved config once, shared across greeting build.
* Story ACT-9 QA fix: Eliminates duplicate resolveConfig() calls per greeting build.
* @returns {Object|null} Resolved config object, or null on failure
*/
_loadResolvedConfig() {
try {
const result = resolveConfig(process.cwd(), { skipCache: true });
return result?.config || null;
} catch (error) {
console.warn('[GreetingBuilder] Failed to load config:', error.message);
return null;
}
}
/**
* Load user profile via config-resolver (L5 User layer has highest priority).
* Story 12.1 - AC3: Uses resolveConfig() to read user_profile from layered hierarchy.
* Story ACT-2 - AC3: Runs validate-user-profile during activation (not just installation).
* Reads fresh each time (skipCache: true) to reflect toggle changes immediately.
* @param {Object} [resolvedConfig] - Pre-loaded config to avoid duplicate resolveConfig() call
* @returns {string} User profile ('bob' | 'advanced'), defaults to 'advanced'
*/
loadUserProfile(resolvedConfig) {
try {
const config = resolvedConfig || this._loadResolvedConfig();
const userProfile = config?.user_profile;
if (!userProfile) {
return DEFAULT_USER_PROFILE;
}
// Story ACT-2 - AC3: Run validation during activation pipeline (graceful)
const validation = validateUserProfile(userProfile);
if (!validation.valid) {
console.warn(`[GreetingBuilder] user_profile validation failed: ${validation.error}`);
return DEFAULT_USER_PROFILE;
}
if (validation.warning) {
console.warn(`[GreetingBuilder] user_profile warning: ${validation.warning}`);
}
return validation.value;
} catch (error) {
console.warn('[GreetingBuilder] Failed to load user_profile:', error.message);
return DEFAULT_USER_PROFILE;
}
}
/**
* Build contextual greeting for agent
* @param {Object} agent - Agent definition
* @param {Object} context - Session context
* @returns {Promise<string>} Formatted greeting
*/
async buildGreeting(agent, context = {}) {
const fallbackGreeting = this.buildSimpleGreeting(agent);
try {
// ACT-11: Use pre-loaded config from pipeline context to avoid duplicate resolveConfig()
const resolvedConfig = context._coreConfig || this._loadResolvedConfig();
// Story ACT-2: Load user profile early so preference manager can account for it
const userProfile = this.loadUserProfile(resolvedConfig);
// Check user preference (Story 6.1.4), now profile-aware (Story ACT-2)
// Story ACT-2: PM agent bypasses bob mode preference restriction because
// PM is the primary interface in bob mode and needs the full contextual greeting.
const preference = (userProfile === 'bob' && agent.id === 'pm')
? this.preferenceManager.getPreference('advanced')
: this.preferenceManager.getPreference(userProfile);
if (preference !== 'auto') {
// Override with fixed level
return this.buildFixedLevelGreeting(agent, preference);
}
// Use session-aware logic (Story 6.1.2.5)
// Story ACT-2: Pass pre-loaded userProfile to avoid double loadUserProfile() call
const greetingPromise = this._buildContextualGreeting(agent, context, userProfile);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Greeting timeout')), GREETING_TIMEOUT),
);
return await Promise.race([greetingPromise, timeoutPromise]);
} catch (error) {
console.warn('[GreetingBuilder] Fallback to simple greeting:', error.message);
return fallbackGreeting;
}
}
/**
* Build contextual greeting (internal implementation)
* Story 10.3: Profile-aware greeting with conditional agent visibility
* Story ACT-2: Accepts pre-loaded userProfile to avoid redundant loadUserProfile() calls
* Story ACT-7: Context-aware sections with parallelization and enriched context
* Story ACT-12: Language removed — delegated to Claude Code native settings.json
* @private
* @param {Object} agent - Agent definition
* @param {Object} context - Session context (may contain pre-loaded values from pipeline)
* @param {string} [preloadedUserProfile] - Pre-loaded user profile (avoids double call)
* @returns {Promise<string>} Contextual greeting
*/
async _buildContextualGreeting(agent, context, preloadedUserProfile) {
// Use pre-loaded values if available, otherwise load
const sessionType = context.sessionType || (await this._safeDetectSessionType(context));
const projectStatus = context.projectStatus || (await this._safeLoadProjectStatus());
// gitConfig: use from enriched context if available, otherwise load
const gitConfig = context.gitConfig || (await this._safeCheckGitConfig());
// Story 10.3 - AC7, AC8: Load user profile fresh each time
// Story ACT-2: Use pre-loaded value if available to avoid double resolveConfig() call
const userProfile = preloadedUserProfile || this.loadUserProfile();
// Story ACT-7 AC1: Build enriched section context for all builders
const sectionContext = {
sessionType,
projectStatus,
gitConfig,
userProfile,
previousAgent: context.previousAgent || null,
sessionStory: context.sessionStory || null,
lastCommands: context.lastCommands || [],
sessionMessage: context.sessionMessage || null,
workflowState: context.workflowState || null,
workflowActive: context.workflowActive || null,
permissions: context.permissions || null,
};
// Permission badge: use from enriched context if available, otherwise load
const permissionBadge = context.permissions?.badge || (await this._safeGetPermissionBadge());
// Build greeting sections based on session type
const sections = [];
// 1. Presentation with permission mode badge (always)
// Story ACT-7 AC2: Adapts based on session type (new=full, existing=brief, workflow=focused)
sections.push(this.buildPresentation(agent, sessionType, permissionBadge, sectionContext));
// 2. Role description (new session only, but skip in bob mode for non-PM)
// Story ACT-7 AC3: References current story and branch when available
if (sessionType === 'new' && !(userProfile === 'bob' && agent.id !== 'pm')) {
sections.push(this.buildRoleDescription(agent, sectionContext));
}
// 3. Project status (if git configured, but skip in bob mode for non-PM)
// Story ACT-7 AC4: Natural language narrative format
if (gitConfig.configured && projectStatus && !(userProfile === 'bob' && agent.id !== 'pm')) {
sections.push(this.buildProjectStatus(projectStatus, sessionType, sectionContext));
}
// Story 10.3 - AC1, AC4: Bob mode redirect for non-PM agents
if (userProfile === 'bob' && agent.id !== 'pm') {
// Show redirect message instead of normal content
sections.push(this.buildBobModeRedirect(agent));
return sections.filter(Boolean).join('\n\n');
}
// Story ACT-7 AC7: Parallel execution of independent sections
// Context section and workflow suggestions use different data sources
const [contextSection, workflowSection] = await Promise.all([
// 4. Context section (intelligent contextualization + recommendations)
// Story ACT-7 AC5: References previous agent handoff intelligently
this._safeBuildSection(() =>
this.buildContextSection(agent, context, sessionType, projectStatus, sectionContext),
),
// 5. Workflow suggestions (Story ACT-5: relaxed trigger + fixed method call)
this._safeBuildSection(() => {
if (sessionType !== 'new') {
return this.buildWorkflowSuggestions(context);
}
return null;
}),
]);
if (contextSection) {
sections.push(contextSection);
}
if (workflowSection) {
sections.push(workflowSection);
}
// 7. Commands (filtered by visibility and user profile)
// Story 10.3 - AC2, AC5: Pass userProfile for profile-aware filtering
const commands = this.filterCommandsByVisibility(agent, sessionType, userProfile);
sections.push(this.buildCommands(commands, sessionType));
// 8. Footer with signature
// Story ACT-7 AC6: Footer varies by session context
sections.push(this.buildFooter(agent, sectionContext));
return sections.filter(Boolean).join('\n\n');
}
/**
* Execute a section builder with timeout protection.
* Story ACT-7 AC8: Fallback to null if section builder exceeds SECTION_TIMEOUT.
* @private
* @param {Function} builderFn - Section builder function (sync or async)
* @returns {Promise<string|null>} Section result or null on timeout/error
*/
async _safeBuildSection(builderFn) {
try {
const result = builderFn();
// If the builder returns a promise, race it against the timeout
if (result && typeof result.then === 'function') {
return await Promise.race([
result,
new Promise((resolve) => setTimeout(() => resolve(null), SECTION_TIMEOUT)),
]);
}
return result;
} catch (error) {
console.warn('[GreetingBuilder] Section builder failed:', error.message);
return null;
}
}
/**
* Build fixed-level greeting (Story 6.1.4)
* ACT-12: Language removed — Claude Code handles translation natively via settings.json
* @param {Object} agent - Agent definition
* @param {string} level - Preference level (minimal|named|archetypal)
* @returns {string} Fixed-level greeting
*/
buildFixedLevelGreeting(agent, level) {
const profile = agent.persona_profile;
if (!profile || !profile.greeting_levels) {
return this.buildSimpleGreeting(agent);
}
// Select greeting based on preference
let greetingText;
switch (level) {
case 'minimal':
greetingText = profile.greeting_levels.minimal || `${agent.icon} ${agent.id} Agent ready`;
break;
case 'named':
greetingText = profile.greeting_levels.named || `${agent.icon} ${agent.name} ready`;
break;
case 'archetypal':
greetingText =
profile.greeting_levels.archetypal ||
`${agent.icon} ${agent.name} the ${profile.archetype} ready`;
break;
default:
greetingText = profile.greeting_levels.named || `${agent.icon} ${agent.name} ready`;
}
return `${greetingText}\n\nType \`*help\` to see available commands.`;
}
/**
* Build simple greeting (fallback)
* ACT-12: Language removed — Claude Code handles translation natively via settings.json
* @param {Object} agent - Agent definition
* @returns {string} Simple greeting
*/
buildSimpleGreeting(agent) {
const greetingLevels =
agent.persona_profile?.communication?.greeting_levels ||
agent.persona_profile?.greeting_levels;
const greeting = greetingLevels?.named || `${agent.icon} ${agent.name} ready`;
return `${greeting}\n\nType \`*help\` to see available commands.`;
}
/**
* Build presentation section
* Story ACT-7 AC2: Adapts based on session type
* - new session: full archetypal intro
* - existing session: brief "Welcome back" with current focus
* - workflow session: focused on workflow state
* @param {Object} agent - Agent definition
* @param {string} sessionType - Session type
* @param {string} permissionBadge - Permission mode badge (optional)
* @param {Object} [sectionContext] - Enriched section context (Story ACT-7)
* @returns {string} Presentation text
*/
buildPresentation(agent, sessionType, permissionBadge = '', sectionContext = null) {
const profile = agent.persona_profile;
// Try greeting_levels from communication first, then fall back to top level
const greetingLevels = profile?.communication?.greeting_levels || profile?.greeting_levels;
if (!greetingLevels) {
const base = `${agent.icon} ${agent.name} ready`;
return permissionBadge ? `${base} ${permissionBadge}` : base;
}
// Story ACT-7 AC2: Presentation adapts based on session type
// ACT-12: Language delegated to Claude Code settings.json — hardcoded English phrases
let greeting;
if (sessionType === 'existing' && sectionContext) {
// Existing session: brief welcome back
const namedGreeting = greetingLevels.named || `${agent.icon} ${agent.name} ready`;
const storyRef = sectionContext.sessionStory || sectionContext.projectStatus?.currentStory;
if (storyRef) {
greeting = `${namedGreeting} -- continuing ${storyRef}`;
} else {
greeting = `${namedGreeting} -- welcome back`;
}
} else if (sessionType === 'workflow' && sectionContext) {
// Workflow session: focused on current workflow
const namedGreeting = greetingLevels.named || `${agent.icon} ${agent.name} ready`;
const workflowPhase = sectionContext.workflowState?.currentPhase || sectionContext.workflowActive;
if (workflowPhase) {
greeting = `${namedGreeting} -- workflow active`;
} else {
greeting = namedGreeting;
}
} else {
// New session or no context: full archetypal greeting
greeting =
greetingLevels.archetypal || greetingLevels.named || `${agent.icon} ${agent.name} ready`;
}
// Append permission badge if available
return permissionBadge ? `${greeting} ${permissionBadge}` : greeting;
}
/**
* Build role description section
* Story ACT-7 AC3: References current story and branch when available.
* Skipped entirely for returning sessions (too verbose).
* @param {Object} agent - Agent definition
* @param {Object} [sectionContext] - Enriched section context (Story ACT-7)
* @returns {string} Role description
*/
buildRoleDescription(agent, sectionContext = null) {
if (!agent.persona || !agent.persona.role) {
return '';
}
let roleText = `**Role:** ${agent.persona.role}`;
// Story ACT-7 AC3: Append story/branch references when available
if (sectionContext) {
const storyRef = sectionContext.sessionStory || sectionContext.projectStatus?.currentStory;
const branchRef = sectionContext.projectStatus?.branch || sectionContext.gitConfig?.branch;
const refs = [];
if (storyRef) {
refs.push(`Story: ${storyRef}`);
}
if (branchRef && branchRef !== 'main' && branchRef !== 'master') {
refs.push(`Branch: \`${branchRef}\``);
}
if (refs.length > 0) {
roleText += `\n ${refs.join(' | ')}`;
}
}
return roleText;
}
/**
* Build project status section
* Story ACT-7 AC4: Natural language narrative format alongside bullet points.
* @param {Object} projectStatus - Project status data
* @param {string} sessionType - Session type
* @param {Object} [sectionContext] - Enriched section context (Story ACT-7)
* @returns {string} Formatted project status
*/
buildProjectStatus(projectStatus, sessionType = 'full', sectionContext = null) {
if (!projectStatus) {
return '';
}
// Story ACT-7 AC4: Use narrative format when enriched context is available
if (sectionContext) {
return this._formatProjectStatusNarrative(projectStatus, sessionType);
}
// Legacy: bullet-point format (backward compatible)
const format = sessionType === 'workflow' ? 'condensed' : 'full';
return this._formatProjectStatus(projectStatus, format);
}
/**
* Format project status as natural language narrative.
* Story ACT-7 AC4: Instead of bullet points, produce human-readable sentences.
* Example: "You're on branch `feat/act-7` with 3 modified files. Story ACT-7 is in progress."
* @private
* @param {Object} status - Project status
* @param {string} sessionType - Session type
* @returns {string} Narrative status
*/
_formatProjectStatusNarrative(status, sessionType) {
// Workflow sessions get condensed inline format
if (sessionType === 'workflow') {
return this._formatProjectStatus(status, 'condensed');
}
const sentences = [];
// Branch + modified files as natural sentence
if (status.branch) {
let branchSentence = `You're on branch \`${status.branch}\``;
const fileCount = status.modifiedFilesTotalCount || 0;
if (fileCount > 0) {
branchSentence += ` with ${fileCount} modified file${fileCount !== 1 ? 's' : ''}`;
}
branchSentence += '.';
sentences.push(branchSentence);
}
// Current story as narrative
if (status.currentStory) {
sentences.push(`Story **${status.currentStory}** is in progress.`);
}
// Recent commits as brief reference
if (status.recentCommits && status.recentCommits.length > 0) {
const lastCommit = status.recentCommits[0];
const commitMsg = typeof lastCommit === 'string' ? lastCommit : lastCommit.message || lastCommit;
const shortMsg = String(commitMsg).length > 60
? String(commitMsg).substring(0, 57) + '...'
: String(commitMsg);
sentences.push(`Last commit: "${shortMsg}"`);
}
if (sentences.length === 0) {
return '';
}
return `📊 **Project Status:** ${sentences.join(' ')}`;
}
/**
* Format project status (legacy bullet-point format)
* @private
* @param {Object} status - Project status
* @param {string} format - 'full' | 'condensed'
* @returns {string} Formatted status
*/
_formatProjectStatus(status, format) {
if (format === 'condensed') {
const parts = [];
if (status.branch) {
parts.push(`🌿 ${status.branch}`);
}
if (status.modifiedFilesTotalCount > 0) {
parts.push(`📝 ${status.modifiedFilesTotalCount} modified`);
}
if (status.currentStory) {
parts.push(`📖 ${status.currentStory}`);
}
return parts.length > 0 ? `📊 ${parts.join(' | ')}` : '';
}
// Full format with emojis
const lines = [];
if (status.branch) {
lines.push(`🌿 **Branch:** ${status.branch}`);
}
if (status.modifiedFiles && status.modifiedFiles.length > 0) {
let filesDisplay = status.modifiedFiles.join(', ');
const totalCount = status.modifiedFilesTotalCount || status.modifiedFiles.length;
if (totalCount > status.modifiedFiles.length) {
const remaining = totalCount - status.modifiedFiles.length;
filesDisplay += ` ...and ${remaining} more`;
}
lines.push(`📝 **Modified:** ${filesDisplay}`);
}
if (status.recentCommits && status.recentCommits.length > 0) {
lines.push(`📖 **Recent:** ${status.recentCommits.join(', ')}`);
}
if (status.currentStory) {
lines.push(`📌 **Story:** ${status.currentStory}`);
}
if (lines.length === 0) {
return '';
}
return `📊 **Project Status:**\n - ${lines.join('\n - ')}`;
}
/**
* Build intelligent context section with recommendations
* Story ACT-7 AC5: References previous agent handoff intelligently.
* @param {Object} agent - Agent definition
* @param {Object} context - Session context
* @param {string} sessionType - Session type
* @param {Object} projectStatus - Project status
* @param {Object} [sectionContext] - Enriched section context (Story ACT-7)
* @returns {string|null} Context section with recommendations
*/
buildContextSection(agent, context, sessionType, projectStatus, sectionContext = null) {
// Skip for new sessions
if (sessionType === 'new') {
return null;
}
const parts = [];
// Build intelligent context narrative
const contextNarrative = this._buildContextNarrative(agent, context, projectStatus);
if (contextNarrative.description) {
parts.push(`💡 **Context:** ${contextNarrative.description}`);
}
// Story ACT-7 AC5: Add handoff context when previous agent is detected
if (sectionContext && sectionContext.previousAgent && !contextNarrative.description) {
const prevName = this._getPreviousAgentName(context);
if (prevName) {
parts.push(`💡 **Context:** Picked up from @${prevName}'s session`);
}
}
if (contextNarrative.recommendedCommand) {
parts.push(` **Recommended:** Use \`${contextNarrative.recommendedCommand}\` to continue`);
}
return parts.length > 0 ? parts.join('\n') : null;
}
/**
* Build intelligent context narrative based on previous work
* Analyzes files, story, and previous agent to create rich context
* @private
*/
_buildContextNarrative(agent, context, projectStatus) {
const prevAgentId = this._getPreviousAgentId(context);
const prevAgentName = this._getPreviousAgentName(context);
// Priority 1: Agent transition + Story + Modified files (richest context)
if (prevAgentId && projectStatus?.modifiedFiles) {
// Use session story if available (more accurate), otherwise use git story
const sessionStory = context.sessionStory || projectStatus.currentStory;
const storyContext = this._analyzeStoryContext({
...projectStatus,
currentStory: sessionStory,
});
const fileContext = this._analyzeModifiedFiles(projectStatus.modifiedFiles, sessionStory);
let description = `Vejo que @${prevAgentName} finalizou os ajustes`;
if (fileContext.keyFiles.length > 0) {
description += ` ${fileContext.summary}`;
}
if (storyContext.storyFile) {
description += ` no **\`${storyContext.storyFile}\`**`;
}
description += `. Agora podemos ${this._getAgentAction(agent.id, storyContext)}`;
const recommendedCommand = this._suggestCommand(agent.id, prevAgentId, storyContext);
return { description, recommendedCommand };
}
// Priority 2: Agent transition + Story (no file details)
if (
prevAgentId &&
projectStatus?.currentStory &&
projectStatus.currentStory !== 'EPIC-SPLIT-IMPLEMENTATION-COMPLETE'
) {
const storyContext = this._analyzeStoryContext(projectStatus);
const description = `Continuando do trabalho de @${prevAgentName} em ${projectStatus.currentStory}. ${this._getAgentAction(agent.id, storyContext)}`;
const recommendedCommand = this._suggestCommand(agent.id, prevAgentId, storyContext);
return { description, recommendedCommand };
}
// Priority 3: Just agent transition
if (prevAgentId) {
const description = `Continuing from @${prevAgentName}`;
const recommendedCommand = this._suggestCommand(agent.id, prevAgentId, {});
return { description, recommendedCommand };
}
// Priority 4: Story-based context
if (
projectStatus?.currentStory &&
projectStatus.currentStory !== 'EPIC-SPLIT-IMPLEMENTATION-COMPLETE'
) {
const storyContext = this._analyzeStoryContext(projectStatus);
const description = `Working on ${projectStatus.currentStory}`;
const recommendedCommand = this._suggestCommand(agent.id, null, storyContext);
return { description, recommendedCommand };
}
// Priority 5: Last command context
if (context.lastCommands && context.lastCommands.length > 0) {
const lastCmd = context.lastCommands[context.lastCommands.length - 1];
const cmdName = typeof lastCmd === 'object' ? lastCmd.command : lastCmd;
const description = `Last action: *${cmdName}`;
return { description, recommendedCommand: null };
}
// Priority 6: Session message
if (context.sessionMessage) {
return { description: context.sessionMessage, recommendedCommand: null };
}
return { description: null, recommendedCommand: null };
}
_getPreviousAgentId(context) {
if (!context.previousAgent) return null;
return typeof context.previousAgent === 'string'
? context.previousAgent
: context.previousAgent.agentId;
}
_getPreviousAgentName(context) {
if (!context.previousAgent) return null;
return typeof context.previousAgent === 'string'
? context.previousAgent
: context.previousAgent.agentName || context.previousAgent.agentId;
}
_analyzeStoryContext(projectStatus) {
const currentStory = projectStatus.currentStory || '';
const storyFile = currentStory ? `${currentStory}.md` : null;
return {
storyId: currentStory,
storyFile: storyFile,
hasStory: !!currentStory && currentStory !== 'EPIC-SPLIT-IMPLEMENTATION-COMPLETE',
};
}
_analyzeModifiedFiles(modifiedFiles, _currentStory) {
if (!modifiedFiles || modifiedFiles.length === 0) {
return { keyFiles: [], summary: '' };
}
const keyFiles = [];
const patterns = [
{
regex: /greeting-builder\.js/,
priority: 1,
desc: 'do **`.aios-core/scripts/greeting-builder.js`**',
category: 'script',
},
{
regex: /agent-config-loader\.js/,
priority: 1,
desc: 'do **`agent-config-loader.js`**',
category: 'script',
},
{
regex: /generate-greeting\.js/,
priority: 1,
desc: 'do **`generate-greeting.js`**',
category: 'script',
},
{
regex: /session-context-loader\.js/,
priority: 1,
desc: 'do **`session-context-loader.js`**',
category: 'script',
},
{
regex: /agents\/.*\.md/,
priority: 1,
desc: 'das definições de agentes',
category: 'agent',
},
{ regex: /\.md$/, priority: 2, desc: 'dos arquivos de documentação', category: 'doc' },
];
// Find matching key files (avoid duplicates)
const seenCategories = new Set();
for (const file of modifiedFiles.slice(0, 5)) {
// Check first 5 files
for (const pattern of patterns) {
if (pattern.regex.test(file) && !seenCategories.has(pattern.category)) {
keyFiles.push({
file,
desc: pattern.desc,
priority: pattern.priority,
category: pattern.category,
});
seenCategories.add(pattern.category);
break;
}
}
}
// Sort by priority and take top 2
keyFiles.sort((a, b) => a.priority - b.priority);
const topFiles = keyFiles.slice(0, 2);
if (topFiles.length === 0) {
return { keyFiles: [], summary: 'dos arquivos do projeto' };
}
if (topFiles.length === 1) {
return { keyFiles: topFiles, summary: topFiles[0].desc };
}
return {
keyFiles: topFiles,
summary: `${topFiles[0].desc} e ${topFiles[1].desc}`,
};
}
_getAgentAction(agentId, _storyContext) {
const actions = {
qa: 'revisar a qualidade dessa implementação',
dev: 'implementar as funcionalidades',
pm: 'sincronizar o progresso',
po: 'validar os requisitos',
sm: 'coordenar o desenvolvimento',
};
return actions[agentId] || 'continuar o trabalho';
}
_suggestCommand(agentId, prevAgentId, storyContext) {
// Agent transition commands
if (prevAgentId === 'dev' && agentId === 'qa') {
return storyContext.storyFile ? `*review ${storyContext.storyFile}` : '*review';
}
if (prevAgentId === 'qa' && agentId === 'dev') {
return '*apply-qa-fixes';
}
if (prevAgentId === 'po' && agentId === 'dev') {
return '*develop-yolo';
}
// Role-based commands when no previous agent
if (agentId === 'qa' && storyContext.storyFile) {
return `*review ${storyContext.storyFile}`;
}
if (agentId === 'dev' && storyContext.hasStory) {
return '*develop-yolo docs/stories/[story-path].md';
}
if (agentId === 'pm' && storyContext.storyId) {
return `*sync-story ${storyContext.storyId}`;
}
return null;
}
/**
* Build current context section (legacy - kept for compatibility)
* @param {Object} context - Session context
* @param {string} sessionType - Session type
* @param {Object} projectStatus - Project status
* @returns {string} Context description
*/
buildCurrentContext(context, sessionType, projectStatus) {
if (sessionType === 'workflow' && projectStatus?.currentStory) {
return `📌 **Context:** Working on ${projectStatus.currentStory}`;
}
if (context.lastCommand) {
return `📌 **Last Action:** ${context.lastCommand}`;
}
return '';
}
/**
* Build workflow suggestions section
* Story ACT-5: Enhanced with SessionState integration for cross-terminal
* workflow continuity and SurfaceChecker for proactive suggestions.
*
* Detection priority:
* 1. SessionState (cross-terminal persistence from Epic 11 Story 11.5)
* 2. Command history (pattern-based detection from workflow-patterns.yaml)
*
* @param {Object} context - Session context
* @returns {string|null} Workflow suggestions or null
*/
buildWorkflowSuggestions(context) {
try {
// Story ACT-5 (AC: 3, 6): Check SessionState first for cross-terminal continuity
const sessionStateResult = this._detectWorkflowFromSessionState();
if (sessionStateResult) {
return sessionStateResult;
}
// Fallback: Pattern-based detection from command history
const commandHistory = context.commandHistory || context.lastCommands || [];
const workflowState = this.workflowNavigator.detectWorkflowState(commandHistory, context);
if (!workflowState) {
return null;
}
const suggestions = this.workflowNavigator.suggestNextCommands(workflowState);
if (!suggestions || suggestions.length === 0) {
return null;
}
// Story ACT-5 (AC: 4): Enhance suggestions with SurfaceChecker proactive triggers
const enhancedSuggestions = this._enhanceSuggestionsWithSurface(suggestions, context);
const greetingMessage = this.workflowNavigator.getGreetingMessage(workflowState);
const header = greetingMessage || 'Next steps:';
return this.workflowNavigator.formatSuggestions(enhancedSuggestions, header);
} catch (error) {
console.warn('[GreetingBuilder] Workflow suggestions failed:', error.message);
return null;
}
}
/**
* Detect workflow state from SessionState for cross-terminal continuity.
* Story ACT-5 (AC: 3, 6): Reads persisted session state to detect
* active workflows that span terminal sessions.
* @private
* @returns {string|null} Formatted workflow section or null
*/
_detectWorkflowFromSessionState() {
try {
const projectRoot = process.cwd();
const sessionState = new SessionState(projectRoot);
// Use synchronous existence check to stay within perf budget
const stateFilePath = sessionState.getStateFilePath();
if (!fs.existsSync(stateFilePath)) {
return null;
}
// Read and parse state file synchronously (fast, local file)
const content = fs.readFileSync(stateFilePath, 'utf8');
const stateData = yaml.load(content);
if (!stateData?.session_state) {
return null;
}
const ss = stateData.session_state;
// Only show if there is an active workflow with a current story
if (!ss.progress?.current_story || !ss.workflow?.current_phase) {
return null;
}
// Build suggestions from session state
const suggestions = [];
const currentStory = ss.progress.current_story;
const currentPhase = ss.workflow.current_phase;
const storiesDone = ss.progress.stories_done?.length || 0;
const totalStories = ss.epic?.total_stories || 0;
suggestions.push({
command: `*develop-yolo ${currentStory}`,
description: `Continue ${currentStory} (phase: ${currentPhase})`,
raw_command: 'develop-yolo',
args: currentStory,
});
if (storiesDone > 0 && totalStories > 0) {
suggestions.push({
command: `*build-status ${currentStory}`,
description: `Check build status (${storiesDone}/${totalStories} stories done)`,
raw_command: 'build-status',
args: currentStory,
});
}
const header = `Workflow in progress: ${ss.epic?.title || 'Active Epic'} (${storiesDone}/${totalStories})`;
return this.workflowNavigator.formatSuggestions(suggestions, header);
} catch (error) {
// Graceful degradation: if SessionState is unavailable, return null
console.warn('[GreetingBuilder] SessionState workflow detection failed:', error.message);
return null;
}
}
/**
* Enhance workflow suggestions with SurfaceChecker proactive triggers.
* Story ACT-5 (AC: 4): Uses surface conditions to add relevant
* proactive suggestions (e.g., cost warnings, risk alerts).
* @private
* @param {Array} suggestions - Base suggestions from WorkflowNavigator
* @param {Object} context - Session context
* @returns {Array} Enhanced suggestions array
*/
_enhanceSuggestionsWithSurface(suggestions, context) {
try {
const checker = new SurfaceChecker();
if (!checker.load()) {
return suggestions; // Graceful: criteria file not found
}
// Build surface context from session data
const surfaceContext = {
risk_level: context.riskLevel || 'LOW',
errors_in_task: context.errorsInTask || 0,
action_type: context.actionType || null,
};
const result = checker.shouldSurface(surfaceContext);
if (result.should_surface && result.message) {
// Prepend a proactive warning suggestion
return [
{
command: '*help',
description: `[${result.severity}] ${result.message}`,
raw_command: 'help',
args: '',
},
...suggestions,
];
}
return suggestions;
} catch (error) {
// Graceful degradation: SurfaceChecker unavailable, return original suggestions
console.warn('[GreetingBuilder] SurfaceChecker enhancement failed:', error.message);
return suggestions;
}
}
/**
* Build contextual suggestions based on project state
* Analyzes current context and suggests relevant next commands
* @param {Object} agent - Agent definition
* @param {Object} projectStatus - Project status data
* @param {string} sessionType - Session type
* @returns {string|null} Contextual suggestions or null
*/
buildContextualSuggestions(agent, projectStatus, _sessionType) {
try {
const suggestions = [];
const agentId = agent.id;
// Analyze current story status
if (projectStatus.currentStory) {
const storyMatch = projectStatus.currentStory.match(/(\d+\.\d+\.\d+(\.\d+)?)/);
const storyId = storyMatch ? storyMatch[1] : null;
// QA agent: suggest validation if story is ready
if (
agentId === 'qa' &&
projectStatus.recentCommits &&
projectStatus.recentCommits.length > 0
) {
const recentCommit = projectStatus.recentCommits[0].message;
if (recentCommit.includes('complete') || recentCommit.includes('implement')) {
if (storyId) {
suggestions.push(`*review ${storyId}`);
} else {
suggestions.push('*code-review committed');
}
}
}
// Dev agent: suggest development tasks
if (agentId === 'dev' && storyId) {
if (projectStatus.modifiedFilesTotalCount > 0) {
suggestions.push('*run-tests');
}
suggestions.push(`*develop-story ${storyId}`);
}
// PM/PO: suggest story/epic management
if ((agentId === 'pm' || agentId === 'po') && storyId) {
suggestions.push(`*validate-story-draft ${storyId}`);
}
}
// Analyze modified files
if (projectStatus.modifiedFilesTotalCount > 0) {
if (agentId === 'qa') {
suggestions.push('*code-review uncommitted');
}
if (agentId === 'dev' && projectStatus.modifiedFilesTotalCount > 5) {
suggestions.push('*commit-changes');
}
}
// Analyze recent work
if (projectStatus.recentCommits && projectStatus.recentCommits.length > 0) {
const lastCommit = projectStatus.recentCommits[0].message;
// If last commit was a test, suggest review
if (lastCommit.includes('test') && agentId === 'qa') {
suggestions.push('*run-tests');
}
// If last commit was a feature, suggest QA
if ((lastCommit.includes('feat:') || lastCommit.includes('feature')) && agentId === 'qa') {
suggestions.push('*code-review committed');
}
}
// No suggestions found
if (suggestions.length === 0) {
return null;
}
// Build suggestion message
const contextSummary = this._buildContextSummary(projectStatus);
const commandsList = suggestions
.slice(0, 2) // Limit to 2 suggestions
.map((cmd) => ` - \`${cmd}\``)
.join('\n');
return `💡 **Context:** ${contextSummary}\n\n**Suggested Next Steps:**\n${commandsList}`;
} catch (error) {
console.warn('[GreetingBuilder] Contextual suggestions failed:', error.message);
return null;
}
}
/**
* Build context summary based on project status
* @private
* @param {Object} projectStatus - Project status data
* @returns {string} Context summary
*/
_buildContextSummary(projectStatus) {
const parts = [];
if (projectStatus.currentStory) {
parts.push(`Working on ${projectStatus.currentStory}`);
}
if (projectStatus.modifiedFilesTotalCount > 0) {
parts.push(`${projectStatus.modifiedFilesTotalCount} files modified`);
}
if (projectStatus.recentCommits && projectStatus.recentCommits.length > 0) {
const lastCommit = projectStatus.recentCommits[0].message;
const shortMsg = lastCommit.length > 50 ? lastCommit.substring(0, 47) + '...' : lastCommit;
parts.push(`Last: "${shortMsg}"`);
}
return parts.join(', ') || 'Ready to start';
}
/**
* Build commands section
* @param {Array} commands - Filtered commands
* @param {string} sessionType - Session type
* @returns {string} Commands list
*/
buildCommands(commands, sessionType) {
if (!commands || commands.length === 0) {
return '**Commands:** Type `*help` for available commands';
}
const header = this._getCommandsHeader(sessionType);
const commandList = commands
.slice(0, 12) // Max 12 commands
.map((cmd) => {
// Handle both object format and string format
if (typeof cmd === 'string') {
return ` - \`*${cmd}\``;
}
if (typeof cmd === 'object' && cmd !== null) {
const name = cmd.name || cmd.command || String(cmd);
const description = cmd.description || '';
return description ? ` - \`*${name}\`: ${description}` : ` - \`*${name}\``;
}
// Fallback for unexpected formats
return ` - \`*${String(cmd)}\``;
})
.filter((cmd) => !cmd.includes('[object Object]')) // Filter out malformed commands
.join('\n');
return `**${header}:**\n${commandList}`;
}
/**
* Get commands header based on session type
* @private
* @param {string} sessionType - Session type
* @returns {string} Header text
*/
_getCommandsHeader(sessionType) {
switch (sessionType) {
case 'new':
return 'Available Commands';
case 'existing':
return 'Quick Commands';
case 'workflow':
return 'Key Commands';
default:
return 'Commands';
}
}
/**
* Build bob mode redirect message for non-PM agents
* Story 10.3 - AC4: Show informative message redirecting to Bob
* @param {Object} agent - Agent definition (used for personalization)
* @returns {string} Redirect message
*/
buildBobModeRedirect(agent) {
const agentName = agent?.name || 'Este agente';
return `💡 **Você está no Modo Assistido.**
${agentName} não está disponível diretamente no Modo Assistido.
Use \`@pm\` (Bob) para todas as interações. Bob vai orquestrar os outros agentes internamente para você.
**Para interagir com Bob:**
- Digite \`@pm\` ou \`/AIOS:agents:pm\`
- Use \`*help\` após ativar Bob para ver comandos disponíveis`;
}
/**
* Build footer section
* Story ACT-7 AC6: Footer varies by session context.
* - new session: full guide prompt + signature
* - existing session: brief tip + signature
* - workflow session: progress note + signature
* @param {Object} agent - Agent definition
* @param {Object} [sectionContext] - Enriched section context (Story ACT-7)
* @returns {string} Footer text with signature
*/
buildFooter(agent, sectionContext = null) {
const parts = [];
// Story ACT-7 AC6: Vary footer content by session context
// ACT-12: Language delegated to Claude Code settings.json — hardcoded English phrases
const sessionType = sectionContext?.sessionType || 'new';
if (sessionType === 'workflow') {
// Workflow: progress note
const storyRef = sectionContext?.sessionStory || sectionContext?.projectStatus?.currentStory;
if (storyRef) {
parts.push(`Focused on **${storyRef}**. Type \`*help\` to see available commands.`);
} else {
parts.push('Workflow active. Type `*help` to see available commands.');
}
} else if (sessionType === 'existing') {
// Existing session: brief tip with session-info reference
parts.push('Type `*help` for commands or `*session-info` for session details.');
} else {
// New session: full guide prompt
parts.push('Type `*guide` for comprehensive usage instructions.');
}
// Add agent signature if available
if (
agent &&
agent.persona_profile &&
agent.persona_profile.communication &&
agent.persona_profile.communication.signature_closing
) {
parts.push('');
parts.push(agent.persona_profile.communication.signature_closing);
}
return parts.join('\n');
}
/**
* Build git warning section
* @returns {string} Git warning message
*/
buildGitWarning() {
return GIT_WARNING_TEMPLATE.trim();
}
/**
* Filter commands by visibility metadata and user profile
* Story 10.3 - AC1, AC2, AC3: Profile-aware command filtering
* @param {Object} agent - Agent definition
* @param {string} sessionType - Session type
* @param {string} userProfile - User profile ('bob' | 'advanced')
* @returns {Array} Filtered commands
*/
filterCommandsByVisibility(agent, sessionType, userProfile = DEFAULT_USER_PROFILE) {
if (!agent.commands || agent.commands.length === 0) {
return [];
}
// Story 10.3 - AC1, AC2: Profile-based filtering
// If bob mode AND not PM agent: return empty (will show redirect message instead)
if (userProfile === 'bob' && agent.id !== 'pm') {
return [];
}
// Story 10.3 - AC5: PM agent shows all commands in bob mode
// Story 10.3 - AC2: Advanced mode shows commands normally (current behavior)
const visibilityFilter = this._getVisibilityFilter(sessionType);
// Filter commands with visibility metadata
const commandsWithMetadata = agent.commands.filter((cmd) => {
if (!cmd.visibility || !Array.isArray(cmd.visibility)) {
return false; // No metadata, exclude from filtered list
}
return cmd.visibility.includes(visibilityFilter);
});
// If we have metadata-based commands, use them
if (commandsWithMetadata.length > 0) {
return commandsWithMetadata;
}
// Backwards compatibility: No metadata found, show first 12 commands
return agent.commands.slice(0, 12);
}
/**
* Get visibility filter for session type
* @private
* @param {string} sessionType - Session type
* @returns {string} Visibility level ('full', 'quick', 'key')
*/
_getVisibilityFilter(sessionType) {
switch (sessionType) {
case 'new':
return 'full';
case 'existing':
return 'quick';
case 'workflow':
return 'key';
default:
return 'full';
}
}
/**
* Safe session type detection with fallback
* @private
* @param {Object} context - Session context
* @returns {Promise<string>} Session type
*/
async _safeDetectSessionType(context) {
try {
const conversationHistory = context.conversationHistory || [];
return this.contextDetector.detectSessionType(conversationHistory);
} catch (error) {
console.warn('[GreetingBuilder] Session detection failed:', error.message);
return 'new'; // Conservative default
}
}
/**
* Safe git config check with fallback
* @private
* @returns {Promise<Object>} Git config result
*/
async _safeCheckGitConfig() {
try {
return this.gitConfigDetector.get();
} catch (error) {
console.warn('[GreetingBuilder] Git config check failed:', error.message);
return { configured: false, type: null, branch: null };
}
}
/**
* Safe project status load with fallback
* @private
* @returns {Promise<Object|null>} Project status or null
*/
async _safeLoadProjectStatus() {
try {
return await loadProjectStatus();
} catch (error) {
console.warn('[GreetingBuilder] Project status load failed:', error.message);
return null;
}
}
/**
* Safe permission badge retrieval with fallback
* @private
* @returns {Promise<string>} Permission mode badge or empty string
*/
async _safeGetPermissionBadge() {
try {
const mode = new PermissionMode();
await mode.load();
return mode.getBadge();
} catch (error) {
console.warn('[GreetingBuilder] Permission mode load failed:', error.message);
return '';
}
}
/**
* Check if git warning should be shown
* @private
* @returns {boolean} True if should show warning
*/
_shouldShowGitWarning() {
if (!this.config || !this.config.git) {
return true; // Default: show warning
}
return this.config.git.showConfigWarning !== false;
}
/**
* Load core configuration
* @private
* @returns {Object|null} Configuration or null
*/
_loadConfig() {
try {
const configPath = path.join(process.cwd(), '.aios-core', 'core-config.yaml');
const content = fs.readFileSync(configPath, 'utf8');
return yaml.load(content);
} catch (_error) {
return null;
}
}
}
module.exports = GreetingBuilder;