Files
rune/shared/auto_detect.py
T

236 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""auto_detect.py — Auto-detection layer for umbrella updates.
Watches for file changes and automatically triggers updates to
all dependent components across all umbrellas.
No manual trigger needed. Build something → system detects what
changed → system knows what depends on it → system updates everything.
Usage:
auto_detect.py watch # Watch for changes (continuous)
auto_detect.py check <file> # Check what would update
auto_detect.py trigger <file> # Trigger update for a file
"""
import hashlib
import json
import os
import sys
import time
from datetime import datetime, timezone
SCRIPTS_DIR = os.path.expanduser("~/.config/opencode/scripts")
SHARED_DIR = os.path.expanduser("~/.config/opencode/shared")
SNAPSHOT_FILE = os.path.expanduser("~/.config/opencode/memory/file_snapshot.json")
sys.path.insert(0, SHARED_DIR)
sys.path.insert(0, SCRIPTS_DIR)
def get_file_hash(filepath: str) -> str:
"""Get hash of a file for change detection."""
try:
with open(filepath, "rb") as f:
return hashlib.md5(f.read()).hexdigest()
except Exception:
return ""
def snapshot_directory(directory: str) -> dict:
"""Take a snapshot of all .py files in a directory."""
snapshot = {}
for f in os.listdir(directory):
if f.endswith(".py"):
path = os.path.join(directory, f)
snapshot[f] = {
"hash": get_file_hash(path),
"mtime": os.path.getmtime(path),
}
return snapshot
def load_snapshot() -> dict:
"""Load the last snapshot from disk."""
if os.path.exists(SNAPSHOT_FILE):
try:
with open(SNAPSHOT_FILE) as f:
return json.load(f)
except json.JSONDecodeError:
return {}
return {}
def save_snapshot(snapshot: dict):
"""Save snapshot to disk."""
os.makedirs(os.path.dirname(SNAPSHOT_FILE), exist_ok=True)
with open(SNAPSHOT_FILE, "w") as f:
json.dump(snapshot, f, indent=2)
def detect_changes(old_snapshot: dict, new_snapshot: dict) -> list:
"""Detect which files changed between snapshots."""
changes = []
for filename, new_info in new_snapshot.items():
old_info = old_snapshot.get(filename)
if not old_info:
changes.append({"file": filename, "type": "added"})
elif old_info["hash"] != new_info["hash"]:
changes.append({"file": filename, "type": "modified"})
for filename in old_snapshot:
if filename not in new_snapshot:
changes.append({"file": filename, "type": "deleted"})
return changes
def get_affected_umbrellas(changed_file: str) -> list:
"""Get all umbrellas affected by a file change."""
try:
import dependency_tracker
dependents = dependency_tracker.get_dependents(changed_file)
except Exception:
dependents = []
# Map files to umbrellas
umbrella_map = {
"memory": ["memory_store", "memory_consolidation", "warm_router", "graph_memory",
"warm_neuron_store", "abstraction", "cache", "recall_router",
"recall_uplift", "dedupe_policy", "memory_env", "drift_core",
"drift_cache", "tier_manager", "shard_manager", "query_memory"],
"governance": ["drift_core", "drift_cache", "drift_compression", "drift_observability",
"audit_ledger", "curator", "claim_audit", "grading", "worthiness",
"lexicon_evolution"],
"identity": ["persona_definition", "persona_check", "persona_gate",
"identity_anchors", "voice_hybrid", "voice_sync", "delivery_adapter",
"prompt_sync", "behavior"],
"growth": ["growth_delta", "growth_simulator", "lessons", "skill_library",
"self_improve", "subconscious_pass", "absorb_persona", "socratic"],
"perception": ["warm_router", "capability_broker", "witness",
"context_pressure", "auto_context"],
"infrastructure": ["microkernel", "hook_engine", "event_bus", "config_audit",
"system_registry", "bootstrap", "dependency_tracker"],
}
affected = set()
changed_stem = changed_file.replace(".py", "")
for umbrella, files in umbrella_map.items():
if changed_stem in files:
affected.add(umbrella)
# Also check dependents
for dep in dependents:
dep_stem = dep.replace(".py", "")
for umbrella, files in umbrella_map.items():
if dep_stem in files:
affected.add(umbrella)
return sorted(affected)
def auto_trigger(changed_file: str, dry_run: bool = False) -> dict:
"""Auto-trigger updates for a changed file."""
umbrellas = get_affected_umbrellas(changed_file)
result = {
"changed_file": changed_file,
"affected_umbrellas": umbrellas,
"triggered": [],
"skipped": [],
}
for umbrella in umbrellas:
if dry_run:
result["triggered"].append({"umbrella": umbrella, "action": "would trigger"})
else:
# Trigger umbrella update
try:
proc = subprocess.run(
[sys.executable, os.path.join(SHARED_DIR, "umbrella_update.py"),
umbrella, "--skip-commit"],
capture_output=True, text=True, timeout=300
)
if proc.returncode == 0:
result["triggered"].append({"umbrella": umbrella, "action": "updated"})
else:
result["skipped"].append({"umbrella": umbrella, "reason": proc.stderr[:100]})
except Exception as e:
result["skipped"].append({"umbrella": umbrella, "reason": str(e)[:100]})
return result
def main():
import argparse
parser = argparse.ArgumentParser(description="Auto-detection layer")
sub = parser.add_subparsers(dest="cmd")
sub.add_parser("watch", help="Watch for changes (continuous)")
p_check = sub.add_parser("check", help="Check what would update")
p_check.add_argument("file")
p_trigger = sub.add_parser("trigger", help="Trigger update for a file")
p_trigger.add_argument("file")
p_trigger.add_argument("--dry-run", action="store_true")
sub.add_parser("snapshot", help="Take a snapshot of current state")
args = parser.parse_args()
if not args.cmd:
parser.print_help()
sys.exit(1)
if args.cmd == "snapshot":
snapshot = {}
for d in [SCRIPTS_DIR, SHARED_DIR]:
snapshot.update(snapshot_directory(d))
save_snapshot(snapshot)
print(f"Snapshot saved: {len(snapshot)} files")
elif args.cmd == "check":
umbrellas = get_affected_umbrellas(args.file)
print(f"File: {args.file}")
print(f"Affected umbrellas: {', '.join(umbrellas)}")
elif args.cmd == "trigger":
result = auto_trigger(args.file, dry_run=args.dry_run)
print(f"File: {result['changed_file']}")
print(f"Affected: {', '.join(result['affected_umbrellas'])}")
for t in result["triggered"]:
print(f" ✓ {t['umbrella']}: {t['action']}")
for s in result["skipped"]:
print(f" ✗ {s['umbrella']}: {s['reason']}")
elif args.cmd == "watch":
print("Watching for changes... (Ctrl+C to stop)")
# Take initial snapshot
snapshot = {}
for d in [SCRIPTS_DIR, SHARED_DIR]:
snapshot.update(snapshot_directory(d))
save_snapshot(snapshot)
while True:
time.sleep(30) # Check every 30 seconds
new_snapshot = {}
for d in [SCRIPTS_DIR, SHARED_DIR]:
new_snapshot.update(snapshot_directory(d))
changes = detect_changes(snapshot, new_snapshot)
if changes:
print(f"\nDetected {len(changes)} changes:")
for change in changes:
print(f" {change['type']}: {change['file']}")
result = auto_trigger(change["file"])
for t in result["triggered"]:
print(f" → {t['umbrella']}: {t['action']}")
snapshot = new_snapshot
save_snapshot(snapshot)
if __name__ == "__main__":
import subprocess
main()