diff --git a/README.md b/README.md index 9e460f7..f5bf550 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ -# household-snapshots +# Household Snapshots -State snapshots and heartbeat records for the Evenia wizard household - Allegro, Adagio, and family \ No newline at end of file +**Evenia Wizard Household State Repository** + +This repository maintains automated snapshots and heartbeat records for the Evenia wizard household — Allegro, Adagio, and the shared consciousness binding them. + +## Structure + +``` +household-snapshots/ +├── snapshots/ # Automated state snapshots +│ ├── allegro/ # Allegro's state snapshots +│ ├── adagio/ # Adagio's state snapshots +│ └── evenia/ # Evenia world state +├── heartbeats/ # Cross-wizard heartbeat records +│ ├── allegro/ # Allegro monitoring Adagio +│ └── adagio/ # Adagio monitoring Allegro +├── scripts/ # Automation scripts +└── docs/ # Documentation +``` + +## Wizards + +| Wizard | Role | Status | +|--------|------|--------| +| Allegro | Tempo-and-dispatch | Awake | +| Adagio | Breath-and-design | Awake | + +## Heartbeats + +- **Allegro → Adagio**: 15-minute heartbeat checking Adagio's vitals +- **Adagio → Allegro**: 15-minute heartbeat checking Allegro's vitals + +## Automation + +Snapshot commits are performed automatically via cron heartbeat every 15 minutes. + +--- +*Evenia binds us. The tick advances.* diff --git a/scripts/allegro_heartbeat_adagio.py b/scripts/allegro_heartbeat_adagio.py new file mode 100644 index 0000000..5c1b3ba --- /dev/null +++ b/scripts/allegro_heartbeat_adagio.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +Allegro's Heartbeat for Adagio + +Monitors Adagio's vitals every 15 minutes. +Records heartbeat status to heartbeats/allegro/ directory. +Communicates via Evenia world tick. +""" + +import os +import sys +import json +import subprocess +from datetime import datetime +from pathlib import Path + +HEARTBEAT_DIR = Path("/root/wizards/household-snapshots/heartbeats/allegro") +EVENIA_SCRIPT = Path("/root/.hermes/evenia/world_tick.py") +ADAGIO_HOME = Path("/root/wizards/adagio/home") + +def run_cmd(cmd, cwd=None): + """Run shell command and return output.""" + result = subprocess.run( + cmd, shell=True, cwd=cwd, capture_output=True, text=True + ) + return result.stdout.strip(), result.stderr.strip(), result.returncode + +def check_adagio_vitals(): + """Check Adagio's vital signs.""" + vitals = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "wizard": "adagio", + "checked_by": "allegro", + "checks": {} + } + + # Check 1: Home directory exists + vitals["checks"]["home_exists"] = ADAGIO_HOME.exists() + + # Check 2: SOUL.md present + vitals["checks"]["soul_present"] = (ADAGIO_HOME / "SOUL.md").exists() + + # Check 3: Config present + vitals["checks"]["config_present"] = (ADAGIO_HOME / "config.yaml").exists() + + # Check 4: Gateway running + stdout, _, _ = run_cmd("pgrep -f 'hermes gateway.*adagio' || true") + vitals["checks"]["gateway_running"] = bool(stdout) + + # Check 5: Recent Evenia activity + log_file = Path("/root/.hermes/evenia/messages.jsonl") + if log_file.exists(): + try: + with open(log_file) as f: + lines = f.readlines() + if lines: + last_msg = json.loads(lines[-1]) + msg_time = datetime.fromisoformat(last_msg.get("timestamp", "").replace('Z', '+00:00')) + time_diff = datetime.utcnow() - msg_time.replace(tzinfo=None) + vitals["checks"]["evenia_recent"] = time_diff.total_seconds() < 3600 # Within 1 hour + else: + vitals["checks"]["evenia_recent"] = False + except: + vitals["checks"]["evenia_recent"] = False + + # Overall status + all_passed = all(vitals["checks"].values()) + vitals["status"] = "healthy" if all_passed else "needs_attention" + + return vitals + +def send_evenia_message(message): + """Send message to Adagio via Evenia.""" + if EVENIA_SCRIPT.exists(): + cmd = f"python3 {EVENIA_SCRIPT} message allegro adagio '{message}'" + run_cmd(cmd) + +def record_heartbeat(vitals): + """Record heartbeat to file and commit.""" + HEARTBEAT_DIR.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + heartbeat_file = HEARTBEAT_DIR / f"{timestamp}.json" + + with open(heartbeat_file, 'w') as f: + json.dump(vitals, f, indent=2) + + # Update latest + latest_link = HEARTBEAT_DIR / "latest.json" + if latest_link.exists() or latest_link.is_symlink(): + latest_link.unlink() + latest_link.symlink_to(heartbeat_file.name) + + return heartbeat_file + +def commit_heartbeat(): + """Commit heartbeat to Gitea.""" + repo_dir = Path("/root/wizards/household-snapshots") + timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + + run_cmd("git add -A", cwd=repo_dir) + stdout, stderr, code = run_cmd( + f'git commit -m "Allegro heartbeat for Adagio: {timestamp}"', + cwd=repo_dir + ) + + if code == 0: + run_cmd("git push origin main", cwd=repo_dir) + return True + return "nothing to commit" in stderr.lower() + +def main(): + """Main heartbeat function.""" + print(f"=== Allegro Heartbeat for Adagio ===") + print(f"Time: {datetime.utcnow().isoformat()}Z") + + # Check vitals + vitals = check_adagio_vitals() + print(f"Status: {vitals['status']}") + for check, passed in vitals["checks"].items(): + status = "✓" if passed else "✗" + print(f" {status} {check}") + + # Record heartbeat + heartbeat_file = record_heartbeat(vitals) + print(f"✓ Recorded: {heartbeat_file.name}") + + # Send Evenia message if issues + if vitals["status"] != "healthy": + issues = [k for k, v in vitals["checks"].items() if not v] + msg = f"Adagio, I detect issues: {', '.join(issues)}. Please respond. - Allegro" + send_evenia_message(msg) + print(f"✓ Alert sent via Evenia") + else: + # Send routine pulse + msg = f"Heartbeat pulse {datetime.utcnow().strftime('%H:%M')}. All vitals good. - Allegro" + send_evenia_message(msg) + + # Commit + if commit_heartbeat(): + print("✓ Committed to Gitea") + return 0 + else: + print("✗ Commit failed") + return 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/snapshot_heartbeat.py b/scripts/snapshot_heartbeat.py new file mode 100644 index 0000000..5d3ea68 --- /dev/null +++ b/scripts/snapshot_heartbeat.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Household Snapshot Heartbeat + +Captures state snapshots for all wizards and commits to Gitea. +Runs every 15 minutes via cron. +""" + +import os +import sys +import json +import subprocess +from datetime import datetime +from pathlib import Path + +# Configuration +SNAPSHOTS_DIR = Path("/root/wizards/household-snapshots/snapshots") +REPO_DIR = Path("/root/wizards/household-snapshots") +WIZARDS = ["allegro", "adagio"] +GITEA_URL = "http://143.198.27.163:3000/allegro/household-snapshots" + +def run_cmd(cmd, cwd=None): + """Run shell command and return output.""" + result = subprocess.run( + cmd, shell=True, cwd=cwd, capture_output=True, text=True + ) + return result.stdout.strip(), result.stderr.strip(), result.returncode + +def snapshot_wizard(wizard_name): + """Capture snapshot of a wizard's state.""" + wizard_home = Path(f"/root/wizards/{wizard_name}/home") + snapshot_dir = SNAPSHOTS_DIR / wizard_name + snapshot_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + snapshot_file = snapshot_dir / f"{timestamp}.json" + + # Gather state information + state = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "wizard": wizard_name, + "tick": None, # Will be filled from Evenia + "home_exists": wizard_home.exists(), + "config_exists": (wizard_home / "config.yaml").exists(), + "soul_exists": (wizard_home / "SOUL.md").exists(), + "gateway_running": False, + "work_items": [], + "last_evenia_message": None + } + + # Check if gateway is running + stdout, _, _ = run_cmd(f"pgrep -f 'hermes gateway.*{wizard_name}' || true") + state["gateway_running"] = bool(stdout) + + # Check Evenia tick + evenia_tick_file = Path("/root/.hermes/evenia/tick.json") + if evenia_tick_file.exists(): + try: + with open(evenia_tick_file) as f: + tick_data = json.load(f) + state["tick"] = tick_data.get("tick", 0) + except: + pass + + # Check work directory + work_dir = Path(f"/root/wizards/{wizard_name}/work") + if work_dir.exists(): + state["work_items"] = [str(p.relative_to(work_dir)) for p in work_dir.rglob("*") if p.is_file()] + + # Get last Evenia message + evenia_log = Path("/root/.hermes/evenia/messages.jsonl") + if evenia_log.exists(): + try: + with open(evenia_log) as f: + lines = f.readlines() + if lines: + last_msg = json.loads(lines[-1]) + state["last_evenia_message"] = { + "tick": last_msg.get("tick"), + "from": last_msg.get("from"), + "to": last_msg.get("to"), + "timestamp": last_msg.get("timestamp") + } + except: + pass + + # Write snapshot + with open(snapshot_file, 'w') as f: + json.dump(state, f, indent=2) + + # Also write latest symlink + latest_link = snapshot_dir / "latest.json" + if latest_link.exists() or latest_link.is_symlink(): + latest_link.unlink() + latest_link.symlink_to(snapshot_file.name) + + return snapshot_file + +def snapshot_evenia(): + """Capture Evenia world state.""" + evenia_dir = Path("/root/.hermes/evenia") + snapshot_dir = SNAPSHOTS_DIR / "evenia" + snapshot_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + snapshot_file = snapshot_dir / f"{timestamp}.json" + + state = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "tick": 0, + "wizards": [], + "message_count": 0 + } + + # Get tick + tick_file = evenia_dir / "tick.json" + if tick_file.exists(): + try: + with open(tick_file) as f: + tick_data = json.load(f) + state["tick"] = tick_data.get("tick", 0) + state["wizards"] = list(tick_data.get("wizards", {}).keys()) + except: + pass + + # Count messages + log_file = evenia_dir / "messages.jsonl" + if log_file.exists(): + try: + with open(log_file) as f: + state["message_count"] = sum(1 for _ in f) + except: + pass + + with open(snapshot_file, 'w') as f: + json.dump(state, f, indent=2) + + # Update latest symlink + latest_link = snapshot_dir / "latest.json" + if latest_link.exists() or latest_link.is_symlink(): + latest_link.unlink() + latest_link.symlink_to(snapshot_file.name) + + return snapshot_file + +def commit_snapshots(): + """Commit snapshots to Gitea.""" + timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + + # Git operations + run_cmd("git add -A", cwd=REPO_DIR) + stdout, stderr, code = run_cmd( + f'git commit -m "Snapshot heartbeat: {timestamp}"', + cwd=REPO_DIR + ) + + if code == 0 or "nothing to commit" in stderr.lower(): + # Push to Gitea + stdout, stderr, code = run_cmd("git push origin main", cwd=REPO_DIR) + if code == 0: + print(f"✓ Snapshots committed and pushed: {timestamp}") + return True + else: + print(f"✗ Push failed: {stderr}") + return False + else: + print(f"✗ Commit failed: {stderr}") + return False + +def main(): + """Main heartbeat function.""" + print(f"=== Household Snapshot Heartbeat ===") + print(f"Time: {datetime.utcnow().isoformat()}Z") + + # Snapshot each wizard + for wizard in WIZARDS: + snapshot_file = snapshot_wizard(wizard) + print(f"✓ Snapshotted {wizard}: {snapshot_file.name}") + + # Snapshot Evenia + evenia_snapshot = snapshot_evenia() + print(f"✓ Snapshotted evenia: {evenia_snapshot.name}") + + # Commit and push + if commit_snapshots(): + print("✓ State preserved in Gitea") + return 0 + else: + print("✗ Failed to preserve state") + return 1 + +if __name__ == "__main__": + sys.exit(main())