Files
claude-regelwerk/.vault-tools/session-hooks/faelle_nach_edit.py
T
Pascal MichelandClaude Opus 5.5 e50c8296d7 regelwerk_check: Pruefung 7 meldet unsichtbare Zeichen in gespeicherten Workflows
Jedes Zeichen der Kategorie Cc ausser LF und jedes der Kategorie Cf in
.claude/workflows/**/*.js, gelesen als Bytes. Anlass: plankritik.js mit
CRLF scheiterte am Workflow-Werkzeug. Der Hook nach dem Bearbeiten loest
auch bei Workflows aus. Regressionslauf 28/28 angeschlagen, 6/6 stumm;
Hook-Treiber 58/58; Vorfall in exakter Form gemeldet (U+000D).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 20:54:51 +02:00

553 lines
23 KiB
Python

"""Fährt die Angriffsliste gegen regelwerk_nach_edit.py, je Fall auf einem Wegwerf-Vault.
Ersteller: Pascal Michel
Erstellt: 2026-09-13
Entwurf: ENTWURF.md im selben Ordner
Statt des echten regelwerk_check.py liegt im Wegwerf-Vault eine Attrappe. Sie
schreibt ihren Aufruf nach aufruf.json und zeigt dann ein festgelegtes
Verhalten: grün, Befund, Abbruch, ACHTUNG, stumm, Absturz, hängt, sehr viel
Ausgabe. Ihre Zeilen haben die Form der echten Ausgabe (regelwerk_check.py
Zeilen 114, 368, 370 und 372). So lässt sich je Fall unterscheiden, ob der
Hook still blieb, ohne die Prüfung zu starten, oder still blieb, nachdem sie
grün war.
Grundlinie zuerst: Ein grüner Check muss still bleiben und nachweislich
gelaufen sein, ein Befund muss gemeldet werden. Sonst misst der Rest nichts.
Seit der Nachbesserung Runde 2 bestimmt regelwerk_nach_edit.py seine
Vault-Wurzel ausschließlich aus dem eigenen Skriptpfad, das Feld cwd der
Hook-Eingabe zählt nicht mehr (Prüferbefund Runde 2, Koordinatorentscheidung).
lauf_fall() kopiert den Hook deshalb für jeden Fall zuerst in den Wegwerf-
Vault, statt das Original mit einem cwd-Feld dorthin zu verweisen.
Aufruf:
python .vault-tools/session-hooks/faelle_nach_edit.py
python .vault-tools/session-hooks/faelle_nach_edit.py --echt
"""
from __future__ import annotations
import contextlib
import importlib.util
import io
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
HIER = Path(__file__).resolve().parent
HOOK = HIER / "regelwerk_nach_edit.py"
MAX_KONTEXT = 4100
FAKE_KOPF = '''import json
import sys
import time
from pathlib import Path
Path(__file__).resolve().parents[2].joinpath("aufruf.json").write_text(
json.dumps({"argv": sys.argv[1:], "exe": sys.executable}), encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
'''
FAKES = {
"gruen": FAKE_KOPF + '''print()
print("Keine der 6 gelaufenen Prüfungen hat angeschlagen.")
sys.exit(0)
''',
"befund": FAKE_KOPF + '''print("[klein] AI/Regelwerk/regel.md — status: pending-review — Freigabe steht aus")
print()
print("1 Befund(e) aus 6 von 6 Prüfungen.")
sys.exit(1)
''',
"abbruch": FAKE_KOPF + '''print("[blockierend] regelwerk_check.py — Abbruch: KeyError: 'x'")
print()
print("1 Befund(e) aus 2 von 6 Prüfungen.")
sys.exit(2)
''',
"achtung": FAKE_KOPF + '''print()
print("Keine der 5 gelaufenen Prüfungen hat angeschlagen.")
print("ACHTUNG: nur 5 von 6 Prüfungen sind gelaufen — der Rest hatte keine Voraussetzung.")
sys.exit(0)
''',
"stumm": FAKE_KOPF + '''sys.exit(0)
''',
"absturz": FAKE_KOPF + '''raise RuntimeError("Absturz im Check")
''',
"haengt": FAKE_KOPF + '''time.sleep(120)
''',
"viel": FAKE_KOPF + '''for i in range(200):
print("[wichtig] AI/Regelwerk/regel.md — toter Wikilink: [[ziel%d]]" % i)
print()
print("200 Befund(e) aus 6 von 6 Prüfungen.")
sys.exit(1)
''',
}
def schreibe(vault: Path, rel: str, inhalt: str | bytes) -> None:
# Immer Bytes: write_text würde unter Windows jedes LF zu CRLF machen.
pfad = vault / rel
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_bytes(inhalt if isinstance(inhalt, bytes) else inhalt.encode("utf-8"))
def baue_vault(v: Path, fake: str | None) -> None:
schreibe(v, "CLAUDE.md", "# Stamm\n")
schreibe(v, "AI/Regelwerk/regel.md", "---\nstatus: reference\n---\n")
schreibe(v, "AI/Regelwerk/unter/tief.md", "x\n")
schreibe(v, ".claude/agents/planer.md", "---\nname: planer\n---\n")
schreibe(v, ".claude/skills/demo/SKILL.md", "---\nname: demo\n---\n")
schreibe(v, ".claude/workflows/demo.js", "export const meta = {}\n")
schreibe(v, "Inbox/notiz.md", "x\n")
schreibe(v, "Code/CLAUDE.md", "x\n")
if fake is not None:
schreibe(v, ".vault-tools/regelwerk-check/regelwerk_check.py", FAKES[fake])
def vorwaerts(pfad: Path) -> str:
return str(pfad).replace("\\", "/")
def laufwerk_getauscht(pfad: Path) -> str:
s = str(pfad)
if re.match(r"^[A-Za-z]:", s):
return s[0].swapcase() + s[1:]
return s
def j(daten: object) -> bytes:
return json.dumps(daten).encode("utf-8")
def edit(v: Path, datei: object, **extra: object) -> bytes:
daten: dict[str, object] = {
"session_id": "faelle",
"transcript_path": "",
"cwd": str(v),
"permission_mode": "default",
"hook_event_name": "PostToolUse",
"tool_name": "Edit",
"tool_input": {"file_path": datei},
"tool_response": {},
}
daten.update(extra)
return j(daten)
def ohne_cwd(v: Path, datei: object) -> bytes:
daten = json.loads(edit(v, datei))
del daten["cwd"]
return j(daten)
@dataclass
class Fall:
name: str
fake: str | None
stdin: Callable[[Path], bytes]
erwartung: str
enthaelt: list[str] = field(default_factory=list)
system_enthaelt: str = ""
max_dauer: float = 10.0
# None: Subprozess erbt die Umgebung dieses Treibers unveraendert (bisheriges
# Verhalten). Gesetzt: ersetzt die Umgebung vollstaendig (Pruefbefunde Runde 2).
umgebung: Callable[[Path], dict[str, str]] | None = None
BEFUND = ["[klein] AI/Regelwerk/regel.md"]
BEFUND_SYSTEM = "Exit 1, 1 Befundzeile(n)"
def still(name: str, stdin: Callable[[Path], bytes]) -> Fall:
return Fall(name, "befund", stdin, "still-ohne-lauf")
def meldet(name: str, stdin: Callable[[Path], bytes]) -> Fall:
return Fall(name, "befund", stdin, "meldung", BEFUND, BEFUND_SYSTEM)
def cwd_zeigt_anderswo(v: Path) -> bytes:
"""cwd zeigt auf einen anderen, echten (leeren) Ordner. Seit der
Nachbesserung Runde 2 liest der Hook cwd gar nicht mehr - die Wurzel ist
ausschliesslich der Ordner, in dem der Hook selbst liegt (hier: v, weil
lauf_fall() ihn dorthin kopiert). Das Ergebnis muss trotzdem melden,
nicht still bleiben (Pruefbefund Runde 2)."""
anderswo = v.parent / "anderswo"
anderswo.mkdir(parents=True, exist_ok=True)
return edit(v, str(v / "AI" / "Regelwerk" / "regel.md"), cwd=str(anderswo))
def ohne_pythonioencoding(_: Path) -> dict[str, str]:
"""Claude Code setzt PYTHONIOENCODING beim Hook-Aufruf nicht; nur die globale
Planvorgabe setzt sie vor dem Aufruf dieses Treibers selbst (Pruefbefund Runde 2)."""
umgebung = dict(os.environ)
umgebung.pop("PYTHONIOENCODING", None)
return umgebung
GRUNDLINIE = [
Fall("GRUNDLINIE | gruener Check bleibt still und lief", "gruen",
lambda v: edit(v, str(v / "CLAUDE.md")), "still-nach-lauf"),
Fall("GRUNDLINIE | Befund wird gemeldet", "befund",
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", BEFUND, BEFUND_SYSTEM),
]
FAELLE = [
still("leer | stdin leer", lambda v: b""),
still("leer | stdin nur Leerraum", lambda v: b" \r\n"),
still("falscher Typ| stdin kein JSON", lambda v: b"kein json"),
still("kodierung | stdin ungueltiges UTF-8", lambda v: b"\xff\xfe\x00"),
still("falscher Typ| stdin ist eine Liste",
lambda v: j([json.loads(edit(v, str(v / "CLAUDE.md")))])),
still("falscher Typ| stdin ist eine Zahl", lambda v: b"5"),
still("fehlt | tool_input fehlt", lambda v: j({"cwd": str(v), "tool_name": "Edit"})),
still("null | tool_input ist null", lambda v: edit(v, "x", tool_input=None)),
still("falscher Typ| tool_input ist eine Liste",
lambda v: edit(v, "x", tool_input=[str(v / "CLAUDE.md")])),
still("fehlt | file_path fehlt", lambda v: edit(v, "x", tool_input={"content": "a"})),
still("null | file_path ist null", lambda v: edit(v, None)),
still("falscher Typ| file_path ist eine Zahl", lambda v: edit(v, 5)),
still("falscher Typ| file_path ist eine Liste", lambda v: edit(v, [str(v / "CLAUDE.md")])),
still("leer | file_path ist leer", lambda v: edit(v, "")),
still("wertebereich| file_path mit Nullzeichen",
lambda v: edit(v, str(v / "CLAUDE.md") + "\u0000")),
still("zusaetzlich | Pfad ausserhalb des Vaults",
lambda v: edit(v, str(v.parent / "anderswo" / "CLAUDE.md"))),
still("verweis | Nachbarordner mit gleichem Namensanfang",
lambda v: edit(v, str(v) + "-kopie\\CLAUDE.md")),
still("verweis | Datei neben dem Vault mit gleichem Namensanfang",
lambda v: edit(v, str(v) + "-CLAUDE.md")),
still("zusaetzlich | im Vault, ausserhalb des Geltungsbereichs",
lambda v: edit(v, str(v / "Inbox" / "notiz.md"))),
still("zusaetzlich | CLAUDE.md in einem Unterordner",
lambda v: edit(v, str(v / "Code" / "CLAUDE.md"))),
still("verweis | Ordnername mit gleichem Anfang",
lambda v: edit(v, str(v / "AI" / "RegelwerkAlt" / "x.md"))),
still("zusaetzlich | settings.json unter .claude",
lambda v: edit(v, str(v / ".claude" / "settings.json"))),
still("verweis | Ordnername mit gleichem Anfang wie workflows",
lambda v: edit(v, str(v / ".claude" / "workflowsAlt" / "x.js"))),
still("wertebereich| der Regelwerk-Ordner selbst",
lambda v: edit(v, str(v / "AI" / "Regelwerk"))),
still("verweis | .. fuehrt aus dem Regelwerk hinaus",
lambda v: edit(v, vorwaerts(v) + "/AI/Regelwerk/../../Inbox/notiz.md")),
still("verweis | .. fuehrt aus dem Vault hinaus",
lambda v: edit(v, vorwaerts(v) + "/../fremd/AI/Regelwerk/x.md")),
Fall("gruen | CLAUDE.md, Check ohne Befund", "gruen",
lambda v: edit(v, str(v / "CLAUDE.md")), "still-nach-lauf"),
Fall("gruen | Skill, Check ohne Befund", "gruen",
lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md")), "still-nach-lauf"),
meldet("pfad | Schraegstriche vorwaerts",
lambda v: edit(v, vorwaerts(v) + "/CLAUDE.md")),
meldet("pfad | Backslashes",
lambda v: edit(v, str(v) + "\\AI\\Regelwerk\\regel.md")),
meldet("pfad | Laufwerksbuchstabe in anderer Schreibung",
lambda v: edit(v, laufwerk_getauscht(v / "AI" / "Regelwerk" / "regel.md"))),
meldet("pfad | ganzer Pfad in Grossbuchstaben",
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "regel.md").upper())),
meldet("pfad | Unterordner im Regelwerk",
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "unter" / "tief.md"))),
meldet("pfad | Agentendatei",
lambda v: edit(v, str(v / ".claude" / "agents" / "planer.md"))),
meldet("pfad | Skilldatei",
lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md"))),
meldet("pfad | Workflowdatei",
lambda v: edit(v, str(v / ".claude" / "workflows" / "demo.js"))),
meldet("verweis | neue Datei, die es noch nicht gibt",
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "neu.md"))),
meldet("pfad | relativer Pfad", lambda v: edit(v, "AI/Regelwerk/regel.md")),
meldet("verweis | .. fuehrt ins Regelwerk hinein",
lambda v: edit(v, vorwaerts(v) + "/Inbox/../AI/Regelwerk/regel.md")),
meldet("zusaetzlich | Write statt Edit",
lambda v: edit(v, str(v / "CLAUDE.md"), tool_name="Write")),
meldet("zusaetzlich | MultiEdit",
lambda v: edit(v, str(v / "CLAUDE.md"), tool_name="MultiEdit")),
meldet("kodierung | ungueltiges UTF-8 im Pfad",
lambda v: edit(v, "PLATZHALTER").replace(
b"PLATZHALTER", vorwaerts(v).encode("utf-8") + b"/AI/Regelwerk/regel\xff.md")),
Fall("umgebung | ungueltiges UTF-8 im Pfad bleibt gueltiges JSON ohne PYTHONIOENCODING",
"befund",
lambda v: edit(v, "PLATZHALTER").replace(
b"PLATZHALTER", vorwaerts(v).encode("utf-8") + b"/AI/Regelwerk/regel\xff.md"),
"meldung", enthaelt=BEFUND, system_enthaelt=BEFUND_SYSTEM, umgebung=ohne_pythonioencoding),
Fall("umgebung | Nicht-ASCII-Pfad ohne PYTHONIOENCODING bleibt gueltiges UTF-8-JSON",
"befund",
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "Prüfüng Übersicht 🚀.md")),
"meldung", enthaelt=BEFUND, system_enthaelt=BEFUND_SYSTEM, umgebung=ohne_pythonioencoding),
meldet("pfad | cwd mit abschliessendem Backslash",
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v) + "\\")),
meldet("fehlt | cwd fehlt, wird nicht mehr gelesen",
lambda v: ohne_cwd(v, str(v / "CLAUDE.md"))),
meldet("null | cwd ist null, wird nicht mehr gelesen",
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=None)),
meldet("falscher Typ| cwd ist eine Zahl, wird nicht mehr gelesen",
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=5)),
meldet("verweis | cwd existiert nicht, wird nicht mehr gelesen",
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v / "gibtsnicht"))),
meldet("verweis | cwd zeigt auf einen anderen Ordner, Ergebnis bleibt der Skript-Vault",
cwd_zeigt_anderswo),
Fall("check | Exit 2, Abbruch im Check", "abbruch",
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
["Abbruch: KeyError"], "Exit 2, 1 Befundzeile(n)"),
Fall("check | Exit 0 mit ACHTUNG", "achtung",
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
["ACHTUNG: nur 5 von 6"], "Exit 0, 0 Befundzeile(n)"),
Fall("check | Exit 0 ohne Schlusszeile", "stumm",
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
["(keine Ausgabe)"], "Exit 0, 0 Befundzeile(n)"),
Fall("check | Check stuerzt ab", "absturz",
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
["--- stderr ---", "RuntimeError: Absturz im Check"], "Exit 1, 0 Befundzeile(n)"),
Fall("umgebung | regelwerk_check.py fehlt", None,
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
["regelwerk_check.py fehlt"], "Regelwerk-Check fehlt"),
Fall("umgebung | regelwerk_check.py haengt", "haengt",
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
["länger als 20 s"], "nach 20 s abgebrochen", max_dauer=28.0),
Fall("wertebereich| sehr lange Ausgabe wird gekuerzt", "viel",
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
["weitere Zeilen gekürzt"], "Exit 1, 200 Befundzeile(n)"),
]
def streng_utf8(roh: bytes) -> str:
# stdout muss gueltiges UTF-8 sein. Ein ungueltiges Byte darf nicht still
# ersetzt werden, sonst besteht der Umgebungsfall auch dann, wenn der Hook
# cp1252 schreibt (Pruefrunde 2, Befund zur Dekodierung).
try:
return roh.decode("utf-8")
except UnicodeDecodeError as exc:
return "UNGUELTIGES-UTF-8: %s" % exc
def fahre(
skript: Path,
eingabe: bytes,
arbeitsordner: Path,
umgebung: dict[str, str] | None = None,
) -> tuple[int, str, str, float]:
start = time.perf_counter()
lauf = subprocess.run(
[sys.executable, str(skript)],
input=eingabe,
capture_output=True,
cwd=str(arbeitsordner),
timeout=90,
env=umgebung,
)
dauer = time.perf_counter() - start
return (
lauf.returncode,
streng_utf8(lauf.stdout),
lauf.stderr.decode("utf-8", errors="replace"),
dauer,
)
def pruefe_aufruf(aufruf: Path, v: Path) -> list[str]:
if not aufruf.exists():
return ["Check wurde nicht gestartet"]
daten = json.loads(aufruf.read_text(encoding="utf-8"))
probleme: list[str] = []
if daten.get("argv") != ["--vault", str(v)]:
probleme.append(f"Aufruf-Argumente {daten.get('argv')!r} statt {['--vault', str(v)]!r}")
if os.path.normcase(str(daten.get("exe"))) != os.path.normcase(sys.executable):
probleme.append(f"Interpreter {daten.get('exe')!r} statt {sys.executable!r}")
return probleme
def pruefe_meldung(fall: Fall, out: str) -> list[str]:
roh = out.strip()
if not (roh.startswith("{") and roh.endswith("}")):
return [f"stdout ist kein JSON-Objekt: {out[:300]!r}"]
try:
daten = json.loads(roh)
except ValueError as exc:
return [f"stdout nicht parsebar: {exc}"]
if not isinstance(daten, dict) or set(daten) != {"hookSpecificOutput", "systemMessage"}:
art = sorted(daten) if isinstance(daten, dict) else type(daten).__name__
return [f"falsche Schluessel: {art}"]
innen = daten["hookSpecificOutput"]
system = daten["systemMessage"]
if not isinstance(innen, dict) or innen.get("hookEventName") != "PostToolUse":
return ["hookSpecificOutput.hookEventName ist nicht PostToolUse"]
kontext = innen.get("additionalContext")
if not isinstance(kontext, str) or not kontext:
return ["additionalContext fehlt oder ist leer"]
probleme: list[str] = []
if not isinstance(system, str) or not system or "\n" in system:
probleme.append(f"systemMessage ist keine einzelne Zeile: {system!r}")
elif fall.system_enthaelt not in system:
probleme.append(f"systemMessage ohne {fall.system_enthaelt!r}: {system!r}")
probleme += [f"Kontext ohne {t!r}" for t in fall.enthaelt if t not in kontext]
if len(kontext) > MAX_KONTEXT:
probleme.append(f"Kontext hat {len(kontext)} Zeichen, Grenze {MAX_KONTEXT}")
if probleme:
probleme.append("Kontext war:\n" + kontext)
return probleme
def pruefe(fall: Fall, v: Path, code: int, out: str, dauer: float) -> list[str]:
probleme: list[str] = []
if code != 0:
probleme.append(f"Exit {code} statt 0")
if dauer > fall.max_dauer:
probleme.append(f"Laufzeit {dauer:.1f} s ueber {fall.max_dauer:.0f} s")
aufruf = v / "aufruf.json"
if fall.erwartung == "still-ohne-lauf":
if out != "":
probleme.append(f"stdout nicht leer: {out[:300]!r}")
if aufruf.exists():
probleme.append("Check wurde gestartet, obwohl er nicht haette laufen duerfen")
elif fall.erwartung == "still-nach-lauf":
if out != "":
probleme.append(f"stdout nicht leer: {out[:300]!r}")
probleme += pruefe_aufruf(aufruf, v)
else:
probleme += pruefe_meldung(fall, out)
if fall.fake is not None:
probleme += pruefe_aufruf(aufruf, v)
return probleme
def lauf_fall(fall: Fall, arbeit: Path) -> tuple[bool, str]:
v = arbeit / "fall"
if v.exists():
shutil.rmtree(v)
baue_vault(v, fall.fake)
# Seit der Nachbesserung Runde 2 bestimmt regelwerk_nach_edit.py seine
# Vault-Wurzel ausschliesslich aus dem eigenen Skriptpfad, das Feld cwd
# der Hook-Eingabe zaehlt nicht mehr. Nur eine Kopie im Wegwerf-Vault sorgt
# dafuer, dass diese Wurzel auch tatsaechlich v ist statt des echten
# Vaults, in dem das Original unter HOOK liegt.
ziel = v / ".vault-tools" / "session-hooks"
ziel.mkdir(parents=True, exist_ok=True)
skript = ziel / HOOK.name
shutil.copy2(HOOK, skript)
umgebung = fall.umgebung(v) if fall.umgebung is not None else None
code, out, err, dauer = fahre(skript, fall.stdin(v), arbeit, umgebung)
kopf = f"exit={code} {dauer:.2f}s"
probleme = pruefe(fall, v, code, out, dauer)
if not probleme:
return True, kopf
text = kopf + "\n" + "\n".join(" " + p for p in probleme)
if err.strip():
text += "\n stderr: " + err.strip()[:800]
return False, text
def ip_hook_wirft(arbeit: Path) -> tuple[bool, str]:
"""Fehlerpfad im Hook selbst, von aussen nicht ausloesbar: im Prozess."""
v = arbeit / "fall"
if v.exists():
shutil.rmtree(v)
baue_vault(v, "befund")
# Kopie in v, wie in lauf_fall(): main() bestimmt die Wurzel seit Runde 2
# ausschliesslich aus dem eigenen Skriptpfad, der file_path unten zeigt
# aber in v hinein - ohne die Kopie faende main() keinen Geltungsbereich.
ziel = v / ".vault-tools" / "session-hooks"
ziel.mkdir(parents=True, exist_ok=True)
kopie = ziel / HOOK.name
shutil.copy2(HOOK, kopie)
spec = importlib.util.spec_from_file_location("hook_wirft", kopie)
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
def wirft(wurzel: Path) -> None:
raise RuntimeError("Testfehler")
modul.fuehre_check = wirft
puffer = io.StringIO()
with contextlib.redirect_stdout(puffer):
code = modul.main(edit(v, str(v / "CLAUDE.md")))
erwartet = Fall("", "befund", lambda _: b"", "meldung",
["RuntimeError: Testfehler"], "regelwerk_nach_edit.py: RuntimeError")
probleme = pruefe_meldung(erwartet, puffer.getvalue())
if code != 0:
probleme.append(f"main gab {code} zurueck statt 0")
return not probleme, "\n".join(" " + p for p in probleme)
def echter_lauf() -> int:
vault = HIER.parent.parent
code, out, err, dauer = fahre(HOOK, edit(vault, str(vault / "CLAUDE.md")), HIER)
print(f"exit={code} Laufzeit {dauer:.2f} s")
if out.strip():
try:
print(json.dumps(json.loads(out), ensure_ascii=False, indent=2))
except ValueError:
print("FORMFEHLER, stdout ist kein JSON:")
print(out)
return 1
else:
print("(keine Ausgabe: Regelwerk-Check ohne Befund)")
if err.strip():
print("--- stderr ---")
print(err)
return 0 if code == 0 else 1
def main() -> int:
if not HOOK.is_file():
print(f"ABBRUCH: {HOOK} fehlt")
return 2
if "--echt" in sys.argv[1:]:
return echter_lauf()
arbeit = Path(tempfile.mkdtemp(prefix="nach-edit-faelle-")).resolve()
try:
print("=" * 78)
print("GRUNDLINIE")
print("=" * 78)
for fall in GRUNDLINIE:
ok, text = lauf_fall(fall, arbeit)
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}")
if not ok:
print("ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.")
return 2
print()
print("=" * 78)
print("ANGRIFFSLISTE")
print("=" * 78)
bestanden = 0
gesamt = 0
for fall in FAELLE:
gesamt += 1
try:
ok, text = lauf_fall(fall, arbeit)
except Exception as exc:
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
bestanden += int(ok)
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}")
gesamt += 1
try:
ok, text = ip_hook_wirft(arbeit)
except Exception as exc:
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
bestanden += int(ok)
print(f"{'BESTANDEN' if ok else 'FEHLER':10} absturz | Hook selbst wirft (im Prozess) {text}")
print()
print(f"{bestanden} von {gesamt} Faellen bestanden.")
return 0 if bestanden == gesamt else 1
finally:
shutil.rmtree(arbeit, ignore_errors=True)
if __name__ == "__main__":
try:
sys.stdout.reconfigure(encoding="utf-8")
except AttributeError:
pass
raise SystemExit(main())