#!/usr/bin/env python3 """ Oracle - Enhanced Session Handoff Generates comprehensive context for new sessions to prevent degradation from compaction. This solves the "sessions going insane" problem by preserving critical context when switching to a fresh session. Usage: # Generate handoff context for new session python session_handoff.py --export # Import handoff context in new session python session_handoff.py --import handoff_context.json # Show what would be included (dry run) python session_handoff.py --preview Environment Variables: ORACLE_VERBOSE: Set to '1' for detailed output """ import os import sys import json import argparse from pathlib import Path from typing import Dict, List, Any, Optional from datetime import datetime, timezone def get_session_context() -> Dict[str, Any]: """Extract critical session context for handoff. Returns: Dictionary with session context for new session """ context = { 'handoff_timestamp': datetime.now(timezone.utc).isoformat(), 'handoff_reason': 'session_degradation', 'oracle_knowledge': {}, 'guardian_health': {}, 'summoner_state': {}, 'active_tasks': [], 'critical_patterns': [], 'recent_corrections': [], 'session_stats': {} } # Load Oracle knowledge (critical patterns only) oracle_dir = Path('.oracle') if oracle_dir.exists(): context['oracle_knowledge'] = load_critical_oracle_knowledge(oracle_dir) # Load Guardian session health guardian_dir = Path('.guardian') if guardian_dir.exists(): context['guardian_health'] = load_guardian_health(guardian_dir) # Load Summoner state (active MCDs) summoner_dir = Path('.summoner') if summoner_dir.exists(): context['summoner_state'] = load_summoner_state(summoner_dir) # Get active tasks from current session context['active_tasks'] = extract_active_tasks() # Get session statistics context['session_stats'] = get_session_statistics() return context def load_critical_oracle_knowledge(oracle_dir: Path) -> Dict[str, Any]: """Load only critical/high-priority Oracle knowledge. This is KISS - we don't dump everything, just what matters. Args: oracle_dir: Path to .oracle directory Returns: Critical knowledge for handoff """ knowledge = { 'critical_patterns': [], 'recent_corrections': [], 'active_gotchas': [], 'project_context': '' } knowledge_dir = oracle_dir / 'knowledge' if not knowledge_dir.exists(): return knowledge # Load critical patterns patterns_file = knowledge_dir / 'patterns.json' if patterns_file.exists(): try: with open(patterns_file, 'r', encoding='utf-8') as f: patterns = json.load(f) # Only critical/high priority knowledge['critical_patterns'] = [ p for p in patterns if p.get('priority') in ['critical', 'high'] ][:10] # Max 10 patterns except (OSError, IOError, json.JSONDecodeError): pass # Load recent corrections (last 5) corrections_file = knowledge_dir / 'corrections.json' if corrections_file.exists(): try: with open(corrections_file, 'r', encoding='utf-8') as f: corrections = json.load(f) # Sort by timestamp, take last 5 sorted_corrections = sorted( corrections, key=lambda x: x.get('created', ''), reverse=True ) knowledge['recent_corrections'] = sorted_corrections[:5] except (OSError, IOError, json.JSONDecodeError): pass # Load active gotchas gotchas_file = knowledge_dir / 'gotchas.json' if gotchas_file.exists(): try: with open(gotchas_file, 'r', encoding='utf-8') as f: gotchas = json.load(f) # Only high priority gotchas knowledge['active_gotchas'] = [ g for g in gotchas if g.get('priority') == 'high' ][:5] # Max 5 gotchas except (OSError, IOError, json.JSONDecodeError): pass return knowledge def load_guardian_health(guardian_dir: Path) -> Dict[str, Any]: """Load Guardian session health metrics. Args: guardian_dir: Path to .guardian directory Returns: Health metrics and degradation signals """ health = { 'last_health_score': None, 'degradation_signals': [], 'handoff_reason': '', 'session_duration_minutes': 0 } health_file = guardian_dir / 'session_health.json' if health_file.exists(): try: with open(health_file, 'r', encoding='utf-8') as f: data = json.load(f) health['last_health_score'] = data.get('health_score') health['degradation_signals'] = data.get('degradation_signals', []) health['handoff_reason'] = data.get('handoff_reason', '') health['session_duration_minutes'] = data.get('duration_minutes', 0) except (OSError, IOError, json.JSONDecodeError): pass return health def load_summoner_state(summoner_dir: Path) -> Dict[str, Any]: """Load Summoner active MCDs and task state. Args: summoner_dir: Path to .summoner directory Returns: Active mission state """ state = { 'active_mcds': [], 'pending_tasks': [], 'completed_phases': [] } # Check for active MCDs mcds_dir = summoner_dir / 'mcds' if mcds_dir.exists(): for mcd_file in mcds_dir.glob('*.md'): try: with open(mcd_file, 'r', encoding='utf-8') as f: content = f.read() # Extract summary and pending tasks state['active_mcds'].append({ 'name': mcd_file.stem, 'file': str(mcd_file), 'summary': extract_mcd_summary(content), 'pending_tasks': extract_pending_tasks(content) }) except (OSError, IOError, UnicodeDecodeError): continue return state def extract_mcd_summary(mcd_content: str) -> str: """Extract executive summary from MCD. Args: mcd_content: MCD markdown content Returns: Summary text (max 200 chars) """ lines = mcd_content.split('\n') in_summary = False summary_lines = [] for line in lines: if '## Executive Summary' in line: in_summary = True continue elif in_summary and line.startswith('##'): break elif in_summary and line.strip(): summary_lines.append(line.strip()) summary = ' '.join(summary_lines) return summary[:200] + '...' if len(summary) > 200 else summary def extract_pending_tasks(mcd_content: str) -> List[str]: """Extract uncompleted tasks from MCD. Args: mcd_content: MCD markdown content Returns: List of pending task descriptions """ pending = [] lines = mcd_content.split('\n') for line in lines: # Look for unchecked checkboxes if '- [ ]' in line: task = line.replace('- [ ]', '').strip() pending.append(task) return pending[:10] # Max 10 pending tasks def extract_active_tasks() -> List[str]: """Extract active tasks from current session. Returns: List of active task descriptions """ # This would integrate with Claude Code's task system # For now, return placeholder return [] def get_session_statistics() -> Dict[str, Any]: """Get current session statistics. Returns: Session stats (duration, files modified, etc.) """ stats = { 'duration_minutes': 0, 'files_modified': 0, 'commands_run': 0, 'errors_encountered': 0 } # Would integrate with Claude Code session tracking # For now, return placeholder return stats def generate_handoff_message(context: Dict[str, Any]) -> str: """Generate human-readable handoff message for new session. Args: context: Session context dictionary Returns: Formatted handoff message """ lines = [] lines.append("=" * 70) lines.append("SESSION HANDOFF CONTEXT") lines.append("=" * 70) lines.append("") # Handoff reason health = context.get('guardian_health', {}) if health.get('handoff_reason'): lines.append(f"Handoff Reason: {health['handoff_reason']}") lines.append(f"Previous Session Health: {health.get('last_health_score', 'N/A')}/100") lines.append(f"Session Duration: {health.get('session_duration_minutes', 0)} minutes") lines.append("") # Critical Oracle knowledge oracle = context.get('oracle_knowledge', {}) if oracle.get('critical_patterns'): lines.append("CRITICAL PATTERNS:") lines.append("-" * 70) for pattern in oracle['critical_patterns'][:5]: lines.append(f" • {pattern.get('title', 'Unknown')}") if pattern.get('content'): lines.append(f" {pattern['content'][:100]}...") lines.append("") if oracle.get('recent_corrections'): lines.append("RECENT CORRECTIONS (Don't repeat these mistakes):") lines.append("-" * 70) for correction in oracle['recent_corrections']: lines.append(f" • {correction.get('title', 'Unknown')}") lines.append("") if oracle.get('active_gotchas'): lines.append("ACTIVE GOTCHAS:") lines.append("-" * 70) for gotcha in oracle['active_gotchas']: lines.append(f" • {gotcha.get('title', 'Unknown')}") lines.append("") # Active Summoner MCDs summoner = context.get('summoner_state', {}) if summoner.get('active_mcds'): lines.append("ACTIVE MISSION CONTROL DOCUMENTS:") lines.append("-" * 70) for mcd in summoner['active_mcds']: lines.append(f" • {mcd['name']}") if mcd.get('summary'): lines.append(f" Summary: {mcd['summary']}") if mcd.get('pending_tasks'): lines.append(f" Pending tasks: {len(mcd['pending_tasks'])}") lines.append("") lines.append("=" * 70) lines.append("Use '/handoff-continue' to pick up where we left off") lines.append("=" * 70) return "\n".join(lines) def export_handoff_context(output_file: str = 'handoff_context.json') -> None: """Export session context for handoff. Args: output_file: Path to output JSON file """ context = get_session_context() # Save JSON with open(output_file, 'w', encoding='utf-8') as f: json.dump(context, f, indent=2) # Print human-readable message message = generate_handoff_message(context) print(message) print(f"\n✅ Handoff context saved to: {output_file}") print("\nIn your new session, run:") print(f" python session_handoff.py --import {output_file}") def import_handoff_context(input_file: str) -> None: """Import handoff context in new session. Args: input_file: Path to handoff JSON file """ if not Path(input_file).exists(): print(f"❌ Handoff file not found: {input_file}") sys.exit(1) with open(input_file, 'r', encoding='utf-8') as f: context = json.load(f) # Display handoff message message = generate_handoff_message(context) print(message) print("\n✅ Session handoff complete!") print("You're now up to speed with critical context from the previous session.") def preview_handoff() -> None: """Preview what would be included in handoff.""" context = get_session_context() message = generate_handoff_message(context) print(message) def main(): parser = argparse.ArgumentParser( description='Enhanced session handoff with Oracle/Guardian/Summoner integration', formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( '--export', action='store_true', help='Export handoff context for new session' ) parser.add_argument( '--import', dest='import_file', help='Import handoff context from file' ) parser.add_argument( '--preview', action='store_true', help='Preview handoff context without exporting' ) parser.add_argument( '--output', default='handoff_context.json', help='Output file for export (default: handoff_context.json)' ) args = parser.parse_args() if args.export: export_handoff_context(args.output) elif args.import_file: import_handoff_context(args.import_file) elif args.preview: preview_handoff() else: parser.print_help() sys.exit(1) if __name__ == '__main__': main()