From 5e0577a871667880313de1c2c4714bbc1f66d4c9 Mon Sep 17 00:00:00 2001 From: Pascal Michel Date: Sat, 19 Sep 2026 04:00:01 +0200 Subject: [PATCH] =?UTF-8?q?Automatischer=20Push:=2019=20Datei(en)=20ge?= =?UTF-8?q?=C3=A4ndert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/rot_nachweis.py - .vault-tools/plan-kopie/welle_vergleich.py - .vault-tools/plan-kopie/welle_vergleich_treiber.py - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/angriffe.py - .vault-tools/trading-plattform-pruefung/browserlauf.py - .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py - .vault-tools/trading-plattform-pruefung/c_eigenlauf.py - .vault-tools/trading-plattform-pruefung/d_eigenlauf.py - .vault-tools/trading-plattform-pruefung/handelstag_auswertung.py - .vault-tools/trading-plattform-pruefung/ntfy_probe_server.py - … und 7 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 --- .vault-tools/plan-kopie/LIESMICH.md | 4 + .vault-tools/plan-kopie/rot_nachweis.py | 195 ++++++++++++++++++ .vault-tools/plan-kopie/welle_vergleich.py | 26 ++- .../plan-kopie/welle_vergleich_treiber.py | 44 ++++ AI/Regelwerk/codeworkflow.md | 4 +- 5 files changed, 265 insertions(+), 8 deletions(-) create mode 100644 .vault-tools/plan-kopie/rot_nachweis.py diff --git a/.vault-tools/plan-kopie/LIESMICH.md b/.vault-tools/plan-kopie/LIESMICH.md index 31d8fc8..3f2c1a9 100644 --- a/.vault-tools/plan-kopie/LIESMICH.md +++ b/.vault-tools/plan-kopie/LIESMICH.md @@ -21,8 +21,12 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko 5. **Nach jeder Welle:** `welle_vergleich.py --projekt --mat [--prosa …] -- `. Prüft, ob genau die Zieldateien geändert sind und ob jede byte-gleich mit der Kopie ist. Es zeigt je Datei nur die erste Abweichung — danach `diff` auf die ganze Datei. Eigene Plankorrekturen im Baum meldet es zu Recht als „außerhalb der Zieldateien". 6. **Escape-Folgen setzen:** `plan_escape.py …` ersetzt echte Zeichen in einer Zeile durch ihre Escape-Folge, ohne dass eine Schreibschicht mitredet. +7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py [--fixturen ] [--node-modules ]`. `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich. + ## Was belegt ist +Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17. + `welle_vergleich_treiber.py` fährt 14 Angriffsfälle gegen `welle_vergleich.py` (Grundlinie zuerst; fremde Datei, unveränderte Zieldatei, Abweichung in erster und letzter Zeile und in der letzten Zieldatei, CRLF, Datei fehlt in der Kopie, Task ohne Zieldateien, Kopie fehlt, Löschung nicht ausgeführt, Prosa-Datei unverändert, Zieldatei außerhalb des Projekts, Datei länger als der Plan). `plan_zeichen.py` hat am 2026-09-17 an einem echten Fund rot gezeigt (U+2028 im Plan). `plan_materialisiere.py` reproduziert Task 108 des Teil-3b-Plans aus dem Stand vor den Wellen byte-genau. `kopie_bauen.py` ist am 2026-09-17 an drei kaputten Eingaben rot gesehen: Ziel existiert schon (Exit 2, nichts kopiert), Ausschluss `node_modules` ohne Gegenstück im Wurzelordner (Befund), und ein aus der Kopie entferntes Paket `trading/data` (die Paketprüfung nennt es; nach dem Zurücklegen leer). Einen Treiber dazu gibt es noch nicht — die drei Fälle sind von Hand gefahren. ## Was die Skripte nicht sehen diff --git a/.vault-tools/plan-kopie/rot_nachweis.py b/.vault-tools/plan-kopie/rot_nachweis.py new file mode 100644 index 0000000..7781805 --- /dev/null +++ b/.vault-tools/plan-kopie/rot_nachweis.py @@ -0,0 +1,195 @@ +"""Rot-Nachweis je Task: die neuen Tests eines Tasks gegen den Produktivcode VOR dem Task, auf Wegwerfkopien. + +Koordinator-Werkzeug (entstanden 2026-09-18, Trading Plattform). plan_materialisiere.py wendet eine +Taskdatei immer ganz an; "erst der Test, dann die Umsetzung" laesst sich damit nicht beobachten. Dieses +Skript stellt den Zustand dazwischen her: Basis + Voraussetzungstasks + der Task selbst, danach die +PRODUKTIVEN Zieldateien des Tasks zurueck auf den Stand vor dem Task (neue Produktivdateien geloescht). +Dann laufen die Testdateien des Tasks. Es tut etwas und berichtet; das Urteil faellt der Koordinator: + ROT mindestens ein Fehlschlag oder Sammelfehler -- die erste Fehlerzeile steht dabei, damit man sieht, + ob der Test an der gemeinten Stelle faellt und nicht an einem Tippfehler. + GRUEN kein Fehlschlag ohne die Umsetzung -- ein Test, der so gruen ist, misst die Umsetzung nicht + (ausser der Plan fuehrt ihn ausdruecklich als Regressionsnetz). +Geltungsbereich und Luecken: +- Testdateien sind Zieldateien unter tests/ sowie *.test.ts und *.test.tsx; alles unter tests/fixtures/ + gilt als Testseite und bleibt stehen. Alles andere ist produktiv. +- Python-Tests laufen in WSL (Debian, ~/venvs/trading), Oberflaechentests unter Windows mit npx vitest; + dafuer setzt das Skript eine Junction auf das node_modules des Projekts und loest sie vor dem Loeschen. +- Erzeugte Fixtures (der Plan laesst sie mit altem Code erzeugen) holt --fixturen aus einem Ordner nach, + wenn sie nach dem Anwenden fehlen. +- Es sieht nicht, WELCHE Tests rot sein muessten; es zaehlt und zeigt die erste Fehlerzeile. +- Die Basis ist eine Kopie, die der Aufrufer VOR der ersten Welle gezogen hat; das Skript fasst das + Projekt selbst nie an. + +Aufruf: python rot_nachweis.py [--fixturen ] [--node-modules ] +--node-modules nennt das node_modules des Projekts, wenn die Basis-Kopie keines traegt. +spec.json: [{"task": "task-141-....md", "vor": ["task-...md", ...]}, ...] +""" + +import json +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +HIER = Path(__file__).resolve().parent +ZIEL = re.compile(r"^- `(.+?)`") +PROJEKTNAME = "trading-plattform/" + + +def zieldateien(taskdatei: Path) -> list[str]: + """Zieldateien relativ zum Projekt. Absolute Pfade werden hinter dem Projektordner abgeschnitten.""" + drin = False + ziele: list[str] = [] + for zeile in taskdatei.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n"): + if zeile.startswith("## "): + drin = zeile.startswith("## Zieldateien") + continue + treffer = ZIEL.match(zeile) if drin else None + if treffer: + pfad = treffer.group(1).replace("\\", "/").removeprefix("./") + if re.match(r"^[A-Za-z]:/", pfad): + stelle = pfad.lower().find(PROJEKTNAME) + if stelle < 0: + raise ValueError(f"{taskdatei.name}: absoluter Pfad ausserhalb des Projekts: {pfad}") + pfad = pfad[stelle + len(PROJEKTNAME) :] + ziele.append(pfad) + return ziele + + +def ist_test(pfad: str) -> bool: + return pfad.startswith("tests/") or pfad.endswith((".test.ts", ".test.tsx")) + + +def wende_an(planordner: Path, kopie: Path, tasks: list[str]) -> str: + if not tasks: + return "" + lauf = subprocess.run( + [sys.executable, "-X", "utf8", str(HIER / "plan_materialisiere.py"), str(planordner), str(kopie), *tasks], + capture_output=True, text=True, encoding="utf-8", errors="replace", + ) + return "" if lauf.returncode == 0 else (lauf.stdout + lauf.stderr)[-600:] + + +def wsl_pfad(pfad: Path) -> str: + text = str(pfad.resolve()).replace("\\", "/") + return f"/mnt/{text[0].lower()}{text[2:]}" + + +def loesche(ordner: Path) -> None: + junction = ordner / "dashboard" / "node_modules" + if junction.exists(): + os.rmdir(junction) # loest nur die Verknuepfung, nie das Ziel + shutil.rmtree(ordner, ignore_errors=True) + + +def erste_fehlerzeile(ausgabe: str) -> str: + for zeile in ausgabe.splitlines(): + kern = zeile.strip() + if kern.startswith(("E ", "FAILED", "ERROR", "AssertionError", "Error:", "TypeError", "ReferenceError")) or " FAIL " in zeile: + return kern[:220] + return "" + + +def main() -> int: + argumente = sys.argv[1:] + fixturen = None + if "--fixturen" in argumente: + stelle = argumente.index("--fixturen") + fixturen = Path(argumente[stelle + 1]) + del argumente[stelle : stelle + 2] + node_modules_arg = None + if "--node-modules" in argumente: + stelle = argumente.index("--node-modules") + node_modules_arg = Path(argumente[stelle + 1]) + del argumente[stelle : stelle + 2] + if len(argumente) != 4: + print(__doc__) + return 3 + basis, planordner, arbeit, spec_pfad = (Path(a) for a in argumente) + if not basis.is_dir() or not planordner.is_dir() or not spec_pfad.is_file(): + print("VORAUSSETZUNG FEHLT: Basis-Kopie, Planordner oder Spec") + return 3 + arbeit.mkdir(parents=True, exist_ok=True) + node_modules = None + for kandidat in (node_modules_arg, basis / "dashboard" / "node_modules"): + if kandidat is not None and kandidat.exists(): + node_modules = kandidat + break + ergebnisse = [] + for eintrag in json.loads(spec_pfad.read_text(encoding="utf-8")): + task, voraus = eintrag["task"], eintrag.get("vor", []) + name = task.split("-")[1] if "-" in task else task + vor, rot = arbeit / f"{name}_vor", arbeit / f"{name}_rot" + for ordner in (vor, rot): + loesche(ordner) + shutil.copytree(basis, vor, ignore=shutil.ignore_patterns("node_modules", "__pycache__")) + fehler = wende_an(planordner, vor, voraus) + shutil.copytree(vor, rot) + fehler = fehler or wende_an(planordner, rot, [task]) + if fehler: + ergebnisse.append((task, "ANWENDEN GESCHEITERT", fehler.replace("\n", " | "))) + print(f"{task}: ANWENDEN GESCHEITERT: {fehler[-300:]}") + continue + ziele = zieldateien(planordner / task) + tests = [z for z in ziele if ist_test(z) and not z.startswith("tests/fixtures/")] + for ziel in ziele: + if ist_test(ziel): + if not (rot / ziel).exists() and fixturen is not None and (fixturen / Path(ziel).name).is_file(): + (rot / ziel).parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(fixturen / Path(ziel).name, rot / ziel) + continue + if (vor / ziel).is_file(): + shutil.copyfile(vor / ziel, rot / ziel) + elif (rot / ziel).is_file(): + (rot / ziel).unlink() + python_tests = [t for t in tests if t.endswith(".py")] + ober_tests = [t for t in tests if not t.endswith(".py")] + zeilen = [] + if python_tests: + befehl = f"cd {wsl_pfad(rot)} && ~/venvs/trading/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60" + lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", befehl], + capture_output=True, text=True, encoding="utf-8", errors="replace") + ausgabe = lauf.stdout + summe = next((z.strip() for z in reversed(ausgabe.splitlines()) if re.search(r"\d+ (passed|failed|error)", z)), "keine Summenzeile") + # Gruen ist nur ein Lauf mit Summenzeile und ohne Fehlschlag. Ohne Summenzeile hat pytest gar + # nicht gesammelt (etwa ein ImportError in tests/conftest.py) -- das ist rot, nicht gruen. + # Gefunden am 2026-09-18: Task 172 stand als GRUEN da, obwohl kein einziger Test lief. + rot_ja = bool(re.search(r"\d+ (failed|error)", summe)) or summe == "keine Summenzeile" + zeilen.append(("ROT " if rot_ja else "GRUEN") + f" pytest: {summe} | {erste_fehlerzeile(ausgabe)}") + if ober_tests: + if node_modules is None: + zeilen.append("VORAUSSETZUNG FEHLT: node_modules in der Basis-Kopie") + else: + import _winapi + _winapi.CreateJunction(str(node_modules.resolve()), str(rot / "dashboard" / "node_modules")) + relativ = [t.removeprefix("dashboard/") for t in ober_tests] + lauf = subprocess.run(["npx.cmd", "vitest", "run", *relativ], cwd=rot / "dashboard", + capture_output=True, text=True, encoding="utf-8", errors="replace") + ausgabe = re.sub(r"\x1b\[[0-9;]*m", "", lauf.stdout + lauf.stderr) + summe = next((z.strip() for z in ausgabe.splitlines() if z.strip().startswith("Tests ")), "") + if not summe: + # Ohne Summenzeile ist vitest nicht bis zum Ende gekommen (Importfehler, Startfehler): + # das Ende der Ausgabe zeigen, statt "rot" zu behaupten, ohne zu wissen woran. + summe = "keine Summenzeile; Ende der Ausgabe: " + " / ".join(z.strip() for z in ausgabe.splitlines()[-6:] if z.strip())[:300] + rot_ja = "failed" in summe or lauf.returncode != 0 + zeilen.append(("ROT " if rot_ja else "GRUEN") + f" vitest: {summe} | {erste_fehlerzeile(ausgabe)}") + if not tests: + zeilen.append("ohne Testdatei unter den Zieldateien") + for zeile in zeilen: + print(f"{task}: {zeile}") + ergebnisse.append((task, zeile[:5].strip(), zeile)) + for ordner in (vor, rot): + loesche(ordner) + gruen = [e for e in ergebnisse if e[1] == "GRUEN"] + print(f"\n{len(ergebnisse)} Laeufe, davon {len(gruen)} gruen ohne Umsetzung.") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Werkzeugs ist selbst ein Befund + print(f"BEFUND: das Werkzeug ist abgebrochen: {ausnahme!r}") + sys.exit(2) diff --git a/.vault-tools/plan-kopie/welle_vergleich.py b/.vault-tools/plan-kopie/welle_vergleich.py index a80cc16..f34e4cf 100644 --- a/.vault-tools/plan-kopie/welle_vergleich.py +++ b/.vault-tools/plan-kopie/welle_vergleich.py @@ -23,7 +23,9 @@ import sys import traceback from pathlib import Path, PureWindowsPath -ZIEL = re.compile(r"^- `(.+?)`\s*\((.*)\)\s*$") +# Seit 2026-09-18 ist die Klammer hinter dem Pfad wahlfrei, und dahinter darf Text stehen +# ("- `pfad` -- nur die Konstante X"): Plaene nennen Zieldateien nicht immer mit "(neu)". +ZIEL = re.compile(r"^- `(.+?)`(?:\s*\((.*?)\))?") def zieldateien(taskdatei: Path, projekt: Path, befunde: list[str]) -> dict[str, bool]: @@ -45,12 +47,22 @@ def zieldateien(taskdatei: Path, projekt: Path, befunde: list[str]) -> dict[str, if not treffer: continue absolut = PureWindowsPath(treffer.group(1)) - try: - rel = absolut.relative_to(PureWindowsPath(str(projekt))) - except ValueError: - befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}") - continue - ziele[rel.as_posix()] = "gelöscht" in treffer.group(2) or "geloescht" in treffer.group(2) + if absolut.drive == "" and absolut.root == "": + # Seit 2026-09-18: Ein Pfad ohne Laufwerk und ohne Wurzel gilt relativ zum Projekt + # (Plaene fuer einen Git-Worktree nennen keine absoluten Pfade). Ein ".." fuehrte + # aus dem Projekt hinaus und ist ein Befund wie ein fremder absoluter Pfad. + if ".." in absolut.parts: + befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}") + continue + rel = absolut + else: + try: + rel = absolut.relative_to(PureWindowsPath(str(projekt))) + except ValueError: + befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}") + continue + klammer = treffer.group(2) or "" + ziele[rel.as_posix()] = "gelöscht" in klammer or "geloescht" in klammer if not ziele: befunde.append(f"{taskdatei.name}: keine Zieldateien gefunden -- ohne sie misst der Lauf nichts") return ziele diff --git a/.vault-tools/plan-kopie/welle_vergleich_treiber.py b/.vault-tools/plan-kopie/welle_vergleich_treiber.py index c5f98e5..33a8996 100644 --- a/.vault-tools/plan-kopie/welle_vergleich_treiber.py +++ b/.vault-tools/plan-kopie/welle_vergleich_treiber.py @@ -114,6 +114,43 @@ def fall_laengere_datei(projekt, mat, task, wurzel): schreibe(projekt / "a.py", "EINS\nzwei\nDREI\nvier\n") +def _relativ(task, projekt): + """Schreibt die Zieldateien der Taskdatei relativ zum Projekt, eine davon mit Schraegstrich.""" + text = task.read_text(encoding="utf-8") + schreibe(task, text.replace(f"{projekt}\\", "").replace("sub\\neu.py", "sub/neu.py")) + + +def fall_relativ_grundlinie(projekt, mat, task, wurzel): + _relativ(task, projekt) + + +def fall_relativ_abweichung(projekt, mat, task, wurzel): + _relativ(task, projekt) + schreibe(projekt / "sub" / "neu.py", "neu eins\nneu ZWEI\n") + + +def _ohne_klammer(task): + """Nimmt den Zieldateien die Klammer (ausser der Loeschmarke) und haengt einer Zeile Text an.""" + text = task.read_text(encoding="utf-8") + text = text.replace("a.py` (geändert)", "a.py` — **nur die erste und die letzte Zeile**") + schreibe(task, text.replace(" (neu)", "").replace("doku.md` (geändert)", "doku.md`")) + + +def fall_ohne_klammer_grundlinie(projekt, mat, task, wurzel): + _ohne_klammer(task) + + +def fall_ohne_klammer_abweichung(projekt, mat, task, wurzel): + _ohne_klammer(task) + schreibe(projekt / "a.py", "Eins\nzwei\nDREI\n") + + +def fall_relativ_nach_aussen(projekt, mat, task, wurzel): + _relativ(task, projekt) + text = task.read_text(encoding="utf-8") + schreibe(task, text.replace("Keine andere Datei", "- `../anderswo/x.py` (neu)\n\nKeine andere Datei")) + + FAELLE = [ ("Grundlinie", fall_grundlinie, 0, "0 Befund(e)."), ("fremde Datei im Baum", fall_fremde_datei, 2, "fremd.py: geaendert"), @@ -130,6 +167,13 @@ FAELLE = [ ("Zieldatei ausserhalb des Projekts", fall_ziel_ausserhalb, 2, "liegt nicht unter dem Projekt"), # Die angehaengte Zeile steht der leeren Restzeile der Kopie gegenueber, deshalb "Zeile 4" und nicht "Laenge". ("Datei laenger als der Plan", fall_laengere_datei, 2, "a.py: weicht vom Plan ab -- Zeile 4: Baum b'vier'"), + # Seit 2026-09-18: Zieldateien relativ zum Projekt (Plaene fuer einen Git-Worktree). + ("Zieldateien relativ zum Projekt", fall_relativ_grundlinie, 0, "0 Befund(e)."), + ("relative Zieldatei weicht ab", fall_relativ_abweichung, 2, "sub/neu.py: weicht vom Plan ab -- Zeile 2"), + ("relative Zieldatei fuehrt nach aussen", fall_relativ_nach_aussen, 2, "liegt nicht unter dem Projekt"), + # Seit 2026-09-18: Die Klammer hinter dem Pfad ist wahlfrei, Text dahinter erlaubt. + ("Zieldateien ohne Klammer", fall_ohne_klammer_grundlinie, 0, "0 Befund(e)."), + ("Zieldatei ohne Klammer weicht ab", fall_ohne_klammer_abweichung, 2, "a.py: weicht vom Plan ab -- Zeile 1"), ] diff --git a/AI/Regelwerk/codeworkflow.md b/AI/Regelwerk/codeworkflow.md index 3fa4d6a..12a2dad 100644 --- a/AI/Regelwerk/codeworkflow.md +++ b/AI/Regelwerk/codeworkflow.md @@ -5,7 +5,7 @@ title: Codeworkflow summary: Arbeitsweise für Code-Projekte — Standardschleife, Planaufteilung je Task, Subagenten und ihre Modellstufen, Gegenprüfung, Lauf mit echten Daten, Aufgabenmanagement. tags: [regelwerk, codeworkflow, code-projekte] created: 2026-08-03 -updated: 2026-09-17 +updated: 2026-09-18 --- # Codeworkflow @@ -45,6 +45,8 @@ Die Lehre daraus gilt über diesen Fall hinaus: **Eine gerechnete Ersparnis ist **3 Ausführen.** Ein Executer, sofern die Zieldateien nicht sauber trennbar sind; sonst mehrere parallel mit disjunkten Dateimengen. Er bekommt den Pfad **seiner Taskdatei** — nicht des Gesamtplans —, die erlaubten Zieldateien, die Verbote und den Verifikationsbefehl. +**Der Extraktor kennt kein „erst die Tests“.** `plan_materialisiere.py` wendet eine Taskdatei immer **vollständig** an; Testdateien und Produktivcode entstehen in einem Durchgang. Die Reihenfolge aus Schritt 7 der Taskform — Test anlegen, Fehlschlag im Wortlaut sehen, dann die Umsetzung — lässt sich damit nicht über zwei Aufrufe herstellen. Der belegte Weg steht deshalb in den Auftrag: die fertige Produktivdatei kurz beiseitelegen (etwa aus `git show HEAD:`), den roten Lauf beobachten, die materialisierte Fassung **byte-gleich** zurücklegen — nie neu schreiben. Am 2026-09-17/18 im Projekt „Home Assistant Dashboard“ sind zwei Executer über diesen Widerspruch gestolpert; beide haben ihn gemeldet statt ihn still aufzulösen, und beide haben den roten Zustand auf diesem Weg belegt. Wer den Satz nicht in den Auftrag schreibt, lässt den Executer zwischen Plan und Werkzeug zerreiben. + **Das Modell steht in der Taskdatei, nicht in der Gewohnheit.** Jede Taskdatei trägt im Frontmatter `modell:` und `modell_begruendung:`. Die Begründung ist Fließtext und steht in Anführungszeichen, wie der Titel ([[AI/Regelwerk/frontmatter|frontmatter.md]], „Quoting"). Fehlt der Eintrag, gilt die Vorgabe aus dem Stamm: Sonnet. Das Kriterium ist eine einzige Frage: > Braucht der Executer für diesen Task Ermessen, oder steht alles wörtlich im Plan?