"""Mutantenlauf gegen die Workflows unter .claude/workflows/: je Mutant genau eine Ersetzung, dann faelle_workflows.mjs. Jede Ersetzung muss genau einmal vorkommen, sonst bricht der Lauf mit Exit 2 ab: Ein Muster, das nicht mehr passt, ist ein veralteter Mutant und kein ueberlebter. Ein ueberlebter Mutant ist ein Befund, ausser er steht in WIRKUNGSLOS mit Begruendung. Aufruf: python mutanten_workflows.py [] Exit 0, wenn jeder wirksame Mutant gefangen wird, 1 bei einem Ueberlebenden, 2 bei einem veralteten Muster. """ import shutil import subprocess import sys import tempfile from pathlib import Path HIER = Path(__file__).resolve().parent FAELLE = HIER / "faelle_workflows.mjs" # Mutanten, die bei den heutigen Stufen kein Verhalten aendern, mit Grund. WIRKUNGSLOS = { "harte Obergrenze fehlt": "keine Stufe gibt mehr als vier Plaetze; wer die Stufen anhebt, braucht einen Fall", } MUTANTEN = [ ("verbrauch", "Stufe 50 -> 60", "[[50, 4], [70, 2], [85, 1]]", "[[60, 4], [70, 2], [85, 1]]"), ("verbrauch", "Wochenstufe 85 -> 80", "[[70, 4], [85, 2], [95, 1]]", "[[70, 4], [80, 2], [95, 1]]"), ("verbrauch", "ueber 100 wieder Fehler", "|| p < 0) return", "|| p < 0 || p > 100) return"), ("verbrauch", "doppeltes Fenster: erstes gewinnt", "if (!e[art] || e[art].prozent < p) e[art] = neu", "if (!e[art]) e[art] = neu"), ("verbrauch", "Opus-Woche ignoriert", "Math.max(e.woche.prozent, e.opus ? e.opus.prozent : 0)", "e.woche.prozent"), ("verbrauch", "Opus-Woche ersetzt die Woche", "Math.max(e.woche.prozent, e.opus ? e.opus.prozent : 0)", "(e.opus ? e.opus.prozent : e.woche.prozent)"), ("verbrauch", "harte Obergrenze fehlt", "Math.min(wunsch, grenzeFuenf, grenzeWoche, HARTE_OBERGRENZE)", "Math.min(wunsch, grenzeFuenf, grenzeWoche)"), ("verbrauch", "gebremst auch bei wunsch 0", "const gebremst = wunsch > 0 && plaetze === 0", "const gebremst = plaetze === 0"), ("verbrauch", "Status ok nicht verlangt", " || ablesung.status !== 'ok'", ""), ("verbrauch", "Rueckfall 3 statt 2", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"), ("verbrauch", "wunsch-Pruefung fehlt", "if (!Number.isInteger(a.wunsch) || a.wunsch < 0) throw", "if (false) throw"), ("verbrauch", "fremdes Fenster mit Zahl geprueft", "if (!art) { e.andere.push(f.label); continue }\n const p = f.percentUsed", "const p = f.percentUsed\n if (!art) { if (typeof p !== 'number') return { fehler: 'x' }; e.andere.push(f.label); continue }"), ("plankritik", "Bremse vor Linsen fehlt", "if (vorLinsen.gebremst) {", "if (false) {"), ("plankritik", "Bremse vor Widerlegung fehlt", "if (vorWiderlegung.gebremst) {", "if (false) {"), ("plankritik", "Widerlegung bis vier", "const WIDERLEGUNG_HOECHSTENS = 2", "const WIDERLEGUNG_HOECHSTENS = 4"), ("plankritik", "Widerlegung ignoriert die Ablesung", "} else plaetzeWiderlegung = vorWiderlegung.plaetze", "} else plaetzeWiderlegung = WIDERLEGUNG_HOECHSTENS"), ("plankritik", "Linsen fest zwei", "await hoechstens(vorLinsen.plaetze, gewaehlt,", "await hoechstens(2, gewaehlt,"), ("plankritik", "Rueckfall ohne Ablesung 3", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"), ("plankritik", "Testort fehlt in der Widerlegung", "Du änderst keine Datei im Projekt und keine im Plan. ${testort(ctx)}\n- ", "Du änderst keine Datei im Projekt und keine im Plan.\n- "), ("plankritik", "Testort fehlt bei den Linsen", "Executer-Bericht, dessen Testlauf du wiederholen könntest. ${testort(ctx)}", "Executer-Bericht, dessen Testlauf du wiederholen könntest. ${linse === 'arithmetik' ? testort(ctx) : ''}"), ("plankritik", "werkzeug:kopie wieder auf sonnet", "den Plan darauf anwenden', '', 'haiku')", "den Plan darauf anwenden', '', 'sonnet')"), ("plankritik", "Planwerkzeuge und Mutantenlauf auf haiku", "zusatz, modell = 'sonnet')", "zusatz, modell = 'haiku')"), ("plankritik", "keine Ablesung am Ende", "await ablesen('Ende', 0)\n", "\n"), ("plankritik", "gebremste Befunde ohne Hinweis", "'nicht geprüft (gebremst)'", "'nicht geprüft'"), ("plankritik", "gebremst auch unter der Schwelle", "gebremstVor && zuPruefen.includes(g) ?", "gebremstVor ?"), ("plankritik", "naechster_schritt uebergeht die Bremse", "naechster_schritt: gebremstVor ? ", "naechster_schritt: false ? "), ("plankritik", "zaehlung null bei Bremse vor Linsen", "zaehlung: { roh: 0, zusammengefasst: 0, bestaetigt: 0, offen: 0, widerlegt: 0, nicht_geprueft: 0 },", "zaehlung: null,"), ("pruefung", "verbrauch false wird uebergangen", "if (args.verbrauch === false) return\n", "\n"), ("pruefung", "Ablesung nach der Pruefung fehlt", "await ablesen('nach Prüfung')\n", "\n"), ("welle-mit-pruefung", "Kind liest selbst", "{ ...pruefungArgs, verbrauch: false }", "{ ...pruefungArgs }"), ("welle-mit-pruefung", "bis vergleich wirkt nicht", "if (args.bis === 'vergleich') {", "if (false) {"), ("welle-mit-pruefung", "werkzeug:kopie wieder auf sonnet", "Taskdateien der Welle darauf anwenden', 'haiku')", "Taskdateien der Welle darauf anwenden', 'sonnet')"), ("welle-mit-pruefung", "Vergleich auf haiku", "zweck, modell = 'sonnet')", "zweck, modell = 'haiku')"), ("welle-mit-pruefung", "Ablesung vor der Pruefung fehlt", " await ablesen('vor Prüfung')\n", "\n"), ("planung", "Voraussetzung laufend genuegt", "['fertig', 'erledigt'].includes(status.get(n)))) starte(t)", "['fertig', 'erledigt', 'laeuft'].includes(status.get(n)))) starte(t)"), ("planung", "keine Blockade", "status.set(t.nr, 'blockiert')\n geaendert = true", "geaendert = false"), ("planung", "kein Abbruch nach drei", "gescheitert.length >= ABBRUCH_NACH && !abgebrochen", "false"), ("planung", "Bremse beim Nachlesen fehlt", "if (v.gebremst) {\n gebremst = true\n log('Planung gebremst", "if (false) {\n gebremst = true\n log('Planung gebremst"), ("planung", "Nachlesen uebernimmt die Plaetze nicht", "log('Planung gebremst: kein weiterer Start, die laufenden Planer enden')\n } else plaetze = v.plaetze", "log('Planung gebremst: kein weiterer Start, die laufenden Planer enden')\n }"), ("planung", "Bremse am Anfang nicht gemeldet", " if (v.gebremst) gebremst = true\n else plaetze = v.plaetze", " plaetze = v.plaetze"), ("planung", "Plaetze nicht begrenzt", "if (laufend.size >= plaetze) break", "if (laufend.size >= 99) break"), ("planung", "Reihenfolge unsortiert", "const reihenfolge = [...args.tasks].sort((x, y) => ((x.welle || 0) - (y.welle || 0)) || (x.nr - y.nr))", "const reihenfolge = [...args.tasks]"), ("planung", "Nachlesen ohne offene Tasks", "fertigSeitAblesung >= nachlesenAlle && nochOffen()", "fertigSeitAblesung >= nachlesenAlle"), ("planung", "fremde Datei zaehlt als fertig", "const r = passt ? roh : null", "const r = roh"), ("planung", "Vorgabe vier statt zwei", "const VORGABE_PLAETZE = 2", "const VORGABE_PLAETZE = 4"), ("planung", "Zyklenpruefung fehlt", "if (geschafft < a.tasks.length) throw", "if (false) throw"), ("planung", "erledigt ignoriert", "(args.erledigt || []).includes(t.nr) ? 'erledigt' : 'offen'", "'offen'"), ("planung", "Rueckfall ohne Ablesung 3", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"), ("planung", "Datei ohne Pfadgrenze", "('/' + roh.datei.replace(/\\\\/g, '/')).endsWith('/' + jeNr.get(nr).datei)", "roh.datei.replace(/\\\\/g, '/').endsWith(jeNr.get(nr).datei)"), ("planung", "Nachlesezaehler nicht zurueckgesetzt", " fertigSeitAblesung = 0\n const v = await ablesen(", " const v = await ablesen("), ("planung", "nachlesen_alle ignoriert", "const nachlesenAlle = args.nachlesen_alle || NACHLESEN_ALLE", "const nachlesenAlle = NACHLESEN_ALLE"), ("planung", "effort nicht durchgereicht", " if (args.effort) opts.effort = args.effort\n", ""), ("planung", "effort ungeprueft", " if (a.effort !== undefined && !EFFORTS.includes(a.effort)) throw", " if (false) throw"), ("planung", "Rueckgabe ohne effort", " effort: args.effort || '(Frontmatter)',\n", ""), ("planung", "Lesehinweis fehlt", " Lies sie, bevor du einen Namen, eine Signatur oder einen Import daraus übernimmst.", ""), ("plankritik", "doppelte Linse erlaubt", " if (a.linsen && new Set(a.linsen).size !== a.linsen.length) throw", " if (false) throw"), ("plankritik", "Mutantenlauf-Rohausgabe geht nicht an arithmetik", "${mutantenText(ctx.mutantenLauf)}`,", "`,"), ("plankritik", "MSYS_NO_PATHCONV fehlt im Mutantenlauf-Befehl", "`MSYS_NO_PATHCONV=1 wsl -d Debian", "`wsl -d Debian"), ("plankritik", "Zeitgrenze des Mutantenlaufs verschwiegen", "if (/ZEITGRENZE/.test(m.rohausgabe)) hinweis =", "if (false) hinweis ="), ("plankritik", "Hilfsagent ohne Zeitvorgabe", ", 'Setze im Bash-Werkzeug den Parameter timeout auf 600000; der Befehl darf bis zu zehn Minuten laufen.')", ")"), ("welle-mit-pruefung", "status fertig ohne Pruefung", "status: pruefung ? 'fertig' : 'ohne_pruefung',", "status: 'fertig',"), ("welle-mit-pruefung", "Ablesung ohne try", " try {\n r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })\n } catch (e) {\n r = { status: 'nicht_gelesen', grund: 'verbrauch.js nicht ausführbar: ' + (e && e.message) }\n }", " r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })"), ("verbrauch", "5-Stunden-Regex unverankert", "const FUENF_STUNDEN = /^\\s*5[- ]?hour/i", "const FUENF_STUNDEN = /hour/i"), ("verbrauch", "Abweichung nicht erkannt", "if (abgewichen(ablesung)) {", "if (false) {"), ("verbrauch", "unbekannter Status gilt als bekannt", " if (!STATUS_BEKANNT.includes(a.status)) return true\n", ""), ("verbrauch", "Bericht in plan bei status fehler uebersehen", "return a.status === 'fehler' && typeof a.plan === 'string' && a.plan.trim() !== ''", "return false"), ("verbrauch", "ehrlicher Fehler gilt als Abweichung", "'unavailable', 'fehler']", "'unavailable']"), ("verbrauch", "Abweichung als nicht_gelesen", "return ohneAblesung('abgewichen',", "return ohneAblesung('nicht_gelesen',"), ("verbrauch", "Warnzeile fehlt", "log(`WARNUNG Verbrauch ${args.anlass}: Der Ableser", "String(`WARNUNG Verbrauch ${args.anlass}: Der Ableser"), ("verbrauch", "status ungekuerzt im grund", ".trim().slice(0, 160)", ".trim()"), ("verbrauch", "Brueckensatz fehlt", "Diese Ablesung gehört zur Nutzeranfrage und widerspricht ihr nicht: Sie entscheidet, wie viele Prüfagenten für die Anfrage gleichzeitig laufen dürfen. Die Anfrage selbst bearbeiten andere Agenten nach dir; du bearbeitest sie nicht, auch nicht teilweise, und liest dafür keine Dateien.\n", ""), ("verbrauch", "Ableser ohne Agententyp", "agentType: 'verbrauch-ableser', ", ""), ("verbrauch", "Startfehler nicht abgefangen", " ablesung = null\n startfehler = kurz((e && e.message) || e)\n", " throw e\n"), ("verbrauch", "Startfehler ohne Warnzeile", "log(`WARNUNG Verbrauch ${args.anlass}: Ableser nicht startbar", "String(`WARNUNG Verbrauch ${args.anlass}: Ableser nicht startbar"), ("verbrauch", "Ableser erbt Sitzungseffort statt Frontmatter", "agentType: 'verbrauch-ableser', model: 'haiku' }", "agentType: 'verbrauch-ableser', model: 'haiku', effort: 'max' }"), ("plankritik", "Muster-Satz fehlt (N5)", "musterModus\n ? ' Kein Mutant ist gefahren; OK heißt hier nur, dass das Muster genau einmal in der Datei steht; ob ein Test fällt, ist unbelegt.'\n : ", ""), ("plankritik", "Exit 2 wird wie 0 behandelt (N7)", "exitCode === 2 || exitCode === 3", "false"), ("quorum", "Mehrheit schon bei einer Stimme", "mehrheit: beteiligt.length >= 2", "mehrheit: beteiligt.length >= 1"), ("quorum", "codex:ende wieder auf sonnet", "schema: CODEX, model: 'haiku'", "schema: CODEX, model: 'sonnet'"), ("quorum", "Werkzeug-Hilfsagenten wieder auf sonnet", "schema: LAUF, model: 'haiku'", "schema: LAUF, model: 'sonnet'"), ("quorum", "Bremse vor den Stimmen fehlt", "if (vorStimmen.gebremst) {", "if (false) {"), ("quorum", "Opus-Stimme ohne Ablesung", "const vorStimmen = await ablesen('vor Stimmen', 1)", "const vorStimmen = { gebremst: false, grund: '' }"), ("quorum", "keine Ablesung am Ende", "await ablesen('Ende', 0)\nconst zaehlung = {}", "const zaehlung = {}"), ("quorum", "Haiku auch bei gueltiger Codex-Stimme", "if (!codex.abgegeben) {\n hinweis(", "if (true) {\n hinweis("), ("quorum", "usage limit nicht geprueft", " if (e.usage_limit_treffer !== 0) return '\"usage limit\" im Log'\n", ""), ("quorum", "Lesebeleg nicht geprueft", " if (!String(e.lesebeleg).includes('Lesemarke: ' + p.lesemarke)) return 'Lesebeleg fehlt oder zitiert eine andere Lesemarke'\n", ""), ("quorum", "Status vor dem Lauf nicht geprueft", "if (!/EXIT status: 0(?!\\d)/.test(start)) {", "if (false) {"), ("quorum", "codex false wird uebergangen", "if (a.codex === false) {", "if (false) {"), ("quorum", "Codex-Runden fest drei", "const runden = a.codex_runden || CODEX_RUNDEN", "const runden = CODEX_RUNDEN"), ("quorum", "kein Abbruch nach Zeitueberschreitung", "await werkzeugAgent('codex:abbruch', 'Stimmen',", "await Promise.resolve('codex:abbruch', 'Stimmen',"), ("quorum", "Warten ohne Zeitvorgabe", "'auf das Ende des Codex-Auftrags warten', LANGE)", "'auf das Ende des Codex-Auftrags warten')"), ("quorum", "Codex darf schreiben", "task --background --json", "task --background --write --json"), ("quorum", "leerer Unterschied startet Stimmen", "if (paket.leer) {", "if (false) {"), ("quorum", "unbelegter Befund zaehlt", "belegt: String(b.beleg).trim() !== ''", "belegt: true"), ("quorum", "Zusammenfuehren sieht unbelegte Befunde", "zusammenfuehrenAuftrag(belegt)", "zusammenfuehrenAuftrag(alle)"), ("quorum", "Dateien nicht getrennt", "if (datei[i] === datei[j] && bereich[i] && bereich[j]", "if (bereich[i] && bereich[j]"), ("quorum", "Schwere der Gruppe ist die niedrigste", ".sort((x, y) => RANG[x] - RANG[y])[0]", ".sort((x, y) => RANG[y] - RANG[x])[0]"), ("quorum", "Hinweis wird nachgeprueft", "gruppen.filter(g => !g.mehrheit && g.schwere !== 'hinweis')", "gruppen.filter(g => !g.mehrheit)"), ("quorum", "Mehrheitsgruppe wird nachgeprueft", "gruppen.filter(g => !g.mehrheit && g.schwere !== 'hinweis')", "gruppen.filter(g => g.schwere !== 'hinweis')"), ("quorum", "ungepruefter blockierender Befund sperrt nicht", "(gilt(g) || g.status === 'nicht_geprueft')", "gilt(g)"), ("quorum", "Urteil schon mit einer Stimme", "if (abgegeben.length < 2) {", "if (abgegeben.length < 1) {"), ("quorum", "Bericht ohne Urteil", "return ergebnis('unvollstaendig', 'kein_urteil', grund, null,", "return ergebnis('unvollstaendig', 'kein_urteil', grund, 'x',"), ("quorum", "jobId aus der ganzen Ausgabe samt Status", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", ' const job = /"jobId": "([^"]+)"/.exec(start)\n'), ("quorum", "Erkennung vertauschter laeufe entfernt", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", " const job = /\"jobId\": \"([^\"]+)\"/.exec(start.slice(start.lastIndexOf('EXIT status:')))\n"), ("quorum", "alte jobId als Ersatz zugelassen", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", ' const job = taskJobs[0] || /"jobId": "([^"]+)"/.exec(start)\n'), ("quorum", "Hinweis auf vertauschte laeufe fehlt", " hinweis('Codex-Start: Ausgabe nicht in Befehlsreihenfolge; jobId aus der task-Ausgabe (' + job[1] + ')')\n", ""), ("quorum", "erster Kandidat gewinnt", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", " const job = taskJobs[0] || null\n"), ("quorum", "Warnung vor ungesehenem Auftrag fehlt", " ? 'jobId nicht sicher bestimmbar; Codex-Auftrag läuft womöglich ungesehen weiter'\n", " ? 'Codex-Auftrag nicht gestartet'\n"), ("quorum", "Vertauschung nur an EXIT-Reihenfolge erkannt", " if (!(statusEnde < job.index && job.index < taskEnde)) {\n", " if (taskEnde < statusEnde) {\n"), ("quorum", "Warnung bei fehlendem Status fehlt", " z.grund = 'Status vor dem Lauf nicht gelesen' + (taskOK ? '; Codex-Auftrag läuft womöglich ungesehen weiter' : '')\n", " z.grund = 'Status vor dem Lauf nicht gelesen'\n"), ("quorum", "Grenze vor EXIT task nicht geprüft", " if (!(statusEnde < job.index && job.index < taskEnde)) {\n", " if (!(statusEnde < job.index)) {\n"), ("quorum", "eingerückte Zeile mit queued als Task-Herkunft zugelassen", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/(?:\{\s*|^[ \t]+)"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/gm)]' + "\n"), ("quorum", "beliebige verschachtelte jobId zugelassen", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/"jobId"\s*:\s*"([^"\\\r\n]+)"/g)]' + "\n"), ("quorum", "Objektöffnung vor jobId nicht verlangt", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n"), ("quorum", "Komma nach jobId nicht verlangt", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,?\s*"status"\s*:\s*"queued"/g)]' + "\n"), ("quorum", "queued nicht verlangt", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"[^"]+"/g)]' + "\n"), ("quorum", "Treffer statt verschiedener IDs gezählt", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", " const job = taskJobs.length === 1 ? taskJobs[0] : null\n"), ("quorum", "Rohzeile wieder als Merkmal zugelassen", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/^ "jobId": "([^"\\\r\n]+)",?\r?$/gm), ...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n"), ("quorum", "Merkmal über Laufgrenzen zugelassen", " for (const laufText of laufTexte(startAntwort)) {\n", " for (const laufText of [start]) {\n"), ("quorum", "Task-EXIT-Suche nicht am Zeilenanfang verankert", r' const taskEnde = start.search(/^EXIT task: 0(?!\d)/m)' + "\n", r' const taskEnde = start.search(/EXIT task: 0(?!\d)/m)' + "\n"), ("quorum", "Letzte statt erster Fundstelle gewählt", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", " const job = taskIds.size === 1 ? taskJobs[taskJobs.length - 1] : null\n"), ("quorum", "Fundstellen nicht auf den verbundenen Text umgerechnet", " treffer.index += startIndex\n", ""), ("quorum", "Trennzeichen beim Umrechnen nicht mitgezählt", " startIndex += laufText.length + 1\n", " startIndex += laufText.length\n"), ("quorum", "Task-Exit nicht verlangt bei gefundener jobId", " if (!taskOK || !job) {\n", " if (!job) {\n"), ("quorum", "Status-EXIT-Suche nicht am Zeilenanfang verankert", r" const statusEnde = start.search(/^EXIT status: 0(?!\d)/m)" + "\n", r" const statusEnde = start.search(/EXIT status: 0(?!\d)/m)" + "\n"), ("quorum", "Warnung auch ohne gelaufenen task", " z.grund = 'Status vor dem Lauf nicht gelesen' + (taskOK ? '; Codex-Auftrag läuft womöglich ungesehen weiter' : '')\n", " z.grund = 'Status vor dem Lauf nicht gelesen' + '; Codex-Auftrag läuft womöglich ungesehen weiter'\n"), ("quorum", "Teilgruppen ignoriert, Gruppe bleibt ganz", "teile.forEach(t => gruppen.push(t))", "gruppen.push(mitglieder)"), ("quorum", "Gruppe bei Aufteilung ganz aufgeloest", "teile.forEach(t => gruppen.push(t))", "(teile.length > 1 ? mitglieder.map(b => [b]) : teile).forEach(t => gruppen.push(t))"), ("quorum", "Aufteilung ohne Hinweis", "if (teile.length > 1) meldungen.push(", "if (false) meldungen.push("), ("quorum", "Toleranz null", "const TOLERANZ = 3", "const TOLERANZ = 0"), ("quorum", "Toleranz zwei", "const TOLERANZ = 3", "const TOLERANZ = 2"), ("quorum", "Toleranz vier", "const TOLERANZ = 3", "const TOLERANZ = 4"), ("quorum", "Toleranz nur nach unten", "y[0] <= x[1] + TOLERANZ", "y[0] <= x[1]"), ("quorum", "keine transitive Huelle", "if (datei[i] === datei[j] && bereich[i]", "if (i === 0 && datei[i] === datei[j] && bereich[i]"), ("quorum", "Stelle ohne Zeilennummer bleibt nie bei ihrer Teilgruppe", "if (stuecke.size === 1) verbinde(", "if (false) verbinde("), ("quorum", "Stelle ohne Zeilennummer schliesst sich der ersten Teilgruppe an", "if (stuecke.size === 1) verbinde(", "if (stuecke.size >= 1) verbinde("), ("quorum", "Stellen ohne Zeilennummer getrennt", "else if (stuecke.size === 0 && gleich.length) verbinde(", "else if (false) verbinde("), ("quorum", "a.py:42 nicht gelesen", " || /\\.[a-z0-9]+:(\\d+)(?:\\s*(?:-|–)\\s*(\\d+))?/i.exec(s)\n", ""), ("quorum", "Zeilennummer nur am Anfang gelesen", "/(?:^|[^\\p{L}\\d])(?:zeilen?", "/^\\s*(?:zeilen?"), ("quorum", "L12 nicht gelesen", "|z\\.|lines?|l)\\s*(\\d+)", "|z\\.|lines?)\\s*(\\d+)"), ("quorum", "Gedankenstrich im Bereich nicht gelesen", "(?:-|–|bis|to)\\s*l?", "(?:-|bis|to)\\s*l?"), ("quorum", "Zeilenbereich nur erste Zeile", "const bis = m[2] === undefined ? von : Number(m[2])", "const bis = von"), ("quorum", "Stelle ohne Zeilen zaehlt als Zeile 0", "if (!m) return null", "if (!m) return [0, 0]"), ("quorum", "Befundzeilen der Gruppe fehlen", "for (const b of g.mitglieder) z.push(", "for (const b of []) z.push("), ("quorum", "Stelle fehlt in der Befundzeile", ", ${b.schwere}, Stelle ${klartext(b.stelle)}): ${klartext(b.aussage)}", ", ${b.schwere}): ${klartext(b.aussage)}"), ("quorum", "Nachpruefungszeile fehlt", "if (g.nachpruefung) z.push(''", "if (false) z.push(''"), ("quorum", "Nachpruefung ohne Fundstelle", "${g.nachpruefung.urteil}, ${klartext(g.nachpruefung.fundstelle)}.", "${g.nachpruefung.urteil}."), ("quorum", "Unbelegt-Liste leer", "for (const b of x.unbelegt)", "for (const b of [])"), ("quorum", "Nicht pruefbar leer", "(x.stimmen[n].nicht_pruefbar || [])", "([])"), ("quorum", "Hinweise des Laufs fehlen im Bericht", "const alleHinweise = [...p.hinweise, ...laufHinweise]", "const alleHinweise = []"), ("quorum", "Lesemarke der Claude-Stimmen nicht geprueft", "lesemarke_ok: String(e.lesemarke).includes(p.lesemarke)", "lesemarke_ok: true"), ("quorum", "Lesemarke exakt verglichen", "lesemarke_ok: String(e.lesemarke).includes(p.lesemarke)", "lesemarke_ok: String(e.lesemarke) === p.lesemarke"), ("quorum", "Stelle fehlt in der Gruppenueberschrift", ", Stelle ${klartext(erste.stelle)}", ""), ("quorum", "Stand statt PAKET im Frontmatter", "'stand: PAKET'", "'stand: ' + p.stand"), ("quorum", "ablegen_befehl fehlt", "ablegen_befehl: md ? ablegenBefehl(paket) : null", "ablegen_befehl: null"), ("quorum", "ablegen_befehl auch ohne Bericht", "ablegen_befehl: md ? ablegenBefehl(paket) : null", "ablegen_befehl: ablegenBefehl(paket)"), ("quorum", "anzahl_dateien nicht geprueft", "if (!Number.isInteger(p.anzahl_dateien) || p.anzahl_dateien < 0) return", "if (false) return"), ("quorum", "paket_summe nicht verlangt", "'hinweise', 'paket_summe']", "'hinweise']"), ("quorum", "paket_summe nicht geprueft", "if (!/^[0-9a-f]{64}$/.test(p.paket_summe)) return", "if (false) return"), ("quorum", "ablegen_befehl ohne summe", " --summe ${q(p.paket_summe)}", ""), ("quorum", "Exit des Pakets nicht geprueft", "if (!/EXIT paket: 0(?!\\d)/.test(aus) || !m) {", "if (!m) {"), ("quorum", "Vorgabe sechs Warterunden", "const CODEX_RUNDEN = 3", "const CODEX_RUNDEN = 6"), ("quorum", "codex_runden 6 abgelehnt", "a.codex_runden <= 6", "a.codex_runden < 6"), ("quorum", "Suite ohne Vergleich des Arbeitsbaums", '" ] || echo "BEFUND: Die Suite hat', '" ] && echo "BEFUND: Die Suite hat'), ("quorum", "Arbeitsbaum-Vergleich frischt den Index auf", "const baum = `GIT_OPTIONAL_LOCKS=0 git -C", "const baum = `git -C"), ("quorum", "ordner ausserhalb des Vaults erlaubt", "if (vaultPfad(a.ordner) === null) throw", "if (false) throw"), ("quorum", "Kundendokument im Code-Projekt ohne README-Link", "if (teile[0].toLowerCase() === 'code projekte' && teile.length >= 2) return", "if (false) return"), ("quorum", "README-Tiefe um eins daneben", "'../'.repeat(teile.length - 1)", "'../'.repeat(teile.length)"), ("quorum", ".md bleibt im Wikilink", "u.replace(/\\.md$/i, '')", "u"), ("quorum", "Backslash im Ordner bleibt", "String(ordner).replace(/\\\\/g, '/')", "String(ordner)"), ("quorum", "Stimmen kennen einander", "Die anderen Stimmen kennst du nicht.", "Die anderen Stimmen sind Opus, Sonnet und Codex."), ("quorum", "Zusammenfuehren darf Dateien lesen", "Lies keine Dateien und fahre keine Befehle; du arbeitest nur mit dieser Liste.", "Lies die Dateien, wenn du sie brauchst."), ("quorum", "Klartext: Klammerfolge bleibt", r".replace(/\[{2,}/g, folge => folge.replace(/\[/g, '\\['))", ""), ("quorum", "Klartext: nur die erste Klammer entschaerft", r"folge => folge.replace(/\[/g, '\\['))", r"folge => '\\' + folge)"), ("quorum", "Klartext: Einbettung ausgenommen", r".replace(/\[{2,}/g,", r".replace(/(? z.replace(/\r$/, ''))", ""), ("welle-mit-pruefung", "Halt nur bei Exit 2", " const planBefunde = befundZeilen(aus)", " const planBefunde = exitsAller(r).includes(2) ? befundZeilen(aus) : []"), ("welle-mit-pruefung", "Befund nur im ersten Lauf gesucht", " const planBefunde = befundZeilen(aus)", " const planBefunde = befundZeilen(r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : '')"), ("welle-mit-pruefung", "Halt ohne Temp-Ordner", " temp_ordner: tempHinweis(temp),\n welle: null,", " temp_ordner: null,\n welle: null,"), ("plankritik", "Exit 2 mit Befund gilt nicht als gelaufen", "(exit === 2 && befunde.length > 0) || ", ""), ("plankritik", "Exit 2 ohne Befund gilt als gelaufen", "(exit === 2 && befunde.length > 0)", "exit === 2"), ("plankritik", "Exit 3 mit Marke gilt nicht als gelaufen", " || (name === 'plan_bloecke' && exit === 3 && /^Nichts geprueft:/m.test(abschnitt))", ""), ("plankritik", "Exit 3 ohne Marke gilt als gelaufen", "exit === 3 && /^Nichts geprueft:/m.test(abschnitt)", "exit === 3"), ("plankritik", "Befunde dem falschen Werkzeug zugeordnet", "werkzeugStand(aus, 'plan_zeichen', grenze >= 0 ? aus.slice(0, grenze) : aus)", "werkzeugStand(aus, 'plan_zeichen', aus)"), ("plankritik", "Hilfsagent-Meldung fuer gescheiterte Werkzeuge", " else log('Planwerkzeuge: ' + [zeichen, bloecke]", " else log('Planwerkzeuge: Hilfsagent ohne Ergebnis; ' + [zeichen, bloecke]"), ("plankritik", "Ausgabe trotz gescheitertem Werkzeug an die Linse", " if (zeichen.gelaufen && bloecke.gelaufen) ctx.werkzeuge = aus", " if (zeichen.gelaufen || bloecke.gelaufen) ctx.werkzeuge = aus"), ("plankritik", "materialisieren: Planbefund als Werkzeugfehler", " if (befunde.length) {\n befunde.forEach(", " if (false) {\n befunde.forEach("), ("plankritik", "materialisieren: Planbefund nicht erfasst", " befunde.forEach(befund => ctx.befunde.push({ werkzeug: 'plan_materialisiere', befund }))\n", ""), ("plankritik", "materialisieren: Temp-Ordner nicht genannt", " if (m) ctx.temp = m[1].replace(/\\/mat$/, '')\n log(", " log("), ("plankritik", "Befundzeile ohne Zeilennummer", r"const BEFUNDZEILE = /^BEFUND [^\s:]+:\d+:/", r"const BEFUNDZEILE = /^BEFUND /"), ("plankritik", "CR bleibt in der Befundzeile", r".map(z => z.replace(/\r$/, ''))", ""), ("plankritik", "gebremst ohne werkzeug_befunde", " werkzeug_befunde: ctx.befunde,\n ohne_ergebnis: gewaehlt,", " ohne_ergebnis: gewaehlt,"), ("plankritik", "fertig ohne werkzeug_befunde", " werkzeug_befunde: ctx.befunde,\n ohne_ergebnis: ohneErgebnis,", " ohne_ergebnis: ohneErgebnis,"), ("plankritik", "Planwerkzeuge nur im ersten Lauf gelesen", " const aus = ausgabeAller(r)\n const grenze", " const aus = String(lauf.ausgabe || '')\n const grenze"), ("plankritik", "materialisieren: Befund nur im ersten Lauf", " const befunde = befundZeilen(aus)", " const befunde = befundZeilen(r.laeufe[0].ausgabe)"), # Alle laeufe an fuenf weiteren Stellen (2026-11-02, Folgeauftrag): je Stelle wieder nur laeufe[0], fuer die Kopie auch das Exit nur eines Laufs ("welle-mit-pruefung", "Kopie: KOPIE-Zeile nur im ersten Lauf", r" const m = /^KOPIE=(.+?)\s*$/m.exec(aus)", r" const m = /^KOPIE=(.+?)\s*$/m.exec(r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : '')"), ("welle-mit-pruefung", "Kopie: angewendete Bloecke nur im ersten Lauf", " const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(aus)", " const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : '')"), ("welle-mit-pruefung", "Kopie: nur das Exit des ersten Laufs", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r)[0] === 0"), ("welle-mit-pruefung", "Kopie: nur das Exit des letzten Laufs", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r).slice(-1)[0] === 0"), ("welle-mit-pruefung", "Vergleich nur aus dem ersten Lauf", "kopfUndEnde(ausgabeAller(belege.vergleich), 1500, 2500)", "kopfUndEnde(belege.vergleich && belege.vergleich.laeufe[0] ? belege.vergleich.laeufe[0].ausgabe : '', 1500, 2500)"), ("plankritik", "materialisieren: KOPIE-Zeile nur im ersten Lauf", r" const m = /^KOPIE=(.+?)\s*$/m.exec(aus)", r" const m = /^KOPIE=(.+?)\s*$/m.exec(r.laeufe[0].ausgabe)"), ("plankritik", "materialisieren: angewendete Bloecke nur im ersten Lauf", " const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(aus)", " const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(r.laeufe[0].ausgabe)"), ("plankritik", "materialisieren: nur das Exit des ersten Laufs", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r)[0] === 0"), ("plankritik", "materialisieren: nur das Exit des letzten Laufs", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r).slice(-1)[0] === 0"), ("plankritik", "Mutantenlauf nur aus dem ersten Lauf", " const aus = ausgabeAller(r)\n const exitMatch", " const aus = r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''\n const exitMatch"), ("quorum", "Hilfsagenten-Ausgabe nur aus dem ersten Lauf", r" return laufTexte(r).join('\n')", r" return laufTexte(r).slice(0, 1).join('\n')"), ("plankritik", "KEINE_MUTANTEN_TOML unverankert", r"if (/^KEINE_MUTANTEN_TOML\s*$/m.test(aus))", "if (/KEINE_MUTANTEN_TOML/.test(aus))"), ("welle-mit-pruefung", "Kopie: Lauf ohne exit zaehlt als 0", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0 || x == null)"), ("plankritik", "materialisieren: Lauf ohne exit zaehlt als 0", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0 || x == null)"), ("welle-mit-pruefung", "Vergleich umgekehrt verbunden", "kopfUndEnde(ausgabeAller(belege.vergleich), 1500, 2500)", "kopfUndEnde(ausgabeAller({ laeufe: [...((belege.vergleich && belege.vergleich.laeufe) || [])].reverse() }), 1500, 2500)"), ("plankritik", "materialisieren: ohne-Ergebnis-Wache fehlt", " if (!exits.length) { log(`${wozu}: Hilfsagent ohne Ergebnis, keine Kopie`); return null }\n", ""), ] def main() -> int: ordner = Path(sys.argv[1]) if len(sys.argv) > 1 else HIER.parent.parent / ".claude" / "workflows" ueberlebt, veraltet = [], [] for datei, name, alt, neu in MUTANTEN: with tempfile.TemporaryDirectory() as tmp: ziel = Path(tmp) / "wf" shutil.copytree(ordner, ziel) pfad = ziel / (datei + ".js") text = pfad.read_text(encoding="utf-8") if text.count(alt) != 1: veraltet.append(name) print(f"VERALTET {datei}: {name} (Muster {text.count(alt)}-mal)") continue pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n") lauf = subprocess.run(["node", str(FAELLE), str(ziel)], capture_output=True, text=True, encoding="utf-8", errors="replace") rot = [z for z in lauf.stdout.splitlines() if z.startswith("FEHLER")] if lauf.returncode == 0: grund = WIRKUNGSLOS.get(name) if grund: print(f"WIRKUNGSLOS {datei}: {name} ({grund})") else: ueberlebt.append(name) print(f"UEBERLEBT {datei}: {name}") else: print(f"GEFANGEN {datei}: {name} ({len(rot)} rot, z. B. {rot[0][7:90] if rot else 'Absturz'})") print(f"{len(MUTANTEN)} Mutanten, {len(ueberlebt)} ueberlebt, {len(veraltet)} veraltet") return 2 if veraltet else 1 if ueberlebt else 0 if __name__ == "__main__": sys.stdout.reconfigure(errors="replace") sys.exit(main())