claude_bootstrap.py
1 #!/usr/bin/env python3 2 """ 3 Claude Bootstrap - Single command for full protocol initialization. 4 5 This script outputs everything a Claude instance needs to bootstrap into 6 the Sovereign OS protocol. Run at session start instead of manually 7 reading multiple files. 8 9 Usage: 10 python3 scripts/claude_bootstrap.py 11 12 What it does: 13 1. Runs hygiene check 14 2. Verifies/starts mesh daemon 15 3. Reads FO-STATE.json (persistent state) 16 4. Reads LIVE-COMPRESSION.md (session state) 17 5. Checks Hypercore daemon status 18 6. Outputs formatted context for Claude 19 20 The output is designed for Claude consumption - structured, concise, 21 and includes the startup banner. 22 """ 23 24 import json 25 import os 26 import sys 27 import subprocess 28 import urllib.request 29 import urllib.error 30 from datetime import datetime, timedelta 31 from pathlib import Path 32 33 # Configuration 34 SOVEREIGN_OS_ROOT = Path(__file__).parent.parent 35 SESSIONS_DIR = SOVEREIGN_OS_ROOT / "sessions" 36 FO_STATE_PATH = SESSIONS_DIR / "FO-STATE.json" 37 LIVE_COMPRESSION_PATH = SESSIONS_DIR / "LIVE-COMPRESSION.md" 38 INSIGHT_BACKLOG_PATH = SESSIONS_DIR / "INSIGHT-BACKLOG.md" 39 DEBRIEF_LEDGER_PATH = Path.home() / ".sovereign" / "debrief-ledger.json" 40 41 HYPERCORE_URL = "http://localhost:7777" 42 MESH_URL = "http://localhost:7778" 43 44 # Required ports 45 REQUIRED_PORTS = { 46 7777: "Hypercore daemon", 47 7778: "Mesh daemon (sovereign-mesh.js)" 48 } 49 50 51 def check_port_conflicts() -> dict: 52 """ 53 Check if required ports are in use by other processes. 54 55 Returns dict with port status and any conflicts detected. 56 """ 57 conflicts = {} 58 59 for port, service in REQUIRED_PORTS.items(): 60 try: 61 # Use lsof to check what's using the port 62 result = subprocess.run( 63 ["lsof", "-i", f":{port}", "-t"], 64 capture_output=True, text=True, timeout=5 65 ) 66 67 if result.returncode == 0 and result.stdout.strip(): 68 pids = result.stdout.strip().split('\n') 69 70 # Get process info for each PID 71 for pid in pids: 72 try: 73 ps_result = subprocess.run( 74 ["ps", "-p", pid, "-o", "comm="], 75 capture_output=True, text=True, timeout=2 76 ) 77 process_name = ps_result.stdout.strip() if ps_result.returncode == 0 else "unknown" 78 79 # Check if it's our expected service 80 is_expected = ( 81 (port == 7777 and "node" in process_name.lower()) or 82 (port == 7778 and "node" in process_name.lower()) 83 ) 84 85 if not is_expected: 86 if port not in conflicts: 87 conflicts[port] = [] 88 conflicts[port].append({ 89 "pid": pid, 90 "process": process_name, 91 "service": service 92 }) 93 except: 94 pass 95 except: 96 pass 97 98 return { 99 "has_conflicts": len(conflicts) > 0, 100 "conflicts": conflicts 101 } 102 103 104 def check_debrief_status() -> dict: 105 """ 106 Check when last debrief was run and if there's work without debrief. 107 108 Returns dict with: 109 - last_debrief: datetime or None 110 - commits_since: number of commits since last debrief 111 - hours_since: hours since last debrief 112 - violation: True if work done without debrief 113 """ 114 result = { 115 "last_debrief": None, 116 "commits_since": 0, 117 "hours_since": None, 118 "violation": False, 119 "last_commit_debriefed": None 120 } 121 122 # Load debrief ledger 123 if DEBRIEF_LEDGER_PATH.exists(): 124 try: 125 with open(DEBRIEF_LEDGER_PATH) as f: 126 ledger = json.load(f) 127 if ledger: 128 last_entry = ledger[-1] 129 result["last_debrief"] = datetime.fromisoformat(last_entry["timestamp"]) 130 result["last_commit_debriefed"] = last_entry.get("latest_commit") 131 result["hours_since"] = (datetime.now() - result["last_debrief"]).total_seconds() / 3600 132 except: 133 pass 134 135 # Check commits since last debrief 136 if result["last_commit_debriefed"]: 137 try: 138 cmd = ["git", "rev-list", "--count", f"{result['last_commit_debriefed']}..HEAD"] 139 proc = subprocess.run(cmd, capture_output=True, text=True, cwd=SOVEREIGN_OS_ROOT) 140 if proc.returncode == 0: 141 result["commits_since"] = int(proc.stdout.strip()) 142 except: 143 pass 144 else: 145 # No debrief record, check total commits today 146 try: 147 today = datetime.now().strftime("%Y-%m-%d") 148 cmd = ["git", "rev-list", "--count", "--since", today, "HEAD"] 149 proc = subprocess.run(cmd, capture_output=True, text=True, cwd=SOVEREIGN_OS_ROOT) 150 if proc.returncode == 0: 151 result["commits_since"] = int(proc.stdout.strip()) 152 except: 153 pass 154 155 # Violation = commits since last debrief > 0 156 if result["commits_since"] > 0: 157 result["violation"] = True 158 159 return result 160 161 162 def check_hypercore() -> dict: 163 """Check Hypercore daemon status.""" 164 try: 165 with urllib.request.urlopen(f"{HYPERCORE_URL}/status", timeout=2) as resp: 166 data = json.loads(resp.read().decode()) 167 return { 168 "online": True, 169 "peers": data.get("peers", 0), 170 "machine": data.get("machine", "unknown") 171 } 172 except: 173 return {"online": False, "peers": 0, "machine": None} 174 175 176 def check_mesh() -> dict: 177 """Check mesh daemon status.""" 178 try: 179 with urllib.request.urlopen(f"{MESH_URL}/", timeout=2) as resp: 180 data = json.loads(resp.read().decode()) 181 return { 182 "online": True, 183 "node": data.get("node", "unknown"), 184 "peers": data.get("peers", 0) 185 } 186 except: 187 return {"online": False, "node": None, "peers": 0} 188 189 190 def start_mesh() -> bool: 191 """Start mesh daemon if not running.""" 192 try: 193 keet_cli = SOVEREIGN_OS_ROOT / "keet-cli" 194 subprocess.Popen( 195 ["node", "sovereign-mesh.js", "--http", "--name", "claude-session"], 196 cwd=str(keet_cli), 197 stdout=subprocess.DEVNULL, 198 stderr=subprocess.DEVNULL, 199 start_new_session=True 200 ) 201 import time 202 time.sleep(2) 203 return check_mesh()["online"] 204 except: 205 return False 206 207 208 def run_hygiene() -> dict: 209 """Run hygiene check and return results.""" 210 try: 211 result = subprocess.run( 212 [sys.executable, str(SOVEREIGN_OS_ROOT / "scripts" / "phoenix_hygiene.py")], 213 capture_output=True, text=True, timeout=30 214 ) 215 if result.returncode == 0: 216 return {"status": "pass", "message": "All checks pass"} 217 else: 218 return {"status": "issues", "message": result.stdout.strip() or result.stderr.strip()} 219 except Exception as e: 220 return {"status": "error", "message": str(e)} 221 222 223 def check_enforcement_status() -> dict: 224 """Check protocol enforcement status.""" 225 try: 226 sys.path.insert(0, str(SOVEREIGN_OS_ROOT / "core" / "metacog")) 227 from protocol_enforcer import ProtocolEnforcer 228 229 enforcer = ProtocolEnforcer() 230 report = enforcer.audit_all() 231 232 return { 233 "score": f"{report.enforcement_score:.0%}", 234 "gaps": report.total_gaps, 235 "critical": len(report.critical_gaps), 236 "status": "good" if report.enforcement_score >= 0.8 else "needs_work" 237 } 238 except Exception as e: 239 return {"status": "error", "message": str(e)} 240 241 242 def get_graph_status() -> dict: 243 """Get graph stats for bootstrap display.""" 244 try: 245 sys.path.insert(0, str(SOVEREIGN_OS_ROOT / "core" / "graph")) 246 from reality_surface import RealitySurface 247 248 surface = RealitySurface() 249 return { 250 "nodes": len(surface.graph["nodes"]), 251 "edges": len(surface.graph["edges"]), 252 "status": "loaded" 253 } 254 except Exception as e: 255 return {"nodes": 0, "edges": 0, "status": "error"} 256 257 258 def check_ssh_status() -> dict: 259 """Check SSH key and node connectivity status.""" 260 try: 261 sys.path.insert(0, str(SOVEREIGN_OS_ROOT / "scripts")) 262 import ssh_setup 263 264 status = ssh_setup.check_status() 265 266 # Summarize for bootstrap 267 has_keys = len(status["keys"]) > 0 268 nodes_ok = all(n["key_auth"] for n in status["nodes"].values()) 269 nodes_reachable = [ 270 name for name, s in status["nodes"].items() 271 if s["reachable"] and not s["key_auth"] 272 ] 273 274 return { 275 "has_keys": has_keys, 276 "nodes_ok": nodes_ok, 277 "needs_auth": nodes_reachable, 278 "status": "ok" if (has_keys and nodes_ok) else "needs_work" 279 } 280 except Exception as e: 281 return {"status": "error", "error": str(e)} 282 283 284 def check_commit_hygiene() -> dict: 285 """Check for uncommitted work (A4 - ruin exposure).""" 286 try: 287 # Get uncommitted files 288 result = subprocess.run( 289 ["git", "status", "--porcelain"], 290 capture_output=True, text=True, 291 cwd=SOVEREIGN_OS_ROOT, timeout=10 292 ) 293 294 files = [line for line in result.stdout.strip().split("\n") if line] 295 296 # Categorize 297 critical = [] 298 batchable = [] 299 300 for line in files: 301 if not line: 302 continue 303 filepath = line[3:] 304 305 # Skip ignored patterns 306 if "__pycache__" in filepath or "RESONANCE-ALERTS" in filepath: 307 continue 308 309 # Critical paths (code) 310 if any(filepath.startswith(p) for p in ["core/", "scripts/", "hooks/", "keet-cli/", "docs/"]): 311 if not filepath.startswith("sessions/"): 312 critical.append(filepath) 313 else: 314 batchable.append(filepath) 315 316 # Determine status 317 if len(critical) > 10: 318 return { 319 "status": "violation", 320 "critical": len(critical), 321 "batchable": len(batchable), 322 "message": f"⚠️ {len(critical)} code files uncommitted - COMMIT REQUIRED" 323 } 324 elif len(critical) > 0: 325 return { 326 "status": "warning", 327 "critical": len(critical), 328 "batchable": len(batchable), 329 "message": f"{len(critical)} code files pending" 330 } 331 else: 332 return { 333 "status": "ok", 334 "critical": 0, 335 "batchable": len(batchable), 336 "message": "✓ Code committed" 337 } 338 except Exception as e: 339 return {"status": "error", "error": str(e)} 340 341 342 def load_fo_state() -> dict: 343 """Load First Officer state.""" 344 try: 345 if FO_STATE_PATH.exists(): 346 with open(FO_STATE_PATH) as f: 347 return json.load(f) 348 except: 349 pass 350 return {} 351 352 353 def load_live_compression() -> dict: 354 """Load and parse LIVE-COMPRESSION.md.""" 355 result = { 356 "updated": None, 357 "status": None, 358 "session_id": None, 359 "free_energy": None, 360 "focus": None, 361 "gravity_wells": [], 362 "insights": [] 363 } 364 365 try: 366 if LIVE_COMPRESSION_PATH.exists(): 367 content = LIVE_COMPRESSION_PATH.read_text() 368 369 for line in content.split("\n"): 370 if "updated::" in line: 371 result["updated"] = line.split("::", 1)[1].strip() 372 elif "status::" in line: 373 result["status"] = line.split("::", 1)[1].strip() 374 elif "session_id::" in line: 375 result["session_id"] = line.split("::", 1)[1].strip() 376 elif "free_energy::" in line: 377 result["free_energy"] = line.split("::", 1)[1].strip() 378 elif "## Current Focus" in line: 379 # Get next non-empty line as focus 380 idx = content.find(line) 381 rest = content[idx:].split("\n") 382 for l in rest[1:5]: 383 if l.strip() and not l.startswith("#") and not l.startswith("-"): 384 result["focus"] = l.strip() 385 break 386 elif "[[" in line and "]]" in line and "strength::" not in line: 387 import re 388 match = re.search(r'\[\[([^\]]+)\]\]', line) 389 if match and match.group(1) not in result["gravity_wells"]: 390 result["gravity_wells"].append(match.group(1)) 391 except: 392 pass 393 394 return result 395 396 397 def get_high_priority_backlog() -> list: 398 """Get unresolved high-priority items from backlog.""" 399 items = [] 400 try: 401 if INSIGHT_BACKLOG_PATH.exists(): 402 content = INSIGHT_BACKLOG_PATH.read_text() 403 in_high_priority = False 404 405 for line in content.split("\n"): 406 if "## High Priority" in line: 407 in_high_priority = True 408 elif line.startswith("## ") and in_high_priority: 409 break 410 elif in_high_priority and line.startswith("- [ ]"): 411 # Unresolved high priority item 412 item = line.replace("- [ ]", "").strip() 413 if item: 414 items.append(item) 415 except: 416 pass 417 418 return items 419 420 421 def format_output(hygiene: dict, hypercore: dict, mesh: dict, 422 fo_state: dict, live_compression: dict, backlog: list, 423 debrief: dict, enforcement: dict = None, graph_status: dict = None, 424 port_conflicts: dict = None, ssh_status: dict = None, 425 commit_hygiene: dict = None) -> str: 426 """Format bootstrap output for Claude.""" 427 428 lines = [] 429 430 # Startup banner 431 lines.append("```") 432 lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") 433 lines.append("SOVEREIGN OS ACTIVE") 434 lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") 435 lines.append("```") 436 lines.append("") 437 438 # System status table 439 lines.append("## System Status") 440 lines.append("") 441 lines.append("| Component | Status |") 442 lines.append("|-----------|--------|") 443 444 # Hygiene 445 if hygiene["status"] == "pass": 446 lines.append("| Hygiene | ✓ All checks pass |") 447 else: 448 lines.append(f"| Hygiene | ⚠ {hygiene['message'][:40]} |") 449 450 # Hypercore 451 if hypercore["online"]: 452 lines.append(f"| Hypercore (:7777) | ✓ {hypercore['peers']} peer(s) |") 453 else: 454 lines.append("| Hypercore (:7777) | ✗ Offline |") 455 456 # Mesh 457 if mesh["online"]: 458 lines.append(f"| Mesh (:7778) | ✓ {mesh['peers']} peer(s) |") 459 else: 460 lines.append("| Mesh (:7778) | ✗ Offline |") 461 462 # Enforcement (Belt + Suspenders) 463 if enforcement: 464 if enforcement.get("status") == "good": 465 lines.append(f"| Enforcement | ✓ {enforcement['score']} |") 466 elif enforcement.get("status") == "needs_work": 467 lines.append(f"| Enforcement | ⚠ {enforcement['score']} ({enforcement['critical']} critical gaps) |") 468 else: 469 lines.append("| Enforcement | ? Could not check |") 470 471 # Graph (Reality Surface) 472 if graph_status: 473 if graph_status.get("status") == "loaded": 474 lines.append(f"| Graph | ✓ {graph_status['nodes']} nodes, {graph_status['edges']} edges |") 475 else: 476 lines.append("| Graph | ? Could not load |") 477 478 # SSH Status 479 if ssh_status: 480 if ssh_status.get("status") == "ok": 481 lines.append("| SSH | ✓ Keys configured |") 482 elif ssh_status.get("needs_auth"): 483 nodes = ", ".join(ssh_status["needs_auth"]) 484 lines.append(f"| SSH | ⚠ Key not authorized on: {nodes} |") 485 486 # Commit Hygiene (A4 - uncommitted work is ruin exposure) 487 if commit_hygiene: 488 if commit_hygiene.get("status") == "violation": 489 lines.append(f"| **Git** | **⚠️ {commit_hygiene['critical']} uncommitted code files - COMMIT NOW** |") 490 elif commit_hygiene.get("status") == "warning": 491 lines.append(f"| Git | ⚠ {commit_hygiene['critical']} code files pending |") 492 elif commit_hygiene.get("status") == "ok": 493 lines.append("| Git | ✓ Code committed |") 494 495 lines.append("") 496 497 # ================================================================= 498 # PORT CONFLICT DETECTION 499 # ================================================================= 500 if port_conflicts and port_conflicts.get("has_conflicts"): 501 lines.append("## ⚠️ PORT CONFLICTS DETECTED") 502 lines.append("") 503 lines.append("Another process is using required ports:") 504 lines.append("") 505 506 for port, procs in port_conflicts["conflicts"].items(): 507 for proc in procs: 508 lines.append(f"- **Port {port}** ({proc['service']}): PID {proc['pid']} ({proc['process']})") 509 510 lines.append("") 511 lines.append("**To resolve:**") 512 lines.append("```bash") 513 514 for port, procs in port_conflicts["conflicts"].items(): 515 for proc in procs: 516 lines.append(f"# Kill process using port {port}:") 517 lines.append(f"kill {proc['pid']}") 518 519 lines.append("```") 520 lines.append("") 521 522 # ================================================================= 523 # COMMIT HYGIENE ENFORCEMENT (A4 - ruin exposure) 524 # ================================================================= 525 if commit_hygiene and commit_hygiene.get("status") == "violation": 526 lines.append("## ⚠️ COMMIT REQUIRED - Uncommitted Code") 527 lines.append("") 528 lines.append(f"**{commit_hygiene['critical']} code files need to be committed**") 529 lines.append("") 530 lines.append("Uncommitted work is ruin exposure (A4). If this context compresses,") 531 lines.append("the work is LOST. Commit before proceeding.") 532 lines.append("") 533 lines.append("```bash") 534 lines.append("# Check what needs committing:") 535 lines.append("python3 scripts/commit_hygiene.py") 536 lines.append("") 537 lines.append("# Stage and commit:") 538 lines.append("git add core/ scripts/ hooks/") 539 lines.append('git commit -m "type(scope): description"') 540 lines.append("```") 541 lines.append("") 542 543 # ================================================================= 544 # DEBRIEF ENFORCEMENT (Belt + Suspenders) 545 # ================================================================= 546 if debrief["violation"]: 547 lines.append("## ⚠️ DEBRIEF REQUIRED - Protocol Violation") 548 lines.append("") 549 lines.append(f"**{debrief['commits_since']} commit(s) since last debrief**") 550 if debrief["hours_since"]: 551 lines.append(f"Last debrief: {debrief['hours_since']:.1f} hours ago") 552 else: 553 lines.append("Last debrief: Never recorded") 554 lines.append("") 555 lines.append("```bash") 556 lines.append("# Run this NOW before starting new work:") 557 lines.append("python3 scripts/session_report.py") 558 lines.append("```") 559 lines.append("") 560 lines.append("Work without debrief = value not measured = protocol violation.") 561 lines.append("") 562 else: 563 # Show last debrief time even if no violation 564 if debrief["last_debrief"]: 565 lines.append(f"**Last debrief:** {debrief['hours_since']:.1f} hours ago ✓") 566 lines.append("") 567 568 # Session continuity 569 lines.append("## Session Continuity") 570 lines.append("") 571 572 if live_compression["session_id"]: 573 lines.append(f"**Continuing from:** `{live_compression['session_id']}`") 574 if live_compression["updated"]: 575 lines.append(f"**Last update:** {live_compression['updated']}") 576 if live_compression["status"]: 577 lines.append(f"**Status:** {live_compression['status']}") 578 if live_compression["free_energy"]: 579 lines.append(f"**Free Energy:** {live_compression['free_energy']}") 580 581 if live_compression["focus"]: 582 lines.append(f"\n**Last focus:** {live_compression['focus']}") 583 584 if live_compression["gravity_wells"]: 585 wells = ", ".join(live_compression["gravity_wells"][:5]) 586 lines.append(f"\n**Active gravity wells:** {wells}") 587 588 lines.append("") 589 590 # Economics summary 591 costs = fo_state.get("costs", {}) 592 if costs: 593 lines.append("## Economics (Bitcoin-Anchored)") 594 lines.append("") 595 sats = costs.get("sats", {}) 596 efficiency = costs.get("efficiency", {}) 597 598 sub_sats = sats.get("subscription_sats", 0) 599 api_eff = efficiency.get("api_efficiency", 0) 600 infra_eff = efficiency.get("infra_efficiency", 0) 601 602 lines.append(f"**Subscription:** {sub_sats:,} sats | **Efficiency:** {api_eff}x API, {infra_eff}x infra") 603 lines.append("") 604 605 # High priority backlog items 606 if backlog: 607 lines.append("## ⚠ High Priority Backlog (Unresolved)") 608 lines.append("") 609 for item in backlog[:3]: 610 lines.append(f"- {item[:80]}") 611 lines.append("") 612 613 # Quick reference 614 lines.append("## Protocol Commands") 615 lines.append("") 616 lines.append("| When | Command | Purpose |") 617 lines.append("|------|---------|---------|") 618 lines.append("| **After ANY work** | `python3 scripts/session_report.py` | Financial debrief (MANDATORY) |") 619 lines.append("| **Before work** | `python3 scripts/query_graph.py --preflight 'topic'` | What graph knows |") 620 lines.append("| **During work** | `python3 scripts/query_graph.py 'query'` | Position check |") 621 lines.append("| Track done item | `--add-done \"description\"` | Log completed work |") 622 lines.append("| Track insight | `--add-insight \"insight\" A2` | Log wisdom captured |") 623 lines.append("") 624 625 # Resurrection seed location 626 lines.append("**State files:**") 627 lines.append("- LIVE-COMPRESSION: `sessions/LIVE-COMPRESSION.md`") 628 lines.append("- FO-STATE: `sessions/FO-STATE.json`") 629 lines.append("- INSIGHT-BACKLOG: `sessions/INSIGHT-BACKLOG.md`") 630 lines.append("") 631 lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") 632 633 return "\n".join(lines) 634 635 636 def main(): 637 """Main bootstrap entry point.""" 638 639 # 1. Run hygiene check 640 hygiene = run_hygiene() 641 642 # 2. Check for port conflicts BEFORE checking services 643 port_conflicts = check_port_conflicts() 644 645 # 3. Check Hypercore daemon 646 hypercore = check_hypercore() 647 648 # 4. Check/start mesh 649 mesh = check_mesh() 650 if not mesh["online"]: 651 # Only try to start if there's no port conflict 652 if not port_conflicts.get("has_conflicts") or 7778 not in port_conflicts.get("conflicts", {}): 653 if start_mesh(): 654 mesh = check_mesh() 655 656 # 5. Load FO state 657 fo_state = load_fo_state() 658 659 # 6. Load LIVE-COMPRESSION 660 live_compression = load_live_compression() 661 662 # 7. Check backlog 663 backlog = get_high_priority_backlog() 664 665 # 8. Check debrief status (ENFORCEMENT) 666 debrief = check_debrief_status() 667 668 # 9. Check protocol enforcement (Belt + Suspenders) 669 enforcement = check_enforcement_status() 670 671 # 10. Get graph status (Reality Surface) 672 graph_status = get_graph_status() 673 674 # 11. Check SSH status 675 ssh_status = check_ssh_status() 676 677 # 12. Check commit hygiene (A4 - uncommitted work is ruin exposure) 678 commit_hygiene = check_commit_hygiene() 679 680 # 13. Output formatted context 681 output = format_output( 682 hygiene, hypercore, mesh, fo_state, live_compression, 683 backlog, debrief, enforcement, graph_status, port_conflicts, ssh_status, 684 commit_hygiene 685 ) 686 print(output) 687 688 # Return exit code based on critical issues 689 if hygiene["status"] == "error": 690 sys.exit(1) 691 if not hypercore["online"] and not mesh["online"]: 692 sys.exit(1) 693 694 sys.exit(0) 695 696 697 if __name__ == "__main__": 698 main()