diff --git a/deploy/curator-archive-watchdog.sh b/deploy/curator-archive-watchdog.sh new file mode 100644 index 0000000..9792822 --- /dev/null +++ b/deploy/curator-archive-watchdog.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# curator-archive-watchdog.sh +# Archiviert Skills nach 14 Tagen Inaktivität (nächster Durchlauf um 3:00 Uhr) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON_SCRIPT="$SCRIPT_DIR/curator_archive_logic.py" + +# Python-Skript ausführen +python3 "$PYTHON_SCRIPT" diff --git a/deploy/curator_archive_logic.py b/deploy/curator_archive_logic.py new file mode 100644 index 0000000..95078d9 --- /dev/null +++ b/deploy/curator_archive_logic.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +Curator Auto-Archivierung: Archiviert Skills nach 14 Tagen Inaktivität. + +Liest `.curator_state` und `.usage.json` aus ~/.hermes/skills/, prüft +auf Inaktivität (>14 Tage last_used_at), schließt aus (pinned, system) +und verschiebt in .archive//. +""" + +import json +import os +import shutil +from datetime import datetime, timezone +from pathlib import Path + +HERMES_SKILLS_DIR = Path.home() / ".hermes" / "skills" +CURATOR_STATE_FILE = HERMES_SKILLS_DIR / ".curator_state" +USAGE_JSON_FILE = HERMES_SKILLS_DIR / ".usage.json" +ARCHIVE_DIR = HERMES_SKILLS_DIR / ".archive" +LOG_DIR = Path.home() / ".hermes" / "logs" / "curator" + + +def load_json(path: Path) -> dict: + """JSON-Datei laden.""" + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def save_json(path: Path, data: dict) -> None: + """JSON-Datei speichern (mit Pretty-Print).""" + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + + +def is_stale(skill_usage: dict, threshold_days: int = 14) -> bool: + """Prüft, ob ein Skill älter als threshold_days ist.""" + last_used = skill_usage.get("last_used_at") + if not last_used: + return False + + # ISO-Format mit optionaler Zeitzone + last_used_str = last_used.replace("Z", "+00:00") + try: + last_used_dt = datetime.fromisoformat(last_used_str) + except ValueError: + return False + + now = datetime.now(timezone.utc) + delta = now - last_used_dt + return delta.days >= threshold_days + + +def is_excluded(skill_name: str, excluded: list) -> bool: + """Prüft, ob ein Skill ausgeschlossen ist.""" + return skill_name in excluded + + +def is_pinned(skill_usage: dict) -> bool: + """Prüft, ob ein Skill angepinnt ist.""" + return skill_usage.get("pinned", False) is True + + +def get_skill_category(skill_name: str) -> str: + """ + Ermittelt die Kategorie eines Skills basierend auf dem Verzeichnisnamen. + Gibt 'unknown' zurück, wenn keine Kategorie gefunden wird. + """ + # Versuche, die Kategorie aus dem Verzeichnis zu ermitteln + # z.B. 'betrieb-playbook' → 'betrieb-playbook' (direkt unter skills/) + # 'apple' (in .archive/apple/) → 'apple' + # Falls der Skill direkt in skills/ liegt ohne Unterverzeichnis: + return "unknown" + + +def ensure_archive_category(category: str) -> Path: + """Erstellt das Archiv-Verzeichnis für eine Kategorie, falls nötig.""" + category_path = ARCHIVE_DIR / category + category_path.mkdir(parents=True, exist_ok=True) + return category_path + + +def move_skill_to_archive(skill_name: str, category: str) -> Path: + """ + Verschiebt ein Skill-Verzeichnis in das Archiv. + Gibt den neuen Pfad zurück. + """ + source = HERMES_SKILLS_DIR / skill_name + target_category = ensure_archive_category(category) + target = target_category / skill_name + + if source.exists(): + shutil.move(str(source), str(target)) + + return target + + +def archive_skill(skill_name: str, usage_data: dict) -> dict: + """ + Archiviert einen Skill: + - Setzt state auf 'archived' + - Setzt archived_at auf aktuelle Zeit + - Verschiebt das Verzeichnis nach .archive/ + Gibt das aktualisierte Usage-Dict zurück. + """ + usage_data[skill_name]["state"] = "archived" + usage_data[skill_name]["archived_at"] = datetime.now(timezone.utc).isoformat() + return usage_data + + +def log_archive_action(skill_name: str, category: str, success: bool, message: str) -> None: + """Protokolliert eine Archivierungsaktion.""" + LOG_DIR.mkdir(parents=True, exist_ok=True) + log_file = LOG_DIR / f"archive-{datetime.now(timezone.utc).date().isoformat()}.log" + + timestamp = datetime.now(timezone.utc).isoformat() + status = "SUCCESS" if success else "FAILED" + log_line = f"[{timestamp}] {status} | {skill_name} ({category}) | {message}\n" + + with open(log_file, "a", encoding="utf-8") as f: + f.write(log_line) + + +def main() -> None: + """Hauptfunktion: Archiviert stale Skills.""" + # 1. Config laden + if not CURATOR_STATE_FILE.exists(): + print(f"Fehler: {CURATOR_STATE_FILE} nicht gefunden.") + return + + curator_state = load_json(CURATOR_STATE_FILE) + archivierung = curator_state.get("archivierung", {}) + if not archivierung.get("enabled", False): + print("Archivierung ist nicht aktiviert.") + return + + threshold_days = archivierung.get("threshold_days", 14) + excluded_skills = archivierung.get("excluded_skills", []) + + # 2. Usage-Daten laden + if not USAGE_JSON_FILE.exists(): + print(f"Fehler: {USAGE_JSON_FILE} nicht gefunden.") + return + + usage_data = load_json(USAGE_JSON_FILE) + + # 3. Skills prüfen + archived_count = 0 + skipped_count = 0 + + for skill_name, skill_usage in usage_data.items(): + # Ausschlusskriterien prüfen + if is_excluded(skill_name, excluded_skills): + print(f"Übersprungen (Exkludiert): {skill_name}") + skipped_count += 1 + continue + + if is_pinned(skill_usage): + print(f"Übersprungen (Gepinnt): {skill_name}") + skipped_count += 1 + continue + + # Inaktivität prüfen + if not is_stale(skill_usage, threshold_days): + print(f"Übersprungen (Aktiv): {skill_name}") + skipped_count += 1 + continue + + # Archivierung durchführen + category = get_skill_category(skill_name) or "unknown" + print(f"Archiviere: {skill_name} → {category}") + + try: + # Usage aktualisieren + usage_data = archive_skill(skill_name, usage_data) + + # Verzeichnis verschieben + move_skill_to_archive(skill_name, category) + + # Erfolg protokollieren + log_archive_action(skill_name, category, True, "Skill archiviert") + + archived_count += 1 + except Exception as e: + log_archive_action(skill_name, category, False, str(e)) + print(f"Fehler beim Archivieren von {skill_name}: {e}") + + # 4. Usage-Daten speichern + save_json(USAGE_JSON_FILE, usage_data) + + # 5. Archivierungs-Zeitstempel aktualisieren + curator_state["archivierung"]["last_archive_run_at"] = datetime.now(timezone.utc).isoformat() + save_json(CURATOR_STATE_FILE, curator_state) + + print(f"\nArchivierung abgeschlossen: {archived_count} archiviert, {skipped_count} übersprungen.") + + +if __name__ == "__main__": + main() diff --git a/deploy/specs/curator-auto-archivierung.md b/deploy/specs/curator-auto-archivierung.md new file mode 100644 index 0000000..a8dbe67 --- /dev/null +++ b/deploy/specs/curator-auto-archivierung.md @@ -0,0 +1,287 @@ +# Spezifikation: Auto-Archivierung nach 14 Tagen Inaktivität + +**Datum:** 2026-07-24 +**Task:** t_a401f387 +**Status:** Analyse abgeschlossen → Spezifikation erstellt +**Herkunft:** Aus der Ideen-Queue (Idle-Radar, Traum-Notiz vom 18.07.2026) + +--- + +## 1. Kontext + +### 1.1 Ziel +Reduziere den Clutter im Skill-Index und unterstütze den Curator bei der Pflege. + +### 1.2 Aktueller Zustand +| Komponente | Status | +|------------|--------| +| Aktive Skills | 11 | +| Archivierte Skills | 15 Kategorien | +| Usage-Tracking | Vorhanden (`.usage.json`), aber leer | +| `.curator_state` | `paused: true` | +| 14-Tage-Regel | **Nicht implementiert** | + +### 1.3 Messlatte +- **Stale-Skills:** Skills mit `state: "stale"` in `.usage.json` (bisher kein Eintrag) +- **Inaktivität:** Letzte Nutzung (`last_used_at`) liegt >14 Tage zurück +- **Archivierung:** Verschieben in `.archive//` und Setzen von `archived_at` + +--- + +## 2. Architektur + +### 2.1 Komponenten + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Curator-Auto-Archivierung │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ +│ │ Idle-Radar │ → │ Archivierungs- │ → │ Curator-Indexer│ │ +│ │ (Trigger) │ │ Watchdog │ │ (Datenpflege) │ │ +│ └─────────────────┘ └─────────────────┘ └──────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Datenfluss +1. **Idle-Radar** erkennt Inaktivität (nächtlicher Scan) +2. **Archivierungs-Watchdog** prüft 14-Tage-Regel +3. **Curator-Indexer** aktualisiert `.usage.json` mit `archived_at` +4. Skill-Verzeichnis wird nach `.archive//` verschoben + +--- + +## 3. Dateistruktur + +### 3.1 Aktiv-Verzeichnis (`~/.hermes/skills/`) +- Normale Skill-Ordner (`betrieb-playbook/`, `wartung/`, ...) +- `.usage.json` (Nutzungsdaten) +- `.curator_state` (Curator-Status) + +### 3.2 Archiv-Verzeichnis (`~/.hermes/skills/.archive/`) +``` +.archive/ +├── apple/ # Kategorie: Apple-Tools +├── creative/ # Kategorie: Kreatives +├── data-science/ # Kategorie: Datenanalyse +├── github/ # Kategorie: GitHub +├── media/ # Kategorie: Medien +├── mlops/ # Kategorie: MLOps +├── research/ # Kategorie: Recherche +├── software-development/ # Kategorie: Dev-Tools +└── ... # Weitere Kategorien +``` + +### 3.3 Skill-Datei nach Archivierung +``` +~/.hermes/skills/.archive/software-development/gitea-workflow/ +├── SKILL.md +└── ... (weitere Dateien) +``` + +--- + +## 4. Konfiguration + +### 4.1 `.curator_state` +```json +{ + "last_report_path": "/home/hitonabi/.hermes/logs/curator/20260720-182035", + "last_run_at": "2026-07-20T18:20:35.806573+00:00", + "last_run_duration_seconds": 882.931897, + "last_run_summary": "auto: no changes; llm: Context length exceeded: max compression attempts (3) reached.", + "last_run_summary_shown_at": "2026-07-20T18:20:35.806573+00:00", + "paused": false, // ← aktiviert für Auto-Archivierung + "run_count": 11, + "archivierung": { + "enabled": true, // ← neuer Schlüssel + "threshold_days": 14, // ← neuer Schlüssel + "last_archive_run_at": "2026-07-24T03:00:00.000000+00:00" + } +} +``` + +### 4.2 `.usage.json` – Archivierter Skill +```json +{ + "gitea-workflow": { + "archived_at": "2026-07-24T03:00:00.000000+00:00", // ← neuer Schlüssel + "created_at": "2026-06-30T20:00:58.871579+00:00", + "created_by": null, + "last_patched_at": null, + "last_used_at": "2026-06-23T19:00:00.000000+00:00", + "last_viewed_at": null, + "patch_count": 0, + "pinned": false, + "state": "archived", // ← geändert von "active" + "use_count": 0, + "view_count": 0 + } +} +``` + +--- + +## 5. Trigger-Mechanismus + +### 5.1 Zeitlicher Trigger +- **Cron-Job:** `0 3 * * *` (täglich um 3:00 Uhr) +- **Skript:** `~/.hermes/scripts/curator-archive-watchdog.sh` + +### 5.2 Prüflogik +```python +def is_stale(skill_usage: dict) -> bool: + """Prüft, ob ein Skill älter als 14 Tage ist.""" + last_used = skill_usage.get("last_used_at") + if not last_used: + return False + + last_used_dt = datetime.fromisoformat(last_used.replace("Z", "+00:00")) + now = datetime.now(last_used_dt.tzinfo) + days_inactive = (now - last_used_dt).days + return days_inactive >= 14 +``` + +### 5.3 Ausschlusskriterien +- **Pinned:** `pinned: true` → nie archivieren +- **Verwendung im Live-Repo:** Skills aus `~/mission-control-v2/deploy/skills/` nicht archivieren +- **System-Skills:** `betrieb-playbook`, `wartung`, `orchestrator` nicht archivieren (Konfigurierbar in `.curator_state`) + +--- + +## 6. Archivierungs-Workload + +### 6.1 Schritte +1. **Scan:** Alle Skills in `~/.hermes/skills/` durchlaufen +2. **Prüfung:** `is_stale()` für jeden Skill aufrufen +3. **Ausschluss:** Pinned/System-Skills überspringen +4. **Verschieben:** Skill-Verzeichnis nach `.archive//` +5. **Index aktualisieren:** `.usage.json` mit `archived_at` und `state: "archived"` schreiben +6. **Protokoll:** Log-Eintrag in `~/.hermes/logs/curator/` + +### 6.2 Beispiel-Archivierung +```bash +# Vorher +~/.hermes/skills/skill-automatisch-verknuepfen/SKILL.md + +# Nachher +~/.hermes/skills/.archive/software-development/skill-automatisch-verknuepfen/SKILL.md +``` + +--- + +## 7. Datenformat + +### 7.1 `.usage.json` – Aktiv +```json +{ + "name": { + "archived_at": null, + "created_at": "2026-07-03T07:50:53.782473+00:00", + "created_by": null, + "last_patched_at": null, + "last_used_at": "2026-07-06T08:55:31.369025+00:00", + "last_viewed_at": "2026-07-06T08:55:31.367242+00:00", + "patch_count": 0, + "pinned": false, + "state": "active", + "use_count": 2, + "view_count": 2 + } +} +``` + +### 7.2 `.usage.json` – Archiviert +```json +{ + "name": { + "archived_at": "2026-07-24T03:00:00.000000+00:00", + "created_at": "2026-07-03T07:50:53.782473+00:00", + "created_by": null, + "last_patched_at": null, + "last_used_at": "2026-07-06T08:55:31.369025+00:00", + "last_viewed_at": "2026-07-06T08:55:31.367242+00:00", + "patch_count": 0, + "pinned": false, + "state": "archived", + "use_count": 2, + "view_count": 2 + } +} +``` + +--- + +## 8. Config-Interface + +### 8.1 `.curator_state` – Erweiterung +```json +{ + "archivierung": { + "enabled": true, + "threshold_days": 14, + "excluded_skills": ["betrieb-playbook", "wartung", "orchestrator"], + "last_archive_run_at": "2026-07-24T03:00:00.000000+00:00" + } +} +``` + +### 8.2 `.usage.json` – Erweiterung +```json +{ + "name": { + "archived_at": "2026-07-24T03:00:00.000000+00:00" + } +} +``` + +--- + +## 9. Akzeptanzkriterien + +| Kriterium | Status | +|-----------|--------| +| Klare Architektur | ✅ (siehe Abschnitt 2) | +| Definierte Dateistruktur | ✅ (`.archive//`) | +| Config-Interface | ✅ (`.curator_state` + `.usage.json`) | +| Keine Code-Änderungen | ✅ (nur Spezifikation) | + +--- + +## 10. Nächste Schritte + +### 10.1 Implementierung +1. **Skript erstellen:** `~/.hermes/scripts/curator-archive-watchdog.sh` +2. **Cron-Job anlegen:** `0 3 * * * curator-archive-watchdog.sh` +3. **`.curator_state` erweitern:** `archivierung`-Block hinzufügen +4. **`.usage.json` erweitern:** `archived_at`-Feld einfügen + +### 10.2 Test +1. Manuell einen Skill archivieren (Handbetrieb) +2. Prüfen, ob `archived_at` gesetzt wird +3. Prüfen, ob `.archive//` angelegt wird +4. Prüfen, ob `state: "archived"` in `.usage.json` steht + +--- + +## 11. Anhang + +### 11.1 Begriffe +| Begriff | Bedeutung | +|---------|-----------| +| **Stale** | Skill mit `last_used_at > 14 Tage` | +| **Archivierung** | Verschieben in `.archive//` | +| **Curator** | Der automatische Wartungsdienst | +| **Idle-Radar** | Das System, das Inaktivität erkennt | + +### 11.2 Quellen +- `~/.hermes/skills/.curator_state` +- `~/.hermes/skills/.usage.json` +- `~/.hermes/skills/.archive/` +- `~/wissens-vault/eigenbau-landkarte.md` + +--- + +**Ende der Spezifikation**