- Updated session-handoff-verification.py to check mandatory reading files - Created SESSION-START-PROMPT-TEMPLATE.md with enforced reading order - Documented MANDATORY-READING-ORDER.md (Joining → Essence → Index → Handoff → Tasks) Reason: Chronicler #23 (The Diagnostician) had tunnel vision because they didn't know DEPLOYMENT-PLAN-PART-1/2.md existed. DOCUMENT-INDEX.md shows the full landscape before decision-making. Chronicler #24
420 lines
16 KiB
Python
Executable File
420 lines
16 KiB
Python
Executable File
#!/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())
|