From 7afc878b3b25ccdc50a2c2ea24727fc1138fdceb Mon Sep 17 00:00:00 2001 From: Symbiont Date: Thu, 19 Mar 2026 20:03:21 +0000 Subject: [PATCH] Heartbeat: auto-detect and commit skill changes Now watches /data/skills/ for changes on every heartbeat tick. Commits and re-packages automatically. Co-Authored-By: Claude Opus 4.6 --- heartbeat.jsonl | 5 +++++ symbiont/heartbeat.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/heartbeat.jsonl b/heartbeat.jsonl index 11e165e..799a6a9 100644 --- a/heartbeat.jsonl +++ b/heartbeat.jsonl @@ -1,2 +1,7 @@ {"timestamp": "2026-03-19T19:41:19.517373", "claude_cli": {"status": "ok", "detail": "authenticated"}, "disk": {"status": "ok", "total": "915G", "used": "20G", "available": "849G", "use_pct": "3%"}, "api_server": {"status": "ok", "detail": "active"}, "ledger": {"calls_today": 4, "cost_today": 0.062}, "queue": {"processed": 0}, "health": "healthy"} {"timestamp": "2026-03-19T19:41:38.652515", "claude_cli": {"status": "ok", "detail": "authenticated"}, "disk": {"status": "ok", "total": "915G", "used": "20G", "available": "849G", "use_pct": "3%"}, "api_server": {"status": "ok", "detail": "active"}, "ledger": {"calls_today": 4, "cost_today": 0.062}, "queue": {"processed": 0}, "health": "healthy"} +{"timestamp": "2026-03-19T19:46:23.559272", "claude_cli": {"status": "ok", "detail": "authenticated"}, "disk": {"status": "ok", "total": "915G", "used": "20G", "available": "849G", "use_pct": "3%"}, "api_server": {"status": "ok", "detail": "active"}, "ledger": {"calls_today": 4, "cost_today": 0.062}, "queue": {"processed": 0}, "health": "healthy"} +{"timestamp": "2026-03-19T19:51:43.565628", "claude_cli": {"status": "ok", "detail": "authenticated"}, "disk": {"status": "ok", "total": "915G", "used": "20G", "available": "849G", "use_pct": "3%"}, "api_server": {"status": "ok", "detail": "active"}, "ledger": {"calls_today": 4, "cost_today": 0.062}, "queue": {"processed": 0}, "health": "healthy"} +{"timestamp": "2026-03-19T19:56:48.576386", "claude_cli": {"status": "ok", "detail": "authenticated"}, "disk": {"status": "ok", "total": "915G", "used": "20G", "available": "849G", "use_pct": "3%"}, "api_server": {"status": "ok", "detail": "active"}, "ledger": {"calls_today": 4, "cost_today": 0.062}, "queue": {"processed": 0}, "health": "healthy"} +{"timestamp": "2026-03-19T20:02:01.418535", "claude_cli": {"status": "ok", "detail": "authenticated"}, "disk": {"status": "ok", "total": "915G", "used": "20G", "available": "849G", "use_pct": "3%"}, "api_server": {"status": "ok", "detail": "active"}, "ledger": {"calls_today": 4, "cost_today": 0.062}, "queue": {"processed": 0}, "health": "healthy"} +{"timestamp": "2026-03-19T20:03:15.583444", "claude_cli": {"status": "ok", "detail": "authenticated"}, "disk": {"status": "ok", "total": "915G", "used": "20G", "available": "849G", "use_pct": "3%"}, "api_server": {"status": "ok", "detail": "active"}, "ledger": {"calls_today": 4, "cost_today": 0.062}, "queue": {"processed": 0}, "skills": {"status": "clean", "changes": 0}, "health": "healthy"} diff --git a/symbiont/heartbeat.py b/symbiont/heartbeat.py index 50c0e69..2efa6a2 100644 --- a/symbiont/heartbeat.py +++ b/symbiont/heartbeat.py @@ -6,6 +6,7 @@ Run by systemd timer every 5 minutes. This is Symbiont's autonomic nervous syste - Process any pending tasks in the queue - Check rate limit status and clear expired limits - Log a heartbeat to the ledger for uptime tracking +- Auto-detect skill changes, commit them, and re-package - Basic self-diagnostics """ @@ -129,6 +130,44 @@ def process_queue(): return {"error": str(e)} +def check_skills(): + """Detect skill changes, commit to git, and re-package.""" + try: + skills_dir = Path("/data/skills") + if not skills_dir.exists(): + return {"status": "skipped", "reason": "skills dir not found"} + + # Check if there are any uncommitted changes in /data/skills + result = subprocess.run( + ["git", "status", "--porcelain"], + cwd=skills_dir, capture_output=True, text=True, timeout=10 + ) + changed_files = result.stdout.strip() + + if not changed_files: + return {"status": "clean", "changes": 0} + + # Count changed skills + change_count = len(changed_files.splitlines()) + + # Commit changes + subprocess.run(["git", "add", "-A"], cwd=skills_dir, capture_output=True, timeout=10) + subprocess.run( + ["git", "commit", "-m", + f"Auto-commit: {change_count} skill file(s) updated by heartbeat\n\nCo-Authored-By: Claude Opus 4.6 "], + cwd=skills_dir, capture_output=True, timeout=10 + ) + + # Re-package all skills + package_script = skills_dir / "package_all.sh" + if package_script.exists(): + subprocess.run(["bash", str(package_script)], cwd=skills_dir, capture_output=True, timeout=60) + + return {"status": "committed", "changes": change_count} + except Exception as e: + return {"status": "error", "detail": str(e)} + + def run_heartbeat(): """Run all checks and log the heartbeat.""" logger.info("Heartbeat starting") @@ -140,6 +179,7 @@ def run_heartbeat(): "api_server": check_api_server(), "ledger": get_ledger_stats(), "queue": process_queue(), + "skills": check_skills(), } # Determine overall health @@ -159,6 +199,7 @@ def run_heartbeat(): logger.info(f"Health: {heartbeat['health']} | " f"CLI: {heartbeat['claude_cli']['status']} | " f"API: {heartbeat['api_server']['status']} | " + f"Skills: {heartbeat['skills']['status']} | " f"Queue processed: {heartbeat['queue'].get('processed', 0)} | " f"Today's calls: {heartbeat['ledger']['calls_today']} " f"(${heartbeat['ledger']['cost_today']})")