Automatischer Push: 45 Datei(en) geändert

- .claude/agents/planer.md
- .claude/agents/quorum-stimme.md
- .claude/skills/bookstack-upload/SKILL.md
- .claude/skills/bookstack-upload/scripts/bookstack/archiv.py
- .claude/skills/bookstack-upload/scripts/bookstack/auswahl.py
- .claude/skills/bookstack-upload/scripts/bookstack/beschreibung.py
- .claude/skills/bookstack-upload/scripts/bookstack/bilder.py
- .claude/skills/bookstack-upload/scripts/bookstack/ergebnis.py
- .claude/skills/bookstack-upload/scripts/bookstack/hochladen.py
- .claude/skills/bookstack-upload/scripts/bookstack/konvertierung.py
- .claude/skills/bookstack-upload/scripts/bookstack/struktur.py
- .claude/skills/bookstack-upload/scripts/bookstack/verweise.py
- … und 33 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-10-01 04:00:02 +02:00
1 parent 60dee0ab68
commit deec79a6ea
45 files changed
+8645 -262

No files matched your search

@@ -0,0 +1,754 @@
"""Faelle fuer quorum_werkzeug.py: je Fall ein kleines Git-Repo oder ein Dokumentensatz im Temp, das Skript laeuft als eigener Prozess.
Die Grundlinie laeuft zuerst; scheitert sie, messen die uebrigen Faelle nichts. Kein Fall
startet Codex oder geht ins Netz: Die Codex-Faelle schreiben die Ergebnisdatei selbst, in der
Form, die codex-companion.mjs result <job> --json liefert (Plugin codex 1.0.6,
scripts/codex-companion.mjs Zeilen 910-926). Die Berichte im Temp haben das Frontmatter, das
quorum.js schreibt (Funktion kopf dort).
Aufruf: python faelle_quorum_werkzeug.py [<pfad zu quorum_werkzeug.py>]
Exit 0, wenn alle Faelle bestehen, sonst 1.
"""
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
from datetime import date
from pathlib import Path
SKRIPT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).with_name("quorum_werkzeug.py")
HEUTE = date.today().isoformat()
MARKE = "0123456789abcdef"
FAELLE = []
ZEILE_FELDER = ["ordner", "anlass", "art", "wurzel", "leer", "basis", "stand", "anzahl_dateien", "lesemarke",
"bericht_pfad", "nummer", "datum", "vorher", "suite", "hinweise", "paket_summe"]
def fall(name):
def eintragen(funktion):
FAELLE.append((name, funktion))
return funktion
return eintragen
def soll(bedingung, text):
return [] if bedingung else [text]
def schreibe(pfad, text):
pfad.parent.mkdir(parents=True, exist_ok=True)
with open(pfad, "w", encoding="utf-8", newline="\n") as datei:
datei.write(text)
def summe(pfad):
return hashlib.sha256(pfad.read_bytes()).hexdigest()
def git(wurzel, *argumente):
return subprocess.run(
["git", "-C", str(wurzel), "-c", "user.name=Quorum Test", "-c", "[email protected]",
"-c", "core.autocrlf=false", "-c", "commit.gpgsign=false", *argumente],
check=True, capture_output=True, text=True, encoding="utf-8", errors="replace").stdout.strip()
def repo(tmp):
"""Repo mit zwei Commits. A legt a.py, b.py und CHANGELOG.md an; B aendert a.py und das CHANGELOG."""
w = tmp / "repo"
w.mkdir()
git(w, "init", "-q")
schreibe(w / "a.py", "x = 1\n")
schreibe(w / "b.py", "y = 2\n")
schreibe(w / "CHANGELOG.md", "# Changelog\n\n## 1.0.0\n- erste Fassung\n")
git(w, "add", "-A")
git(w, "commit", "-q", "-m", "A")
a = git(w, "rev-parse", "HEAD")
schreibe(w / "a.py", "x = 2\n")
schreibe(w / "CHANGELOG.md", "# Changelog\n\n## 1.1.0\n- neue Zeile im Log\n\n## 1.0.0\n- erste Fassung, korrigiert\n")
git(w, "commit", "-q", "-am", "B")
return w, a, git(w, "rev-parse", "HEAD")
def bericht(ordner, name, nummer, stand, urteil="frei"):
"""Ein Bericht im Frontmatter-Format, das quorum.js schreibt."""
if isinstance(stand, list):
stand_text = "stand:\n" + "".join(f' - "{s}"\n' for s in stand)
else:
stand_text = f"stand: {stand}\n"
schreibe(ordner / name,
"---\ntype: note\n"
f'title: "Quorum {nummer}: einspielen"\n'
"status: done\n"
'summary: "Urteil frei: kein bestaetigter und kein ungepruefter blockierender Befund; Stimmen Opus, Sonnet, Codex."\n'
"tags: [quorum]\ncreated: 2026-09-20\nupdated: 2026-09-20\n"
f"nummer: {nummer}\nanlass: einspielen\nurteil: {urteil}\n"
'basis: "0000000000000000000000000000000000000000"\n'
f"{stand_text}codex: abgegeben\n---\n\n# Quorum {nummer}: einspielen\n")
def workflow_bericht(pfad, nummer, basis, stand_zeilen=("stand: PAKET",), rumpf="", zeilenende="\n"):
"""Ein Bericht, wie ihn quorum.js als bericht_md liefert: stand steht als Platzhalter PAKET."""
zeilen = ["---", "type: note", f'title: "Quorum {nummer}: einspielen"', "status: done",
'summary: "Urteil frei."', "tags: [quorum]", f"created: {HEUTE}", f"updated: {HEUTE}",
f"nummer: {nummer}", "anlass: einspielen", "urteil: frei", f'basis: "{basis}"',
*stand_zeilen, "codex: abgegeben", "---", "", f"# Quorum {nummer}: einspielen", "", rumpf]
pfad.parent.mkdir(parents=True, exist_ok=True)
with open(pfad, "w", encoding="utf-8", newline="") as datei:
datei.write(zeilenende.join(zeilen) + zeilenende)
return pfad
def fahre(*argumente):
umgebung = dict(os.environ)
umgebung["PYTHONIOENCODING"] = "utf-8"
# Das Skript muss GIT_OPTIONAL_LOCKS selbst setzen; ein Wert aus der aufrufenden Shell verdeckte das.
umgebung.pop("GIT_OPTIONAL_LOCKS", None)
lauf = subprocess.run([sys.executable, str(SKRIPT), *[str(x) for x in argumente]], capture_output=True, env=umgebung)
aus = lauf.stdout.decode("utf-8", "replace").replace("\r\n", "\n")
return lauf.returncode, aus, lauf.stderr.decode("utf-8", "replace").replace("\r\n", "\n")
def paket_json(ausgabe):
for zeile in ausgabe.splitlines():
if zeile.startswith("QUORUM_PAKET "):
return json.loads(zeile[len("QUORUM_PAKET "):])
return None
def voll(ziel):
"""Der Inhalt von paket.json, oder None."""
pfad = ziel / "paket.json"
return json.loads(pfad.read_text(encoding="utf-8")) if pfad.exists() else None
def code_paket(tmp, w, *zusatz):
ziel = Path(tempfile.mkdtemp(dir=tmp))
code, aus, err = fahre("paket", "--anlass", "einspielen", "--projekt", w, "--ziel", ziel, *zusatz)
return code, aus + err, paket_json(aus), ziel
def dokusatz(tmp):
o = tmp / "kunde"
schreibe(o / "00-uebersicht.md", "# Uebersicht\n")
schreibe(o / "kapitel" / "01.md", "# Eins\n")
schreibe(o / "kapitel" / "02.md", "# Zwei\n")
return o
def doku_paket(tmp, o, *dateien, uebersicht="00-uebersicht.md"):
ziel = Path(tempfile.mkdtemp(dir=tmp))
code, aus, err = fahre("paket", "--anlass", "kundendokument", "--ordner", o, "--uebersicht", uebersicht, "--ziel", ziel, "--dateien", *dateien)
return code, aus + err, paket_json(aus), ziel
def ablegen(paket_ordner, bericht_datei, ziel, paket_summe=None):
"""ablegen mit --summe; ohne Angabe die SHA-256 von paket.json im Paketordner, Nullen, wenn es fehlt."""
if paket_summe is None:
pfad = Path(paket_ordner) / "paket.json"
paket_summe = summe(pfad) if pfad.exists() else "0" * 64
code, aus, err = fahre("ablegen", "--paket", paket_ordner, "--bericht", bericht_datei, "--ziel", ziel, "--summe", paket_summe)
return code, aus + err
ANTWORT = {"lesebeleg": f"Lesemarke: {MARKE}", "befunde": [{"datei": "a.py", "stelle": "1", "schwere": "Wichtig", "aussage": "x ist falsch", "beleg": "x = 2"}], "nicht_pruefbar": []}
ANTWORT_LIMIT = {**ANTWORT, "befunde": [{**ANTWORT["befunde"][0], "aussage": "zaehlt jedes usage limit im Log", "beleg": 'USAGE_LIMIT = re.compile(r"usage limit", re.I)'}]}
LOG_FEHLALARM = (
"[2026-09-30T10:00:00.000Z] Starting Codex task.\n"
'[2026-09-30T10:00:05.000Z] Running command: rg -n "usage limit" .vault-tools/quorum\n'
"[2026-09-30T10:01:00.000Z] Assistant message captured: Der Zaehler fuer usage limit liest das ganze Log.\n"
"\n[2026-09-30T10:01:00.000Z] Assistant message\n"
"Der Zaehler fuer usage limit liest das ganze Log.\n"
"Codex error: You've hit your usage limit. (Zitat aus einem alten Log)\n"
)
def block(antwort):
return "QUORUM-ANTWORT-ANFANG\n" + json.dumps(antwort, ensure_ascii=False) + "\nQUORUM-ANTWORT-ENDE\n"
def ergebnisdatei(tmp, roh, status="completed", log="[2026-09-30T10:00:00Z] Turn completed.\n", mit_log=True):
logdatei = tmp / "job.log"
if mit_log:
schreibe(logdatei, log)
job = {"id": "task-q1", "status": status, "logFile": logdatei.as_posix(), "startedAt": "2026-09-30T10:00:00.000Z", "completedAt": "2026-09-30T10:03:12.000Z"}
pfad = tmp / "ergebnis.json"
schreibe(pfad, json.dumps({"job": job, "storedJob": {**job, "result": {"status": 0, "rawOutput": roh}}}))
return pfad
def auswerten(pfad):
code, aus, err = fahre("codex", "--ergebnis", pfad)
zeilen = dict(z.split(": ", 1) for z in aus.splitlines() if ": " in z and not z.startswith((" ", "{", "}")))
antwort = None
if "ANTWORT-JSON:\n" in aus:
rest = aus.split("ANTWORT-JSON:\n", 1)[1].strip()
antwort = None if rest == "keine" else json.loads(rest)
return code, aus + err, zeilen, antwort
# ---------------------------------------------------------------- paket, Code
@fall("Grundlinie: Code mit Vorbericht, Diff, CHANGELOG, Lesemarke und Codex-Auftrag")
def f_grundlinie(tmp):
w, a, b = repo(tmp)
bericht(w / "docs" / "quorum", "2026-09-20-einspielen.md", 1, a)
git(w, "add", "-A")
git(w, "commit", "-q", "-m", "Bericht 1")
code, text, p, ziel = code_paket(tmp, w)
f = soll(code == 0 and p is not None, f"Exit {code}: {text}")
if not p:
return f
v = voll(ziel) or {}
f += soll(p["basis"] == a and p["stand"] == git(w, "rev-parse", "HEAD") and p["leer"] is False, f"basis/stand/leer {p['basis']} {p['stand']} {p['leer']}")
f += soll(v.get("dateien") == [{"pfad": "CHANGELOG.md", "art": "geaendert"}, {"pfad": "a.py", "art": "geaendert"}] and p["anzahl_dateien"] == 2, f"dateien {v.get('dateien')} {p['anzahl_dateien']}")
f += soll(p["nummer"] == 2 and p["vorher"]["nummer"] == 1 and p["vorher"]["urteil"] == "frei" and p["art"] == "code", f"nummer/vorher {p['nummer']} {p['vorher']}")
f += soll(p["bericht_pfad"] == (w / "docs" / "quorum" / f"{HEUTE}-einspielen.md").as_posix(), f"bericht_pfad {p['bericht_pfad']}")
md = (ziel / "paket.md").read_text(encoding="utf-8")
auftrag = (ziel / "codex-auftrag.md").read_text(encoding="utf-8")
diff = (ziel / "diff.patch").read_text(encoding="utf-8")
f += soll(re.fullmatch(r"[0-9a-f]{16}", p["lesemarke"]) is not None and f"\nLesemarke: {p['lesemarke']}\n" in md, "Lesemarke fehlt in paket.md")
f += soll(p["lesemarke"] not in auftrag, "Lesemarke steht im Codex-Auftrag")
f += soll((ziel / "paket.md").as_posix() in auftrag and "QUORUM-ANTWORT-ANFANG" in auftrag and "QUORUM-ANTWORT-ENDE" in auftrag, "Codex-Auftrag ohne Paketpfad oder Marken")
f += soll("- neue Zeile im Log" in md and "- erste Fassung, korrigiert" in md and "\n- erste Fassung\n" not in md, "CHANGELOG-Zeilen falsch")
f += soll("+x = 2" in diff and "docs/quorum" not in diff, "diff.patch falsch")
f += soll(sorted(p) == sorted(ZEILE_FELDER), f"Felder der Zeile {sorted(p)}")
f += soll({k: x for k, x in v.items() if k not in ("dateien", "stand_liste")} == {k: x for k, x in p.items() if k != "paket_summe"} and v.get("stand_liste", 0) is None, "paket.json weicht von QUORUM_PAKET ab")
f += soll(p.get("paket_summe") == hashlib.sha256((ziel / "paket.json").read_bytes()).hexdigest(), f"paket_summe {p.get('paket_summe')} passt nicht zu paket.json")
return f
@fall("Erstes Quorum ohne --basis: Befund, kein Paket")
def f_erstes_ohne_basis(tmp):
w, a, b = repo(tmp)
code, text, p, ziel = code_paket(tmp, w)
return soll(code == 2 and "--basis angeben" in text and not (ziel / "paket.md").exists() and "Traceback" not in text, f"Exit {code}: {text}")
@fall("Erstes Quorum mit --basis: nummer 1, kein Vorbericht")
def f_erstes_mit_basis(tmp):
w, a, b = repo(tmp)
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
return soll(code == 0 and p is not None and p["nummer"] == 1 and p["vorher"] is None and p["basis"] == a and p["anzahl_dateien"] == 2, f"Exit {code}: {text}")
@fall("Erstes Quorum mit --basis HEAD: leerer Unterschied ist ein Befund")
def f_erstes_leer(tmp):
w, a, b = repo(tmp)
code, text, p, ziel = code_paket(tmp, w, "--basis", "HEAD")
return soll(code == 2 and "leerem Unterschied" in text, f"Exit {code}: {text}")
@fall("Unbekannte basis: Befund ohne Traceback")
def f_basis_unbekannt(tmp):
w, a, b = repo(tmp)
code, text, p, ziel = code_paket(tmp, w, "--basis", "gibt-es-nicht")
return soll(code == 2 and "BEFUND" in text and "rev-parse" in text and "gibt-es-nicht" in text and "Traceback" not in text, f"Exit {code}: {text}")
@fall("Hinweise: basis weicht vom letzten Stand ab, basis ist kein Vorfahr; gleiche basis ohne Hinweis")
def f_hinweise(tmp):
w, a, b = repo(tmp)
git(w, "checkout", "-q", "-b", "seite", a)
schreibe(w / "s.py", "s = 1\n")
git(w, "add", "-A")
git(w, "commit", "-q", "-m", "S")
s = git(w, "rev-parse", "HEAD")
git(w, "checkout", "-q", "-")
bericht(w / "docs" / "quorum", "2026-09-20-einspielen.md", 1, a)
f = []
for basis, weicht, vorfahr in (("HEAD~1", False, False), (s, True, True)):
code, text, p, ziel = code_paket(tmp, w, "--basis", basis)
h = " ".join(p["hinweise"]) if p else ""
f += soll(code == 0 and p is not None and ("basis weicht" in h) == weicht and ("kein Vorfahr" in h) == vorfahr, f"basis {basis}: Exit {code}: {h} {text}")
return f
@fall("Leerer Unterschied mit unversioniertem Bericht: leer, Urteil des Vorberichts reicht durch")
def f_leer(tmp):
w, a, b = repo(tmp)
bericht(w / "docs" / "quorum", "2026-09-20-einspielen.md", 1, b, urteil="nicht_frei")
code, text, p, ziel = code_paket(tmp, w)
return soll(code == 0 and p is not None and p["leer"] is True and p["anzahl_dateien"] == 0 and p["vorher"]["urteil"] == "nicht_frei" and p["nummer"] == 2, f"Exit {code}: {text}")
@fall("Seit der Basis nur docs/quorum geaendert: leer")
def f_nur_berichte(tmp):
w, a, b = repo(tmp)
bericht(w / "docs" / "quorum", "2026-09-20-einspielen.md", 1, b)
git(w, "add", "-A")
git(w, "commit", "-q", "-m", "Bericht 1")
code, text, p, ziel = code_paket(tmp, w)
return soll(code == 0 and p is not None and p["leer"] is True and p["basis"] == b, f"Exit {code}: {text}")
@fall("Geaenderte Datei im Arbeitsbaum: Befund")
def f_unsauber_geaendert(tmp):
w, a, b = repo(tmp)
schreibe(w / "a.py", "x = 3\n")
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
return soll(code == 2 and "nicht sauber" in text, f"Exit {code}: {text}")
@fall("Unversionierte Datei ausserhalb docs/quorum: Befund")
def f_unsauber_neu(tmp):
w, a, b = repo(tmp)
schreibe(w / "neu.py", "z = 1\n")
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
return soll(code == 2 and "nicht sauber" in text and "neu.py" in text, f"Exit {code}: {text}")
@fall("Unversionierte Datei bei status.showUntrackedFiles=no: trotzdem Befund")
def f_unsauber_verborgen(tmp):
w, a, b = repo(tmp)
git(w, "config", "status.showUntrackedFiles", "no")
schreibe(w / "neu.py", "z = 1\n")
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
return soll(code == 2 and "nicht sauber" in text and "neu.py" in text, f"Exit {code}: {text}")
@fall("Hoechste nummer gewinnt, an erster und an letzter Stelle der Dateiliste")
def f_hoechste_nummer(tmp):
f = []
for erste, letzte in ((3, 2), (2, 3)):
teil = tmp / f"r{erste}"
teil.mkdir()
w, a, b = repo(teil)
bericht(w / "docs" / "quorum", "2026-09-01-version.md", erste, b if erste == 3 else a)
bericht(w / "docs" / "quorum", "2026-09-30-einspielen.md", letzte, b if letzte == 3 else a)
code, text, p, ziel = code_paket(teil, w)
f += soll(code == 0 and p is not None and p["basis"] == b and p["nummer"] == 4 and p["vorher"]["nummer"] == 3, f"erste {erste}, letzte {letzte}: Exit {code}: {text}")
return f
@fall("Zwei Berichte mit derselben nummer: Befund")
def f_nummer_doppelt(tmp):
w, a, b = repo(tmp)
bericht(w / "docs" / "quorum", "2026-09-01-version.md", 1, a)
bericht(w / "docs" / "quorum", "2026-09-30-einspielen.md", 1, b)
code, text, p, ziel = code_paket(tmp, w)
return soll(code == 2 and "zwei Berichten" in text, f"Exit {code}: {text}")
@fall("Bericht ohne nummer, mit nummer als Wort und Datei ohne Frontmatter: je ein Befund")
def f_bericht_kaputt(tmp):
f = []
for name, inhalt, erwartet in (
("ohne-nummer", "---\ntype: note\nstand: abc\n---\n", "nummer fehlt"),
("nummer-wort", "---\nnummer: drei\nstand: abc\n---\n", "nummer fehlt"),
("ohne-kopf", "# nur Text\n", "kein Frontmatter"),
):
teil = tmp / name
teil.mkdir()
w, a, b = repo(teil)
schreibe(w / "docs" / "quorum" / f"{name}.md", inhalt)
code, text, p, ziel = code_paket(teil, w)
f += soll(code == 2 and erwartet in text and "Traceback" not in text, f"{name}: Exit {code}: {text}")
return f
@fall("Stand eines Code-Berichts als Liste: Befund")
def f_code_stand_liste(tmp):
w, a, b = repo(tmp)
bericht(w / "docs" / "quorum", "2026-09-20-einspielen.md", 1, ["0" * 64 + " a.py"])
code, text, p, ziel = code_paket(tmp, w)
return soll(code == 2 and "erwartet ist ein Commit" in text, f"Exit {code}: {text}")
@fall("Neue, geaenderte und entfernte Datei im Unterschied")
def f_arten(tmp):
w, a, b = repo(tmp)
schreibe(w / "c.py", "z = 3\n")
(w / "b.py").unlink()
git(w, "add", "-A")
git(w, "commit", "-q", "-m", "C")
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
arten = {d["pfad"]: d["art"] for d in (voll(ziel) or {}).get("dateien", [])}
return soll(code == 0 and arten == {"CHANGELOG.md": "geaendert", "a.py": "geaendert", "b.py": "entfernt", "c.py": "neu"}, f"Exit {code}: {arten} {text}")
@fall("Umbenannte Datei: alter Pfad entfernt, neuer Pfad neu")
def f_umbenannt(tmp):
w, a, b = repo(tmp)
git(w, "config", "diff.renames", "true")
git(w, "mv", "b.py", "umbenannt.py")
git(w, "commit", "-q", "-m", "C")
code, text, p, ziel = code_paket(tmp, w, "--basis", b)
arten = {d["pfad"]: d["art"] for d in (voll(ziel) or {}).get("dateien", [])}
return soll(code == 0 and arten == {"b.py": "entfernt", "umbenannt.py": "neu"}, f"Exit {code}: {arten} {text}")
@fall("Berichtspfad schon belegt: Zaehler -2")
def f_pfad_belegt(tmp):
w, a, b = repo(tmp)
bericht(w / "docs" / "quorum", f"{HEUTE}-einspielen.md", 1, a)
code, text, p, ziel = code_paket(tmp, w)
return soll(code == 0 and p is not None and p["bericht_pfad"].endswith(f"/docs/quorum/{HEUTE}-einspielen-2.md"), f"Exit {code}: {text}")
@fall("Suite-Datei: letzte EXIT-Zeile und genau die letzten 80 Zeilen stehen im Paket")
def f_suite(tmp):
w, a, b = repo(tmp)
suite = tmp / "suite.txt"
schreibe(suite, "EXIT suite: 0\n" + "".join(f"zeile {n}\n" for n in range(1, 120)) + "EXIT suite: 1\n")
code, text, p, ziel = code_paket(tmp, w, "--basis", a, "--suite-datei", suite)
md = (ziel / "paket.md").read_text(encoding="utf-8") if (ziel / "paket.md").exists() else ""
f = soll(code == 0 and p is not None and p["suite"] == "exit 1", f"Exit {code}: {text}")
return f + soll("Die letzten 80 Zeilen der Ausgabe:" in md and "\nzeile 41\n" in md and "\nzeile 40\n" not in md and "\nEXIT suite: 1\n```\n" in md, f"Zeilen im Paket: {md[-400:]}")
@fall("Suite-Datei fehlt: Befund")
def f_suite_fehlt(tmp):
w, a, b = repo(tmp)
code, text, p, ziel = code_paket(tmp, w, "--basis", a, "--suite-datei", tmp / "gibt-es-nicht.txt")
return soll(code == 2 and "suite-datei" in text, f"Exit {code}: {text}")
@fall("Kontext: vorhandene Datei steht im Paket, fehlende ist ein Befund")
def f_kontext(tmp):
w, a, b = repo(tmp)
plan = tmp / "plan" / "00-uebersicht.md"
schreibe(plan, "# Plan\n")
code, text, p, ziel = code_paket(tmp, w, "--basis", a, "--kontext", plan)
md = (ziel / "paket.md").read_text(encoding="utf-8") if (ziel / "paket.md").exists() else ""
f = soll(code == 0 and f"- {plan.as_posix()}" in md, f"Exit {code}: {text}")
code, text, p, ziel = code_paket(tmp, w, "--basis", a, "--kontext", plan, tmp / "fehlt.md")
return f + soll(code == 2 and "kontext fehlt" in text, f"Exit {code}: {text}")
@fall("projekt ist ein Unterordner des Repos: Befund")
def f_unterordner(tmp):
w, a, b = repo(tmp)
schreibe(w / "unter" / "d.py", "d = 1\n")
git(w, "add", "-A")
git(w, "commit", "-q", "-m", "C")
ziel = Path(tempfile.mkdtemp(dir=tmp))
code, aus, err = fahre("paket", "--anlass", "version", "--projekt", w / "unter", "--basis", a, "--ziel", ziel)
return soll(code == 2 and "nicht die Wurzel" in aus, f"Exit {code}: {aus} {err}")
@fall("Ziel mit vorhandenem paket.md: Befund, nichts ueberschrieben")
def f_ziel_belegt(tmp):
w, a, b = repo(tmp)
ziel = Path(tempfile.mkdtemp(dir=tmp))
schreibe(ziel / "paket.md", "alt\n")
code, aus, err = fahre("paket", "--anlass", "einspielen", "--projekt", w, "--basis", a, "--ziel", ziel)
return soll(code == 2 and "schon ein paket.md" in aus and (ziel / "paket.md").read_text(encoding="utf-8") == "alt\n", f"Exit {code}: {aus} {err}")
@fall("paket laesst .git/index unberuehrt, auch wo git status ihn auffrischen wuerde (GIT_OPTIONAL_LOCKS=0)")
def f_index_unberuehrt(tmp):
"""Zwei gleiche Repos mit veralteten Stat-Daten fuer a.py. Die Gegenprobe zeigt, dass ein
gewoehnliches git status dort .git/index schreibt; ohne sie misst der zweite Teil nichts."""
f = []
for teil in ("gegenprobe", "paket"):
(tmp / teil).mkdir()
w, a, b = repo(tmp / teil)
zeit = (w / "a.py").stat().st_mtime - 1000
os.utime(w / "a.py", (zeit, zeit))
index = w / ".git" / "index"
vorher = summe(index)
if teil == "gegenprobe":
umgebung = {k: v for k, v in os.environ.items() if k != "GIT_OPTIONAL_LOCKS"}
subprocess.run(["git", "-C", str(w), "status", "--porcelain"], check=True, capture_output=True, env=umgebung)
f += soll(summe(index) != vorher, "Gegenprobe: git status ohne GIT_OPTIONAL_LOCKS=0 laesst .git/index unveraendert; der Fall misst so nichts")
else:
code, text, p, ziel = code_paket(tmp / teil, w, "--basis", a)
f += soll(code == 0 and p is not None, f"Exit {code}: {text}")
f += soll(summe(index) == vorher, "paket hat .git/index geschrieben")
return f
# ---------------------------------------------------------------- paket, Kundendokument
@fall("Kundendokument, erstes Quorum: alle Dateien neu, Stand sortiert, kein Diff")
def f_doku_erstes(tmp):
o = dokusatz(tmp)
code, text, p, ziel = doku_paket(tmp, o, "kapitel/02.md", "kapitel/01.md")
if not p:
return [f"Exit {code}: {text}"]
v = voll(ziel) or {}
liste = [f"{summe(o / 'kapitel' / '01.md')} kapitel/01.md", f"{summe(o / 'kapitel' / '02.md')} kapitel/02.md"]
f = soll(code == 0 and p["art"] == "doku" and p["basis"] == "erstes Quorum" and p["nummer"] == 1 and p["vorher"] is None, f"Kopf {p}")
f += soll(v.get("stand_liste") == liste and p["stand"] == "sha256:" + hashlib.sha256("\n".join(sorted(liste)).encode("utf-8")).hexdigest(), f"stand {p['stand']} {v.get('stand_liste')}")
f += soll(sorted(d["art"] for d in v.get("dateien", [])) == ["neu", "neu"] and p["anzahl_dateien"] == 2 and p["leer"] is False, f"dateien {v.get('dateien')}")
f += soll(p["bericht_pfad"] == (o / "quorum" / f"{HEUTE}-kundendokument.md").as_posix() and not (ziel / "diff.patch").exists(), f"bericht_pfad {p['bericht_pfad']}")
return f
@fall("Kundendokument: stand in der Zeile ist die Pruefsumme ueber die sortierte Stand-Liste")
def f_doku_stand_summe(tmp):
o = dokusatz(tmp)
eins = summe(o / "kapitel" / "01.md")
for n in range(200):
schreibe(o / "kapitel" / "02.md", f"# Zwei {n}\n")
if summe(o / "kapitel" / "02.md") < eins:
break
liste = [f"{eins} kapitel/01.md", f"{summe(o / 'kapitel' / '02.md')} kapitel/02.md"]
richtig = "sha256:" + hashlib.sha256("\n".join(sorted(liste)).encode("utf-8")).hexdigest()
falsch = "sha256:" + hashlib.sha256("\n".join(liste).encode("utf-8")).hexdigest()
f = soll(richtig != falsch, "Fixture unterscheidet sortiert und unsortiert nicht")
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md")
v = voll(ziel) or {}
f += soll(code == 0 and p is not None and p["stand"] == richtig, f"Exit {code}: stand {p['stand'] if p else None} {text}")
return f + soll(v.get("stand_liste") == liste, f"stand_liste {v.get('stand_liste')}")
@fall("Kundendokument, zweites Quorum: geaendert, neu und entfernt, unveraendert fehlt")
def f_doku_zweites(tmp):
o = dokusatz(tmp)
schreibe(o / "kapitel" / "03.md", "# Drei\n")
alt = [f"{summe(o / 'kapitel' / n)} kapitel/{n}" for n in ("01.md", "02.md", "03.md")]
bericht(o / "quorum", "2026-09-20-kundendokument.md", 1, alt)
schreibe(o / "kapitel" / "01.md", "# Eins, neu gefasst\n")
schreibe(o / "kapitel" / "04.md", "# Vier\n")
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md", "kapitel/04.md")
arten = {d["pfad"]: d["art"] for d in (voll(ziel) or {}).get("dateien", [])}
return soll(code == 0 and arten == {"kapitel/01.md": "geaendert", "kapitel/04.md": "neu", "kapitel/03.md": "entfernt"} and p["basis"] == "2026-09-20-kundendokument.md" and p["nummer"] == 2, f"Exit {code}: {arten} {text}")
@fall("Kundendokument ohne Aenderung: leer")
def f_doku_leer(tmp):
o = dokusatz(tmp)
stand = [f"{summe(o / 'kapitel' / n)} kapitel/{n}" for n in ("01.md", "02.md")]
bericht(o / "quorum", "2026-09-20-kundendokument.md", 1, stand)
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md")
return soll(code == 0 and p is not None and p["leer"] is True and p["anzahl_dateien"] == 0, f"Exit {code}: {text}")
@fall("Kundendokument: Pfad nach aussen, absoluter Pfad, fehlende und doppelte Datei sind Befunde")
def f_doku_pfade(tmp):
o = dokusatz(tmp)
schreibe(tmp / "draussen.md", "# Draussen\n")
f = []
for dateien, erwartet in (
(["../draussen.md"], "nur Pfade innerhalb"),
([(tmp / "draussen.md").as_posix()], "nur Pfade innerhalb"),
(["kapitel/09.md"], "Datei fehlt"),
(["kapitel/01.md", "kapitel/01.md"], "doppelt"),
):
code, text, p, ziel = doku_paket(tmp, o, *dateien)
f += soll(code == 2 and erwartet in text and "Traceback" not in text, f"{dateien}: Exit {code}: {text}")
return f
@fall("Kundendokument: fehlende Uebersicht und --projekt sind Befunde")
def f_doku_args(tmp):
o = dokusatz(tmp)
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", uebersicht="fehlt.md")
f = soll(code == 2 and "uebersicht fehlt" in text, f"uebersicht: Exit {code}: {text}")
ziel = Path(tempfile.mkdtemp(dir=tmp))
code, aus, err = fahre("paket", "--anlass", "kundendokument", "--ordner", o, "--uebersicht", "00-uebersicht.md", "--dateien", "kapitel/01.md", "--projekt", tmp, "--ziel", ziel)
return f + soll(code == 2 and "gelten nicht fuer kundendokument" in aus, f"projekt: Exit {code}: {aus} {err}")
@fall("Kundendokument: stand als Text oder ohne Pruefsumme ist ein Befund")
def f_doku_stand_kaputt(tmp):
f = []
for name, stand, erwartet in (("text", "abc", "keine Liste"), ("ohne-summe", ["kapitel/01.md"], "unlesbar")):
teil = tmp / name
teil.mkdir()
o = dokusatz(teil)
bericht(o / "quorum", "2026-09-20-kundendokument.md", 1, stand)
code, text, p, ziel = doku_paket(teil, o, "kapitel/01.md")
f += soll(code == 2 and erwartet in text, f"{name}: Exit {code}: {text}")
return f
# ---------------------------------------------------------------- ablegen
@fall("ablegen, Code: stand PAKET wird der Commit, CRLF wird LF, Ordner angelegt, naechster Lauf ist leer")
def f_ablegen_code(tmp):
w, a, b = repo(tmp)
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
if not p:
return [f"paket: Exit {code}: {text}"]
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"], zeilenende="\r\n")
code, text = ablegen(ziel, quelle, p["bericht_pfad"], p["paket_summe"])
abgelegt = Path(p["bericht_pfad"])
roh = abgelegt.read_bytes() if abgelegt.exists() else b""
erwartet = quelle.read_bytes().replace(b"\r\n", b"\n").replace(b"\nstand: PAKET\n", f"\nstand: {b}\n".encode("ascii"))
f = soll(code == 0 and roh == erwartet, f"Exit {code}: {text} {roh[:400]!r}")
code, text, p2, ziel2 = code_paket(tmp, w)
return f + soll(code == 0 and p2 is not None and p2["leer"] is True and p2["basis"] == b and p2["nummer"] == 2, f"zweiter Lauf: Exit {code}: {text}")
@fall("ablegen, Kundendokument: stand PAKET wird die Liste, nur im Frontmatter, naechster Lauf ist leer")
def f_ablegen_doku(tmp):
o = dokusatz(tmp)
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md")
if not p:
return [f"paket: Exit {code}: {text}"]
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"], rumpf="stand: PAKET")
code, text = ablegen(ziel, quelle, p["bericht_pfad"], p["paket_summe"])
abgelegt = Path(p["bericht_pfad"])
inhalt = abgelegt.read_text(encoding="utf-8") if abgelegt.exists() else ""
liste = "".join(f' - "{summe(o / "kapitel" / n)} kapitel/{n}"\n' for n in ("01.md", "02.md"))
f = soll(code == 0 and f'basis: "erstes Quorum"\nstand:\n{liste}codex: abgegeben\n' in inhalt and inhalt.rstrip("\n").endswith("\nstand: PAKET"), f"Exit {code}: {text} {inhalt[:600]}")
code, text, p2, ziel2 = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md")
return f + soll(code == 0 and p2 is not None and p2["leer"] is True and p2["nummer"] == 2, f"zweiter Lauf: Exit {code}: {text}")
@fall("ablegen: falsche nummer, falsche basis, stand PAKET fehlt oder doppelt, Ziel vorhanden, ohne Frontmatter, ohne paket.json sind Befunde; nichts geschrieben")
def f_ablegen_befunde(tmp):
w, a, b = repo(tmp)
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
if not p:
return [f"paket: Exit {code}: {text}"]
vorhanden = tmp / "vorhanden.md"
schreibe(vorhanden, "alt\n")
f = []
for name, nummer, basis, stand_zeilen, nach, erwartet in (
("nummer", p["nummer"] + 1, p["basis"], ("stand: PAKET",), tmp / "n" / "x.md", "nummer"),
("basis", p["nummer"], b, ("stand: PAKET",), tmp / "b" / "x.md", "basis"),
("ohne-stand", p["nummer"], p["basis"], ("stand: x",), tmp / "o" / "x.md", "stand: PAKET"),
("stand-doppelt", p["nummer"], p["basis"], ("stand: PAKET", "stand: PAKET"), tmp / "d" / "x.md", "stand: PAKET"),
("ziel-da", p["nummer"], p["basis"], ("stand: PAKET",), vorhanden, "gibt es schon"),
):
quelle = workflow_bericht(tmp / f"{name}.md", nummer, basis, stand_zeilen)
code, text = ablegen(ziel, quelle, nach)
unberuehrt = nach.read_text(encoding="utf-8") == "alt\n" if nach == vorhanden else not nach.parent.exists()
f += soll(code == 2 and "BEFUND" in text and erwartet in text and unberuehrt and "Traceback" not in text, f"{name}: Exit {code}: {text}")
ohne_kopf = tmp / "ohne-kopf.md"
schreibe(ohne_kopf, "# nur Text\nstand: PAKET\n")
code, text = ablegen(ziel, ohne_kopf, tmp / "k" / "x.md")
f += soll(code == 2 and "ohne Frontmatter" in text and not (tmp / "k").exists(), f"ohne-kopf: Exit {code}: {text}")
leer = Path(tempfile.mkdtemp(dir=tmp))
code, text = ablegen(leer, tmp / "nummer.md", tmp / "p" / "x.md")
return f + soll(code == 2 and "paket.json nicht lesbar" in text and not (tmp / "p").exists(), f"ohne-paket: Exit {code}: {text}")
@fall("ablegen: --summe eines anderen Pakets, umformatiertes paket.json oder fehlendes --summe: Exit 2, nichts geschrieben")
def f_ablegen_summe(tmp):
w, a, b = repo(tmp)
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
code2, text2, p2, ziel2 = code_paket(tmp, w, "--basis", a)
if not p or not p2:
return [f"paket: Exit {code}/{code2}: {text} {text2}"]
f = soll(p["nummer"] == p2["nummer"] and p["basis"] == p2["basis"] and p["paket_summe"] != p2["paket_summe"], "Fixture: Die beiden Pakete sollen sich in paket_summe unterscheiden, nicht in nummer und basis")
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"])
code, text = ablegen(ziel2, quelle, tmp / "fremd" / "x.md", p["paket_summe"])
f += soll(code == 2 and "BEFUND" in text and "weicht von der SHA-256" in text and not (tmp / "fremd").exists(), f"fremdes Paket: Exit {code}: {text}")
schreibe(ziel / "paket.json", (ziel / "paket.json").read_text(encoding="utf-8") + "\n")
code, text = ablegen(ziel, quelle, tmp / "umformatiert" / "x.md", p["paket_summe"])
f += soll(code == 2 and "weicht von der SHA-256" in text and not (tmp / "umformatiert").exists(), f"paket.json umformatiert: Exit {code}: {text}")
code, aus, err = fahre("ablegen", "--paket", ziel2, "--bericht", quelle, "--ziel", tmp / "ohne" / "x.md")
return f + soll(code == 2 and "--summe" in aus + err and not (tmp / "ohne").exists(), f"ohne --summe: Exit {code}: {aus} {err}")
# ---------------------------------------------------------------- codex
@fall("Codex: completed, Log ohne usage limit, Antwort gelesen, Schwere klein geschrieben")
def f_codex_grund(tmp):
code, text, z, antwort = auswerten(ergebnisdatei(tmp, "Ich habe geprueft.\n" + block(ANTWORT)))
f = soll(code == 0 and z.get("job_status") == "completed" and z.get("log_gelesen") == "ja" and z.get("usage_limit_treffer") == "0", f"Exit {code}: {text}")
f += soll(z.get("antwort_gefunden") == "ja" and z.get("dauer_s") == "192", f"antwort/dauer {z}")
return f + soll(antwort is not None and antwort["lesebeleg"] == f"Lesemarke: {MARKE}" and antwort["befunde"][0]["schwere"] == "wichtig", f"antwort {antwort}")
@fall("Codex: usage limit im Log, gross geschrieben")
def f_codex_limit(tmp):
code, text, z, antwort = auswerten(ergebnisdatei(tmp, block(ANTWORT), log="[x] Codex error: You've hit your Usage Limit. Try again later.\n"))
return soll(code == 0 and z.get("usage_limit_treffer") == "1", f"Exit {code}: {text}")
@fall("Codex: usage limit nur in Befehl, Nachricht und Final output zaehlt nicht")
def f_codex_limit_fehlalarm(tmp):
roh = block(ANTWORT_LIMIT)
log = LOG_FEHLALARM + "\n[2026-09-30T10:02:00.000Z] Final output\n" + roh + "[2026-09-30T10:02:01.000Z] Turn completed.\n"
code, text, z, antwort = auswerten(ergebnisdatei(tmp, roh, log=log))
return soll(code == 0 and z.get("job_status") == "completed" and z.get("usage_limit_treffer") == "0" and z.get("antwort_gefunden") == "ja", f"Exit {code}: {text}")
@fall("Codex: echte Fehlerzeile nach anderen Zeilen, im Final output wiederholt, zaehlt einmal")
def f_codex_limit_echt(tmp):
log = ("[2026-09-05T11:37:00.000Z] Starting Codex task.\n"
'[2026-09-05T11:37:10.000Z] Running command: rg -n "TODO" .\n'
"[2026-09-05T11:37:32.037Z] Codex error: You've hit your usage limit. Upgrade to Plus or try again later.\n"
"\n[2026-09-05T11:37:32.100Z] Final output\n"
"Codex error: You've hit your usage limit. Upgrade to Plus or try again later.\n")
code, text, z, antwort = auswerten(ergebnisdatei(tmp, "", status="failed", log=log))
return soll(code == 0 and z.get("usage_limit_treffer") == "1", f"Exit {code}: {text}")
@fall("Codex: Log fehlt")
def f_codex_ohne_log(tmp):
code, text, z, antwort = auswerten(ergebnisdatei(tmp, block(ANTWORT), mit_log=False))
return soll(code == 0 and z.get("log_gelesen") == "nein", f"Exit {code}: {text}")
@fall("Codex: Zwischenmeldung, kaputtes JSON, unbekannte Schwere, fehlender Beleg und fehlende Ende-Marke ergeben keine Antwort")
def f_codex_ohne_antwort(tmp):
f = []
falsch_schwere = {**ANTWORT, "befunde": [{**ANTWORT["befunde"][0], "schwere": "high"}]}
ohne_beleg = {**ANTWORT, "befunde": [{k: v for k, v in ANTWORT["befunde"][0].items() if k != "beleg"}]}
for name, roh in (
("zwischenmeldung", "Ich pruefe nun die Dateien im Projekt."),
("kaputtes-json", "QUORUM-ANTWORT-ANFANG\n{kein json\nQUORUM-ANTWORT-ENDE\n"),
("schwere-high", block(falsch_schwere)),
("ohne-beleg", block(ohne_beleg)),
("ohne-ende", "QUORUM-ANTWORT-ANFANG\n" + json.dumps(ANTWORT)),
):
teil = tmp / name
teil.mkdir()
code, text, z, antwort = auswerten(ergebnisdatei(teil, roh))
f += soll(code == 0 and z.get("antwort_gefunden") == "nein" and antwort is None and z.get("fehler"), f"{name}: Exit {code}: {text}")
return f
@fall("Codex: zwei Bloecke, der letzte zaehlt")
def f_codex_zwei_bloecke(tmp):
zweite = {**ANTWORT, "lesebeleg": "Lesemarke: ffffffffffffffff"}
code, text, z, antwort = auswerten(ergebnisdatei(tmp, block(ANTWORT) + "Nachtrag\n" + block(zweite)))
return soll(code == 0 and antwort is not None and antwort["lesebeleg"] == "Lesemarke: ffffffffffffffff", f"Exit {code}: {text}")
@fall("Codex: failed geht als job_status durch")
def f_codex_failed(tmp):
code, text, z, antwort = auswerten(ergebnisdatei(tmp, "", status="failed"))
return soll(code == 0 and z.get("job_status") == "failed" and z.get("antwort_gefunden") == "nein", f"Exit {code}: {text}")
@fall("Codex: Ergebnisdatei kein JSON, ohne job oder fehlend ist ein Befund")
def f_codex_datei_kaputt(tmp):
kaputt = tmp / "kaputt.json"
schreibe(kaputt, "{kein json")
ohne_job = tmp / "ohne-job.json"
schreibe(ohne_job, json.dumps({"storedJob": {}}))
f = []
for pfad, erwartet in ((kaputt, "nicht lesbar"), (ohne_job, "ohne job"), (tmp / "fehlt.json", "nicht lesbar")):
code, text, z, antwort = auswerten(pfad)
f += soll(code == 2 and erwartet in text and "Traceback" not in text, f"{pfad.name}: Exit {code}: {text}")
return f
# ---------------------------------------------------------------- Lauf
def main():
gescheitert = 0
grundlinie_gescheitert = False
for nummer, (name, funktion) in enumerate(FAELLE):
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as ordner:
try:
fehler = funktion(Path(ordner))
except Exception as e: # Ein Absturz im Fall ist ein Befund, kein Abbruch des Laufs.
fehler = [f"Absturz im Fall: {type(e).__name__}: {e}"]
if fehler:
gescheitert += 1
grundlinie_gescheitert = grundlinie_gescheitert or nummer == 0
print(f"FEHLER {name}")
for text in fehler:
print(f" {text[:600]}")
else:
print(f"OK {name}")
if grundlinie_gescheitert:
print("HINWEIS Grundlinie gescheitert: Die uebrigen Faelle messen nichts.")
print(f"{len(FAELLE)} Faelle, {gescheitert} gescheitert")
return 1 if gescheitert else 0
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.exit(main())