Phase 2 Drei-Welten-Plan: No-Progress-Bremse + Betrieb-Playbook + mem0-Lern-Buendelung

- no-progress-bremse.py: generische, turn-uebergreifende Schleifenbremse (pre/post_tool_call);
  Muster-Hash statt hartkodierter Fehler-Strings, Selbst-Lernen via neue-signaturen.jsonl.
  Schliesst die Luecke der nativen guardrails (pro Turn, nur klassifizierte Fehler).
- ensure-profile-hooks.py: kann Eintraege in BESTEHENDE Event-Bloecke einfuegen
  (zweiter pre_tool_call-Key haette den Tabu-Guard lautlos verschluckt) + registriert Bremse.
- betrieb-playbook-Skill: SSH-/Deploy-Diagnose-Checklisten (Passphrase-Falle zuerst),
  Triage-Regel im Steckbrief-Hook; Skill wird in alle Profile gesynct.
- mc2-memory: Lern-Extraktion gebuendelt (4 Turns oder 180 s idle = EIN Lauf) statt pro Turn -
  Extraktion konkurrierte mit Lucys Hirn um die Slots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-19 14:21:18 +02:00
parent 331aafa25d
commit 903f3bc074
6 changed files with 290 additions and 24 deletions
@@ -29,6 +29,7 @@ hint=""
ctx="$(cat <<EOF
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642, mem0-Gedaechtnis :8765. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
FRONTEND-BAUEN (seit 15.07. abends): Node LTS liegt sudo-frei unter ~/.local (node/npm im Worker-PATH). Bei Frontend-Aenderungen baust DU das dist MIT: cd frontend && npm ci --no-audit --no-fund && npm run build — und committest frontend/dist im selben Branch (die Box zeigt sonst nach Annahme den alten Stand; Bau-Beweis 15.07.: kompletter Build in ~4 s).
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""No-Progress-Bremse (Phase 2 des Drei-Welten-Plans, 19.07.2026).
Generische, turn-UEBERGREIFENDE Schleifenbremse — ergaenzt Hermes' native
tool_loop_guardrails, die (a) pro Turn zaehlen und (b) nur als Fehler
klassifizierte Ergebnisse sehen. Die teuer gelernte Luecke (SSH-Marathon 18.07.,
36 min / 75 Schleifen): ein Befehl laeuft "erfolgreich" durch, die AUSGABE sagt
aber immer dasselbe ("Permission denied") — der Agent variiert Befehle gegen
eine unsichtbare Wand, ueber Turn-Grenzen hinweg, und nichts stoppt ihn.
Prinzip (KEINE hartkodierten Fehler-Strings — Leitplanke des Commanders:
"wenn sich was aendert, muss es das SELBST erkennen"):
post_tool_call → Ergebnis normalisieren (Zahlen/Hex-IDs raus) → Muster-Hash.
Gleicher Hash beim gleichen Tool in Folge = Zaehler hoch.
Unbekannte Muster landen in neue-signaturen.jsonl —
Futter fuer Traum/Radar, um daraus Playbooks zu verdichten.
pre_tool_call → Zaehler >= 3 fuer dieses Tool: EINMAL mechanisch blocken
(Diagnose-Anweisung statt naechster Blindversuch), Zaehler
zuruecksetzen. Der Folgeversuch ist frei; laeuft er wieder
3x gegen dasselbe Muster, blockt es erneut.
Gezaehlt wird nur, wo Wiederholung wirklich Stillstand heisst: terminal IMMER
(dort verstecken sich die "erfolgreichen" Fehler), andere Tools nur bei
status=error. Lese-Tools deckt die native idempotent-Bremse ab.
Aufruf (hooks in config.yaml): `no-progress-bremse.py pre` bzw. `... post`.
State: ~/.hermes/state/no-progress/<session>.json (48 h Selbst-Aufraeumen).
Fail-open: jeder eigene Fehler -> {} (die Bremse darf nie selbst zur Wand werden).
"""
import hashlib
import json
import os
import re
import sys
import time
STATE_DIR = os.path.expanduser("~/.hermes/state/no-progress")
SIG_LOG = os.path.join(STATE_DIR, "neue-signaturen.jsonl")
SEEN_FILE = os.path.join(STATE_DIR, "seen-sigs.json")
THRESHOLD = 3
STATE_TTL = 48 * 3600
BLOCK_REASON = (
"NO-PROGRESS-BREMSE: '{tool}' hat {n}x in Folge dasselbe Ergebnismuster geliefert — "
"das ist Stillstand, egal wie sehr die Befehle variieren. STOPP. "
"1) Benenne das Hindernis in EINEM Satz (was genau meldet das Ergebnis?). "
"2) Diagnose statt Variation — bei SSH/Zugriff ZUERST: Schluessel-Passphrase pruefen "
"(`ssh-keygen -y -f <key> </dev/null` — stilles 'Permission denied' = verschluesselter Key), "
"dann Gegenseite (Auth-Log, Dienststatus, Erreichbarkeit) LESEN statt raten. "
"3) Bleibt es unklar: Aufgabe als blocked/Karte mit dem EXAKTEN Ergebnistext an den "
"Commander uebergeben. Dein naechster Versuch ist frei — nutze ihn erst NACH der Diagnose."
)
_NUM_RE = re.compile(r"\d+")
_HEX_RE = re.compile(r"\b[0-9a-f]{8,}\b")
_WS_RE = re.compile(r"\s+")
def _norm_sig(tool: str, text: str) -> str:
t = (text or "")[:2000].lower()
t = _HEX_RE.sub("#", _NUM_RE.sub("#", t))
t = _WS_RE.sub(" ", t).strip()[:400]
return hashlib.sha1(f"{tool}|{t}".encode()).hexdigest()[:16]
def _session_key(p: dict) -> str:
sid = p.get("session_id") or p.get("parent_session_id") or ""
if sid:
return re.sub(r"[^A-Za-z0-9_.-]", "_", str(sid))[:80]
return "cwd-" + hashlib.sha1(os.getcwd().encode()).hexdigest()[:12]
def _prune() -> None:
now = time.time()
for f in os.listdir(STATE_DIR):
fp = os.path.join(STATE_DIR, f)
if f.endswith(".json") and f != os.path.basename(SEEN_FILE):
if now - os.path.getmtime(fp) > STATE_TTL:
os.remove(fp)
def _load(path: str) -> dict:
try:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
except Exception:
return {}
def _save(path: str, data: dict) -> None:
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh)
os.replace(tmp, path)
def main() -> None:
mode = sys.argv[1] if len(sys.argv) > 1 else "post"
payload = json.load(sys.stdin)
tool = str(payload.get("tool_name") or "")
if not tool:
print("{}")
return
os.makedirs(STATE_DIR, exist_ok=True)
sfile = os.path.join(STATE_DIR, _session_key(payload) + ".json")
state = _load(sfile)
tools = state.setdefault("tools", {})
if mode == "post":
status = str(payload.get("status") or "ok")
# terminal immer beobachten (dort tarnen sich Fehler als Erfolg),
# sonst nur echte Fehler — Lese-Tools regelt die native Bremse.
if tool != "terminal" and status != "error":
print("{}")
return
sig = _norm_sig(tool, str(payload.get("result") or ""))
entry = tools.get(tool) or {}
entry["count"] = entry.get("count", 0) + 1 if entry.get("sig") == sig else 1
entry["sig"] = sig
tools[tool] = entry
_save(sfile, state)
_prune()
seen = _load(SEEN_FILE)
sigs = seen.setdefault("sigs", [])
if sig not in sigs and status == "error" or (tool == "terminal" and sig not in sigs and entry["count"] >= 2):
sigs.append(sig)
del sigs[:-500]
_save(SEEN_FILE, seen)
sample = _WS_RE.sub(" ", str(payload.get("result") or ""))[:200]
with open(SIG_LOG, "a", encoding="utf-8") as fh:
fh.write(json.dumps({"ts": int(time.time()), "tool": tool,
"sig": sig, "sample": sample}, ensure_ascii=False) + "\n")
print("{}")
return
# mode == "pre"
entry = tools.get(tool) or {}
if entry.get("count", 0) >= THRESHOLD:
entry["count"] = 0
tools[tool] = entry
_save(sfile, state)
print(json.dumps({"decision": "block",
"reason": BLOCK_REASON.format(tool=tool, n=THRESHOLD)},
ensure_ascii=False))
return
print("{}")
if __name__ == "__main__":
try:
main()
except Exception:
print("{}")