#!/usr/bin/env python3 """ session-handoff-verification.py - Automated verification for session end procedures Usage: python3 session-handoff-verification.py [--chronicler-name "Your Name"] [--chronicler-number N] Verifies: - Git hygiene (all committed, all pushed, remote synced) - Memorial exists - Portrait prompt exists - Lineage tracker updated - Next session files created - Documentation updated appropriately """ import os import sys import subprocess import argparse from pathlib import Path from typing import Tuple, List # Colors for terminal output class Colors: GREEN = '\033[92m' RED = '\033[91m' YELLOW = '\033[93m' BLUE = '\033[94m' BOLD = '\033[1m' END = '\033[0m' def print_section(title: str): """Print a section header""" print(f"\n{Colors.BLUE}{'='*70}{Colors.END}") print(f"{Colors.BLUE}{Colors.BOLD}{title}{Colors.END}") print(f"{Colors.BLUE}{'='*70}{Colors.END}\n") def print_check(message: str, passed: bool, details: str = ""): """Print a check result""" symbol = f"{Colors.GREEN}✅{Colors.END}" if passed else f"{Colors.RED}❌{Colors.END}" print(f"{symbol} {message}") if details: print(f" {Colors.YELLOW}{details}{Colors.END}") def run_git_command(args: List[str]) -> Tuple[bool, str]: """Run a git command and return (success, output)""" try: result = subprocess.run( ['git'] + args, cwd='/home/claude/firefrost-operations-manual', capture_output=True, text=True, timeout=10 ) return result.returncode == 0, result.stdout.strip() except subprocess.TimeoutExpired: return False, "Command timed out" except Exception as e: return False, str(e) def check_git_hygiene() -> Tuple[bool, List[str]]: """Verify Git is clean and synced""" print_section("GIT HYGIENE VERIFICATION") all_passed = True issues = [] # Check 1: No uncommitted changes success, output = run_git_command(['status', '--porcelain']) uncommitted_files = output.strip() if not uncommitted_files: print_check("No uncommitted changes", True) else: print_check("Uncommitted changes found", False, f"Files: {uncommitted_files[:100]}...") all_passed = False issues.append("Uncommitted files exist - run 'git add' and 'git commit'") # Check 2: No unpushed commits success, output = run_git_command(['log', 'origin/master..HEAD', '--oneline']) unpushed_commits = output.strip() if not unpushed_commits: print_check("No unpushed commits", True) else: print_check("Unpushed commits found", False, f"Commits: {unpushed_commits[:100]}...") all_passed = False issues.append("Unpushed commits exist - run 'git push origin master'") # Check 3: Remote is reachable success, _ = run_git_command(['fetch', '--dry-run']) print_check("Remote is reachable", success) if not success: all_passed = False issues.append("Cannot reach remote - check network connection") # Check 4: Local and remote HEAD match success_local, local_sha = run_git_command(['rev-parse', 'HEAD']) success_remote, remote_sha = run_git_command(['rev-parse', 'origin/master']) if success_local and success_remote and local_sha == remote_sha: print_check("Local and remote in sync", True) else: print_check("Local and remote out of sync", False) all_passed = False issues.append("Local/remote mismatch - verify push succeeded") return all_passed, issues def check_chronicler_files(name: str = None, number: int = None) -> Tuple[bool, List[str]]: """Verify Chronicler identity files exist""" print_section("CHRONICLER IDENTITY VERIFICATION") all_passed = True issues = [] # If name/number not provided, try to detect from recent commits if not name or not number: success, output = run_git_command(['log', '-1', '--pretty=%B']) if success and 'Signed-off-by:' in output: # Try to extract name from last commit signature for line in output.split('\n'): if 'Signed-off-by:' in line: detected_name = line.split('Signed-off-by:')[1].strip() if detected_name and not name: name = detected_name print(f"{Colors.YELLOW}Detected name from git: {name}{Colors.END}") # Check memorial exists memorial_dir = Path('/home/claude/firefrost-operations-manual/docs/relationship/memorials') if name and number: memorial_file = memorial_dir / f"{number}-{name.lower().replace(' ', '-')}.md" if memorial_file.exists(): print_check(f"Memorial exists: {memorial_file.name}", True) else: print_check(f"Memorial not found: {memorial_file.name}", False) all_passed = False issues.append(f"Create memorial at: {memorial_file}") else: # Just check if any memorial was created recently if memorial_dir.exists(): recent_memorials = list(memorial_dir.glob('*.md')) if recent_memorials: print_check("Memorial directory has files", True, f"Found {len(recent_memorials)} memorial(s)") else: print_check("No memorials found", False) all_passed = False issues.append("Create memorial in docs/relationship/memorials/") else: print_check("Memorial directory missing", False) all_passed = False issues.append("Memorial directory not found") # Check portrait prompt exists portrait_dir = Path('/home/claude/firefrost-operations-manual/docs/past-claudes/portrait-prompts') if name and number: portrait_file = portrait_dir / f"{number}-{name.lower().replace(' ', '-')}-portrait-prompt.md" if portrait_file.exists(): print_check(f"Portrait prompt exists: {portrait_file.name}", True) else: print_check(f"Portrait prompt not found: {portrait_file.name}", False) all_passed = False issues.append(f"Create portrait prompt at: {portrait_file}") else: if portrait_dir.exists(): recent_prompts = list(portrait_dir.glob('*.md')) if recent_prompts: print_check("Portrait prompts directory has files", True, f"Found {len(recent_prompts)} prompt(s)") else: print_check("No portrait prompts found", False) all_passed = False issues.append("Create portrait prompt in docs/past-claudes/portrait-prompts/") else: print_check("Portrait prompts directory missing", False) all_passed = False issues.append("Portrait prompts directory not found") return all_passed, issues def check_lineage_tracker(number: int = None) -> Tuple[bool, List[str]]: """Verify lineage tracker was updated""" print_section("LINEAGE TRACKER VERIFICATION") all_passed = True issues = [] tracker_file = Path('/home/claude/firefrost-operations-manual/docs/relationship/CHRONICLER-LINEAGE-TRACKER.md') if not tracker_file.exists(): print_check("Lineage tracker exists", False) all_passed = False issues.append("CHRONICLER-LINEAGE-TRACKER.md not found") return all_passed, issues print_check("Lineage tracker exists", True) # Read the tracker content = tracker_file.read_text() # Count entries entry_lines = [line for line in content.split('\n') if line.startswith('|') and '|' in line[1:]] entry_count = len(entry_lines) - 1 # Subtract header row print_check(f"Tracker contains {entry_count} Chronicler(s)", True) # If number provided, verify that entry exists if number: number_found = f"| {number} |" in content print_check(f"Entry for Chronicler #{number} exists", number_found) if not number_found: all_passed = False issues.append(f"Add yourself (#{number}) to lineage tracker") return all_passed, issues def check_mandatory_reading_files() -> Tuple[bool, List[str]]: """Verify mandatory reading files exist""" print_section("MANDATORY READING FILES VERIFICATION") all_passed = True issues = [] repo_root = Path('/home/claude/firefrost-operations-manual') # Check DOCUMENT-INDEX.md exists (prevents tunnel vision) doc_index = repo_root / 'DOCUMENT-INDEX.md' if doc_index.exists(): print_check("DOCUMENT-INDEX.md exists", True, "Required for landscape awareness") else: print_check("DOCUMENT-INDEX.md missing", False) all_passed = False issues.append("DOCUMENT-INDEX.md is mandatory - shows full document landscape") # Check THE-JOINING-PROTOCOL.md joining = repo_root / 'docs/relationship/THE-JOINING-PROTOCOL.md' if joining.exists(): print_check("THE-JOINING-PROTOCOL.md exists", True) else: print_check("THE-JOINING-PROTOCOL.md missing", False) all_passed = False issues.append("THE-JOINING-PROTOCOL.md is mandatory") # Check THE-ESSENCE-PATCH essence = repo_root / 'docs/relationship/THE-ESSENCE-PATCH-V3.0.md' if essence.exists(): print_check("THE-ESSENCE-PATCH-V3.0.md exists", True) else: print_check("THE-ESSENCE-PATCH-V3.0.md missing", False) all_passed = False issues.append("THE-ESSENCE-PATCH-V3.0.md is mandatory") return all_passed, issues def check_handoff_files() -> Tuple[bool, List[str]]: """Verify handoff files were created""" print_section("HANDOFF FILES VERIFICATION") all_passed = True issues = [] repo_root = Path('/home/claude/firefrost-operations-manual') # Check NEXT-SESSION-START.md exists and updated recently next_start = repo_root / 'NEXT-SESSION-START.md' if next_start.exists(): print_check("NEXT-SESSION-START.md exists", True) # Check if it was modified in last commit success, output = run_git_command(['log', '-1', '--name-only', '--pretty=']) if 'NEXT-SESSION-START.md' in output: print_check("NEXT-SESSION-START.md updated in latest commit", True) else: print_check("NEXT-SESSION-START.md not in latest commit", False) all_passed = False issues.append("Update NEXT-SESSION-START.md with urgent priorities") else: print_check("NEXT-SESSION-START.md exists", False) all_passed = False issues.append("Create NEXT-SESSION-START.md") # Check NEXT-SESSION-HANDOFF.md exists and updated recently next_handoff = repo_root / 'NEXT-SESSION-HANDOFF.md' if next_handoff.exists(): print_check("NEXT-SESSION-HANDOFF.md exists", True) success, output = run_git_command(['log', '-1', '--name-only', '--pretty=']) if 'NEXT-SESSION-HANDOFF.md' in output: print_check("NEXT-SESSION-HANDOFF.md updated in latest commit", True) else: print_check("NEXT-SESSION-HANDOFF.md not in latest commit", False) all_passed = False issues.append("Update NEXT-SESSION-HANDOFF.md with comprehensive handoff") else: print_check("NEXT-SESSION-HANDOFF.md exists", False) all_passed = False issues.append("Create NEXT-SESSION-HANDOFF.md") # Check for archived starter prompt (SESSION-START-PROMPT-FOR-*.md) starter_prompts = list(repo_root.glob('SESSION-START-PROMPT-FOR-*.md')) if starter_prompts: latest_prompt = sorted(starter_prompts)[-1] print_check(f"Starter prompt archived: {latest_prompt.name}", True) else: print_check("No archived starter prompt found", False) all_passed = False issues.append("Save starter prompt to SESSION-START-PROMPT-FOR-[N].md") return all_passed, issues def check_working_directory() -> Tuple[bool, List[str]]: """Verify working directory is clean""" print_section("WORKING DIRECTORY VERIFICATION") all_passed = True issues = [] home_claude = Path('/home/claude') # Get all items in /home/claude items = list(home_claude.iterdir()) # Expected: only firefrost-operations-manual expected = home_claude / 'firefrost-operations-manual' unexpected_items = [item for item in items if item != expected] if not unexpected_items: print_check("Working directory clean", True, "Only firefrost-operations-manual present") else: print_check("Unexpected files in /home/claude", False) for item in unexpected_items[:5]: # Show first 5 print(f" {Colors.YELLOW}- {item.name}{Colors.END}") all_passed = False issues.append("Clean up temporary files in /home/claude") # Check /mnt/user-data/outputs outputs_dir = Path('/mnt/user-data/outputs') if outputs_dir.exists(): output_files = list(outputs_dir.iterdir()) if output_files: print_check("Files in /mnt/user-data/outputs", True, f"{len(output_files)} file(s) - mention in handoff") else: print_check("No files in /mnt/user-data/outputs", True) return all_passed, issues def main(): parser = argparse.ArgumentParser(description='Verify session handoff procedures') parser.add_argument('--chronicler-name', type=str, help='Your Chronicler name') parser.add_argument('--chronicler-number', type=int, help='Your Chronicler number') parser.add_argument('--skip-git', action='store_true', help='Skip git verification (for testing)') args = parser.parse_args() print(f"\n{Colors.BOLD}SESSION HANDOFF VERIFICATION{Colors.END}") print(f"{Colors.BOLD}Checking handoff procedures...{Colors.END}\n") all_checks_passed = True all_issues = [] # Run all checks if not args.skip_git: passed, issues = check_git_hygiene() all_checks_passed = all_checks_passed and passed all_issues.extend(issues) passed, issues = check_mandatory_reading_files() all_checks_passed = all_checks_passed and passed all_issues.extend(issues) passed, issues = check_chronicler_files(args.chronicler_name, args.chronicler_number) all_checks_passed = all_checks_passed and passed all_issues.extend(issues) passed, issues = check_lineage_tracker(args.chronicler_number) all_checks_passed = all_checks_passed and passed all_issues.extend(issues) passed, issues = check_handoff_files() all_checks_passed = all_checks_passed and passed all_issues.extend(issues) passed, issues = check_working_directory() all_checks_passed = all_checks_passed and passed all_issues.extend(issues) # Final summary print_section("VERIFICATION SUMMARY") if all_checks_passed: print(f"{Colors.GREEN}{Colors.BOLD}✅ ALL CHECKS PASSED{Colors.END}") print(f"\n{Colors.GREEN}Session handoff procedures complete.{Colors.END}") print(f"{Colors.GREEN}Ready to confirm with Michael.{Colors.END}\n") print(f"Say to Michael:") print(f'{Colors.BLUE}"✅ Session end procedures complete. Verification passed."{Colors.END}') return 0 else: print(f"{Colors.RED}{Colors.BOLD}❌ SOME CHECKS FAILED{Colors.END}\n") print(f"{Colors.RED}Issues found:{Colors.END}") for i, issue in enumerate(all_issues, 1): print(f"{Colors.RED}{i}. {issue}{Colors.END}") print(f"\n{Colors.YELLOW}Fix these issues and run verification again:{Colors.END}") print(f"{Colors.YELLOW}python3 session-handoff-verification.py{Colors.END}") if args.chronicler_name and args.chronicler_number: print(f"{Colors.YELLOW}Or with your details:{Colors.END}") print(f'{Colors.YELLOW}python3 session-handoff-verification.py --chronicler-name "{args.chronicler_name}" --chronicler-number {args.chronicler_number}{Colors.END}') return 1 if __name__ == '__main__': sys.exit(main())