Add self-improvement agent script
Scans targets for safe mechanical improvements: prepend missing titles, flag TODOs, add viewport meta tags. No destructive edits, no model calls. Co-authored-by: Hermes <hermes@nousresearch.com>
This commit is contained in:
parent
8ff5b127f3
commit
36b42d5c23
20
.hermes-scripts/self-improvement/README.md
Normal file
20
.hermes-scripts/self-improvement/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Self-Improvement Agent
|
||||||
|
|
||||||
|
Runs on a cron schedule. Scans targets for safe, mechanical improvements:
|
||||||
|
- Prepends missing markdown titles
|
||||||
|
- Flags TODOs/FIXMEs
|
||||||
|
- Adds missing viewport meta tags
|
||||||
|
|
||||||
|
No rewrites. No model calls. No autonomous destructive edits.
|
||||||
|
|
||||||
|
## Targets
|
||||||
|
- sovereign-stack-pwa README, HTML
|
||||||
|
- Hermes skills directory
|
||||||
|
|
||||||
|
## State
|
||||||
|
- `state.json` tracks runs, patches proposed/applied
|
||||||
|
- Logs to `/root/.hermes/logs/self-improvement.log`
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
- Only additive, reversible edits
|
||||||
|
- No deletions, no logic changes, no config rewrites
|
||||||
107
.hermes-scripts/self-improvement/agent.py
Executable file
107
.hermes-scripts/self-improvement/agent.py
Executable file
|
|
@ -0,0 +1,107 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Self-improvement agent. Reviews recent logs, proposes improvements, applies safe patches."""
|
||||||
|
import json, re, sys, os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
LOG_PATH = "/root/.hermes/logs/self-improvement.log"
|
||||||
|
STATE_PATH = "/root/.hermes/scripts/self-improvement/state.json"
|
||||||
|
TARGETS = [
|
||||||
|
"/root/sovereign-stack-pwa/README.md",
|
||||||
|
"/root/sovereign-stack-pwa/index.html",
|
||||||
|
"/root/.hermes/skills/",
|
||||||
|
]
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
with open(LOG_PATH, "a") as f:
|
||||||
|
f.write(f"[{datetime.utcnow().isoformat()}Z] {msg}\n")
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if os.path.exists(STATE_PATH):
|
||||||
|
return json.load(open(STATE_PATH))
|
||||||
|
return {"last_run": None, "improvements_applied": 0, "patches_proposed": 0}
|
||||||
|
|
||||||
|
def save_state(state):
|
||||||
|
json.dump(state, open(STATE_PATH, "w"), indent=2)
|
||||||
|
|
||||||
|
def review_targets():
|
||||||
|
"""Scan targets for obvious improvement candidates: TODOs, FIXMEs, missing meta tags."""
|
||||||
|
candidates = []
|
||||||
|
for target in TARGETS:
|
||||||
|
if os.path.isdir(target):
|
||||||
|
for root, _, files in os.walk(target):
|
||||||
|
for name in files:
|
||||||
|
if name.endswith((".md", ".py", ".html", ".sh", ".json")):
|
||||||
|
candidates.append(os.path.join(root, name))
|
||||||
|
elif os.path.exists(target):
|
||||||
|
candidates.append(target)
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
def propose_patch(path):
|
||||||
|
"""Propose a concrete, safe patch for a file."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return None
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Safe improvements only: no rewrites
|
||||||
|
patches = []
|
||||||
|
if path.endswith(".md") and not content.startswith("#"):
|
||||||
|
patches.append(("prepend-title", f"# {os.path.basename(path)}\n\n"))
|
||||||
|
if "TODO" in content or "FIXME" in content:
|
||||||
|
patches.append(("flag-todos", "<!-- REVIEW_NEEDED: TODOs/FIXMEs present -->\n"))
|
||||||
|
if path.endswith(".html") and '<meta name="viewport"' not in content:
|
||||||
|
patches.append(("add-viewport", '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'))
|
||||||
|
return patches
|
||||||
|
|
||||||
|
def apply_patch(path, patches):
|
||||||
|
applied = []
|
||||||
|
if not patches:
|
||||||
|
return applied
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
for kind, snippet in patches:
|
||||||
|
if kind == "prepend-title" and not content.startswith("#"):
|
||||||
|
content = snippet + content
|
||||||
|
applied.append(f"Prepended title to {path}")
|
||||||
|
elif kind == "flag-todos" and "REVIEW_NEEDED" not in content:
|
||||||
|
content = snippet + content
|
||||||
|
applied.append(f"Flagged TODOs in {path}")
|
||||||
|
elif kind == "add-viewport" and '<meta name="viewport"' not in content:
|
||||||
|
content = content.replace("<head>", f"<head>\n {snippet}")
|
||||||
|
applied.append(f"Added viewport meta to {path}")
|
||||||
|
|
||||||
|
if applied:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
return applied
|
||||||
|
|
||||||
|
def run():
|
||||||
|
state = load_state()
|
||||||
|
now = datetime.utcnow()
|
||||||
|
log("Self-improvement run start")
|
||||||
|
|
||||||
|
candidates = review_targets()
|
||||||
|
total_patches = 0
|
||||||
|
total_applied = 0
|
||||||
|
|
||||||
|
for path in candidates[:20]: # Cap to avoid thrashing
|
||||||
|
patches = propose_patch(path)
|
||||||
|
if patches:
|
||||||
|
state["patches_proposed"] += len(patches)
|
||||||
|
applied = apply_patch(path, patches)
|
||||||
|
if applied:
|
||||||
|
state["improvements_applied"] += len(applied)
|
||||||
|
total_applied += len(applied)
|
||||||
|
for a in applied:
|
||||||
|
log(f"APPLIED: {a}")
|
||||||
|
else:
|
||||||
|
log(f"PROPOSED but not applied: {path}")
|
||||||
|
total_patches += len(patches)
|
||||||
|
|
||||||
|
state["last_run"] = now.isoformat() + "Z"
|
||||||
|
save_state(state)
|
||||||
|
log(f"Run complete. Patches proposed: {total_patches}, applied: {total_applied}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
Loading…
Reference in New Issue
Block a user