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 <noreply@anthropic.com>
This commit is contained in:
Symbiont 2026-03-19 20:03:21 +00:00
parent dd91089be0
commit 7afc878b3b
2 changed files with 46 additions and 0 deletions

View File

@ -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"}

View File

@ -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 <noreply@anthropic.com>"],
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']})")