Initial commit
This commit is contained in:
448
skills/oracle/scripts/session_handoff.py
Executable file
448
skills/oracle/scripts/session_handoff.py
Executable file
@@ -0,0 +1,448 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user