--- type: plan-task title: "Task 03 — Grundlinie, Fälle und Mutanten für quorum.js" status: draft summary: "faelle_workflows.mjs bekommt einen Abschnitt quorum mit eigener Antwort-Attrappe, Grundlinie und Angriffsliste; mutanten_workflows.py bekommt die Mutanten, die diese Fälle fangen sollen." tags: [plan-task, vault-tools, codeworkflow] created: 2026-09-29 updated: 2026-09-29 modell: haiku modell_begruendung: "Abschrift: Beide Blöcke stehen wörtlich im Plan und werden per plan_materialisiere.py angewendet; Zieldateien und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche oder Auslassungspunkte." --- # Task 03 — Grundlinie, Fälle und Mutanten für `quorum.js` Die globalen Vorgaben stehen in `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben“. Arbeitsverzeichnis ist `D:/Claude`. **Voraussetzung:** Task 02 ist angewendet, `.claude/workflows/quorum.js` liegt im Baum. Deshalb Welle 2. Was danach anders ist: `node .vault-tools/workflow-pruefung/faelle_workflows.mjs` fährt `quorum.js` mit Grundlinie und Angriffsliste gegen die Attrappe, und `mutanten_workflows.py` baut in `quorum.js` die Fehler ein, die diese Fälle fangen sollen. Die Antwort-Attrappe für das Zusammenführen gruppiert wie ein sorgfältiger Agent nach Datei und Stelle; die für die Nachprüfung urteilt, wie der jeweilige Fall es vorgibt. ## Zieldateien - `D:\Claude\.vault-tools\workflow-pruefung\faelle_workflows.mjs` (geändert) - `D:\Claude\.vault-tools\workflow-pruefung\mutanten_workflows.py` (geändert) ## Verbote - Keine andere Datei, auch nicht `attrappe.mjs` und keine Datei unter `.claude/workflows/`. - Kein Lauf eines echten Workflows, kein Aufruf des Codex-Plugins. - Nichts von Hand schreiben oder umformulieren; ein Widerspruch wird gemeldet. - Kein Commit, kein `git add`. ## Schritt 1 — Blöcke anwenden ``` python D:/Claude/.vault-tools/plan-kopie/plan_bloecke.py vor D:/Claude/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/task-03-faelle-und-mutanten.md --projekt D:/Claude ``` Erwartet: `0 Befund(e).` ``` python D:/Claude/.vault-tools/plan-kopie/plan_materialisiere.py D:/Claude/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum D:/Claude task-03-faelle-und-mutanten.md ``` Erwartet: `'ersetzt': 2` und `0 Befund(e).` ### Quorum-Fälle in `.vault-tools/workflow-pruefung/faelle_workflows.mjs` Aus: ``` // ---------------------------------------------------------------- Lauf ``` wird: ```javascript // ---------------------------------------------------------------- quorum // Fälle für quorum.js (Plan .vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/). Eigene // Antworten, weil die Labels der Stimmen und der Codex-Hilfsagenten nur hier vorkommen. Vorgabe: // Opus, Sonnet und Codex melden denselben Befund an src/a.py Zeile 10 (Mehrheit, blockierend), // Opus zusätzlich einen wichtigen Einzelbefund, Sonnet einen Hinweis. const Q_ARGS = { anlass: 'einspielen', projekt: 'D:/x' } const Q_MARKE = '0123456789abcdef' const qPaket = (x = {}) => ({ ordner: 'C:/tmp/q', anlass: 'einspielen', art: 'code', wurzel: 'D:/x', leer: false, basis: 'a'.repeat(40), stand: 'b'.repeat(40), dateien: [{ pfad: 'src/a.py', art: 'geaendert' }], lesemarke: Q_MARKE, bericht_pfad: 'D:/x/docs/quorum/2026-09-30-einspielen.md', nummer: 2, datum: '2026-09-30', vorher: { pfad: 'D:/x/docs/quorum/2026-09-20-version.md', nummer: 1, urteil: 'frei' }, suite: 'ohne', hinweise: [], ...x }) const qB = (datei, stelle, schwere, beleg = 'zitierte Zeile') => ({ datei, stelle, schwere, aussage: `${schwere} in ${datei} ${stelle}`, beleg }) const Q_STIMMEN = { opus: [qB('src/a.py', '10', 'blockierend'), qB('src/a.py', '30', 'wichtig')], sonnet: [qB('src/a.py', '10', 'wichtig'), qB('src/b.py', '5', 'hinweis')], codex: [qB('src/a.py', '10', 'blockierend')], haiku: [qB('src/c.py', '1', 'wichtig')], } const Q_START = '{\n "running": []\n}\nEXIT status: 0\n{\n "jobId": "task-q1",\n "status": "queued"\n}\nEXIT task: 0' const Q_WARTEN_FERTIG = '{\n "waitTimedOut": false\n}\nEXIT warten: 0' const Q_WARTEN_OFFEN = '{\n "waitTimedOut": true\n}\nEXIT warten: 0' // Gruppiert wie ein sorgfältiger Sonnet-Agent: gleiche Datei und gleiche Stelle. function qGruppiere(prompt) { const jeStelle = new Map() for (const m of prompt.matchAll(/^\[([A-Z]\d+)\] Stimme \S+ \| (.+?) \| (.+?) \| \S+$/gm)) { const k = m[2] + '|' + m[3] if (!jeStelle.has(k)) jeStelle.set(k, []) jeStelle.get(k).push(m[1]) } return { gruppen: [...jeStelle.values()].map(ids => ({ ids, begruendung: 'gleiche Stelle' })) } } function qUrteile(prompt, urteil = () => 'bestaetigt') { return { urteile: [...prompt.matchAll(/^Befund ([A-Z]\d+):$/gm)].map(m => ({ id: m[1], urteil: urteil(m[1]), fundstelle: 'src/a.py:1, zitiert', begruendung: 'nachgestellt' })) } } function antwortenQuorum(k = {}) { let n = 0 const stimmen = k.stimmen || Q_STIMMEN const ohne = k.ohne || [] const lauf = (label, text) => ok({ laeufe: [{ befehl: label, exit: 0, ausgabe: text }] }) return async aufruf => { const label = aufruf.label || '' if (ohne.includes(label)) return undefined if (label.startsWith('verbrauch:')) { const x = (k.ablesungen || [])[n++] if (x === OHNE) return undefined return ok(x === undefined ? ablesung(10, 10) : x) } if (label === 'werkzeug:paket') return lauf(label, k.paketAusgabe !== undefined ? k.paketAusgabe : 'Paket in C:/tmp/q\nQUORUM_PAKET ' + JSON.stringify(qPaket(k.paket)) + '\nEXIT paket: 0') if (label === 'codex:start') return lauf(label, k.codexStart !== undefined ? k.codexStart : Q_START) if (label.startsWith('codex:warten:')) return lauf(label, k.codexWarten ? k.codexWarten(Number(label.slice('codex:warten:'.length))) : Q_WARTEN_FERTIG) if (label === 'codex:abbruch') return lauf(label, '{\n "status": "cancelled"\n}\nEXIT cancel: 0') if (label === 'codex:ende') return ok({ exit_codex: 0, job_status: 'completed', log_gelesen: true, usage_limit_treffer: 0, antwort_gefunden: true, dauer_s: 192, lesebeleg: 'Lesemarke: ' + Q_MARKE, befunde: stimmen.codex || [], nicht_pruefbar: [], ...(k.codexEnde || {}) }) if (label.startsWith('stimme:')) return ok({ lesemarke: Q_MARKE, befunde: stimmen[label.slice('stimme:'.length)] || [], nicht_pruefbar: [] }, 30) if (label === 'zusammenfuehren') return ok(k.gruppen ? k.gruppen(aufruf.prompt) : qGruppiere(aufruf.prompt)) if (label === 'nachpruefung') return ok(k.urteile ? k.urteile(aufruf.prompt) : qUrteile(aufruf.prompt)) throw new Error('unerwarteter Agent ' + label) } } const QR = (a, k, o = {}) => fuehreAus(WF('quorum'), a, antwortenQuorum(k), { ordner: ORDNER, ...o }) const qGruppe = (e, status) => (e && Array.isArray(e.gruppen) ? e.gruppen.filter(g => g.status === status) : []) // Die IDs, die der Agent mit diesem Label unter "Befund :" zur Nachprüfung bekam. function qNachgeprueft(p) { const a = mitLabel(p, 'nachpruefung')[0] return a ? [...a.prompt.matchAll(/^Befund ([A-Z]\d+):$/gm)].map(m => m[1]) : null } fall('quorum', 'Grundlinie: Paket, drei Stimmen gleichzeitig, Mehrheit sperrt, Einzelbefund nachgeprüft, Bericht', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, {}) soll(f === null, 'Fehler: ' + f) soll(e && e.status === 'fertig' && e.urteil === 'nicht_frei', 'status/urteil ' + JSON.stringify(e && [e.status, e.urteil, e.grund])) const erst = p.agents.slice().sort((x, y) => x.start - y.start)[0] soll(erst && erst.label === 'werkzeug:paket' && erst.modell === 'sonnet' && erst.effort === 'low', 'erster Agent ' + JSON.stringify(erst && [erst.label, erst.modell, erst.effort])) const ab = mitLabel(p, 'verbrauch:').map(x => x.label) soll(JSON.stringify(ab) === JSON.stringify(['verbrauch:quorum:vor Stimmen', 'verbrauch:quorum:Ende']), 'Ablesungen: ' + JSON.stringify(ab)) const opus = mitLabel(p, 'stimme:opus')[0] const sonnet = mitLabel(p, 'stimme:sonnet')[0] soll(opus && opus.modell === 'opus' && opus.agentType === 'quorum-stimme' && opus.effort === undefined, 'Opus-Stimme ' + JSON.stringify(opus && [opus.modell, opus.agentType, opus.effort])) soll(sonnet && sonnet.modell === 'sonnet' && sonnet.agentType === 'quorum-stimme' && sonnet.effort === undefined, 'Sonnet-Stimme ' + JSON.stringify(sonnet && [sonnet.modell, sonnet.agentType, sonnet.effort])) soll(opus && /^Auftragsart: Stimme\.$/m.test(opus.prompt) && opus.prompt.includes('Prüfpaket: C:/tmp/q/paket.md') && opus.prompt.includes('Wurzel: D:/x'), 'Auftrag der Stimme ohne Auftragsart, Paket oder Wurzel') soll(mitLabel(p, 'stimme:haiku').length === 0, 'Haiku trotz gültiger Codex-Stimme') const start = mitLabel(p, 'codex:start')[0] soll(start && laufendBeiStart(p, start, 'stimme:') === 2, 'Codex startete nicht neben Opus und Sonnet') soll(mitLabel(p, 'codex:').length === 3 && mitLabel(p, 'codex:').every(x => x.modell === 'sonnet' && x.effort === 'low' && !x.agentType), 'Codex-Hilfsagenten: ' + JSON.stringify(mitLabel(p, 'codex:').map(x => [x.label, x.modell, x.effort]))) const zf = mitLabel(p, 'zusammenfuehren')[0] const np = mitLabel(p, 'nachpruefung')[0] soll(zf && zf.modell === 'sonnet' && zf.effort === 'medium' && !zf.agentType, 'Zusammenführen ' + JSON.stringify(zf && [zf.modell, zf.effort, zf.agentType])) soll(np && np.modell === 'sonnet' && np.agentType === 'quorum-stimme' && /^Auftragsart: Nachprüfung\.$/m.test(np.prompt), 'Nachprüfung ' + JSON.stringify(np && [np.modell, np.agentType])) soll(JSON.stringify(qNachgeprueft(p)) === '["O2"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p))) const g = qGruppe(e, 'mehrheit')[0] soll(g && g.schwere === 'blockierend' && JSON.stringify(g.stimmen.slice().sort()) === '["codex","opus","sonnet"]', 'Mehrheitsgruppe ' + JSON.stringify(g && [g.schwere, g.stimmen])) soll(qGruppe(e, 'hinweis_ungeprueft').length === 1 && qGruppe(e, 'bestaetigt').length === 1, 'Status der Gruppen ' + JSON.stringify(e && e.gruppen && e.gruppen.map(x => x.status))) soll(e && JSON.stringify(e.zaehlung.opus) === JSON.stringify({ abgegeben: true, gesamt: 2, belegt: 2, bestaetigt: 2, alleinfunde: 1 }), 'zaehlung opus ' + JSON.stringify(e && e.zaehlung && e.zaehlung.opus)) soll(e && e.zaehlung.sonnet.bestaetigt === 1 && e.zaehlung.sonnet.alleinfunde === 0 && e.zaehlung.codex.bestaetigt === 1 && !('haiku' in e.zaehlung), 'zaehlung ' + JSON.stringify(e && e.zaehlung)) soll(e && e.bericht_pfad === 'D:/x/docs/quorum/2026-09-30-einspielen.md', 'bericht_pfad ' + (e && e.bericht_pfad)) const md = (e && e.bericht_md) || '' for (const z of ['---\ntype: note\n', '\nnummer: 2\n', '\nanlass: einspielen\n', '\nurteil: nicht_frei\n', `\nbasis: "${'a'.repeat(40)}"\n`, `\nstand: ${'b'.repeat(40)}\n`, '\ncodex: abgegeben\n---\n', 'Projekt: [README](../../README.md)']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z)) ohneVerstoss(p, soll) return fehler }) fall('quorum', 'Bericht: Stimmentabelle mit Codex-Laufzeit, Mehrheit zuerst, Verbrauch vor und nach dem Lauf', async () => { const { fehler, soll } = pruefe() const { ergebnis: e } = await QR(Q_ARGS, {}) const md = (e && e.bericht_md) || '' for (const z of [ '| Opus | ja | 2 | 2 | 2 | 1 | nicht gemessen |', '| Codex | ja | 1 | 1 | 1 | 0 | 3 min 12 s |', '### G1: blockierend, Mehrheit (Opus, Sonnet, Codex)', 'Verbrauch vor den Stimmen: 5 Stunden 10 %, Woche 10 %; am Ende: 5 Stunden 10 %, Woche 10 %.', ]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z)) return fehler }) for (const [urteil, erwartet] of [['widerlegt', 'frei'], ['bestaetigt', 'nicht_frei']]) { fall('quorum', `blockierender Einzelbefund ${urteil}: ${erwartet}`, async () => { const { fehler, soll } = pruefe() const stimmen = { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/b.py', '5', 'hinweis')], codex: [] } const { ergebnis: e } = await QR(Q_ARGS, { stimmen, urteile: pr => qUrteile(pr, () => urteil) }) soll(e && e.status === 'fertig' && e.urteil === erwartet, JSON.stringify(e && [e.status, e.urteil, e.grund])) soll(e && e.zaehlung.opus.alleinfunde === (urteil === 'bestaetigt' ? 1 : 0), 'alleinfunde ' + JSON.stringify(e && e.zaehlung && e.zaehlung.opus)) return fehler }) } fall('quorum', 'Nachprüfung ohne Ergebnis: ungeprüfter blockierender Einzelbefund sperrt, ungeprüfter wichtiger nicht', async () => { const { fehler, soll } = pruefe() const a = await QR(Q_ARGS, { stimmen: { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/b.py', '3', 'wichtig')], codex: [] }, ohne: ['nachpruefung'] }) soll(a.ergebnis && a.ergebnis.urteil === 'nicht_frei' && qGruppe(a.ergebnis, 'nicht_geprueft').length === 2, 'blockierend: ' + JSON.stringify(a.ergebnis && [a.ergebnis.urteil, a.ergebnis.gruppen.map(g => g.status)])) soll(a.ergebnis && a.ergebnis.hinweise.some(h => /Nachprüfung ohne Ergebnis/.test(h)), 'kein Hinweis auf die fehlende Nachprüfung') const b = await QR(Q_ARGS, { stimmen: { opus: [qB('src/a.py', '10', 'wichtig')], sonnet: [qB('src/b.py', '3', 'wichtig')], codex: [] }, ohne: ['nachpruefung'] }) soll(b.ergebnis && b.ergebnis.urteil === 'frei', 'wichtig: ' + JSON.stringify(b.ergebnis && [b.ergebnis.urteil, b.ergebnis.grund])) return fehler }) fall('quorum', 'nur Hinweise mit je einer Stimme: keine Nachprüfung, frei', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen: { opus: [qB('src/a.py', '1', 'hinweis')], sonnet: [qB('src/b.py', '2', 'hinweis')], codex: [] } }) soll(mitLabel(p, 'nachpruefung').length === 0, 'Nachprüfung für Hinweise gestartet') soll(e && e.urteil === 'frei' && qGruppe(e, 'hinweis_ungeprueft').length === 2, JSON.stringify(e && [e.urteil, e.gruppen.map(g => g.status)])) return fehler }) fall('quorum', 'Befund ohne Beleg zählt nicht für die Mehrheit und geht nicht ins Zusammenführen', async () => { const { fehler, soll } = pruefe() const stimmen = { opus: [qB('src/a.py', '10', 'blockierend', ' '), qB('src/b.py', '1', 'wichtig')], sonnet: [qB('src/a.py', '10', 'blockierend')], codex: [] } const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen }) const zf = mitLabel(p, 'zusammenfuehren')[0] soll(zf && !zf.prompt.includes('[O1]') && zf.prompt.includes('[O2]') && zf.prompt.includes('[S1]'), 'Zusammenführen: ' + (zf && zf.prompt.slice(0, 400))) soll(e && JSON.stringify(e.unbelegt.map(b => b.id)) === '["O1"]', 'unbelegt ' + JSON.stringify(e && e.unbelegt)) soll(qGruppe(e, 'mehrheit').length === 0, 'Mehrheit trotz unbelegtem Befund') soll(JSON.stringify(qNachgeprueft(p)) === '["O2","S1"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p))) soll(e && e.zaehlung.opus.gesamt === 2 && e.zaehlung.opus.belegt === 1, JSON.stringify(e && e.zaehlung && e.zaehlung.opus)) return fehler }) fall('quorum', 'Gruppe über verschiedene Dateien wird aufgelöst: keine Mehrheit', async () => { const { fehler, soll } = pruefe() const stimmen = { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/b.py', '10', 'blockierend')], codex: [] } const { ergebnis: e } = await QR(Q_ARGS, { stimmen, gruppen: () => ({ gruppen: [{ ids: ['O1', 'S1'], begruendung: 'zusammen' }] }), urteile: pr => qUrteile(pr, () => 'widerlegt') }) soll(e && qGruppe(e, 'mehrheit').length === 0 && e.urteil === 'frei', JSON.stringify(e && [e.urteil, e.gruppen.map(g => g.status)])) soll(e && e.hinweise.some(h => /aufgelöst: verschiedene Dateien/.test(h)), 'kein Hinweis auf die aufgelöste Gruppe: ' + JSON.stringify(e && e.hinweise)) return fehler }) fall('quorum', 'Zusammenführen mit unbekannter und doppelter ID: beide verworfen, keine Mehrheit', async () => { const { fehler, soll } = pruefe() const stimmen = { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/a.py', '10', 'blockierend')], codex: [] } const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen, gruppen: () => ({ gruppen: [{ ids: ['O1', 'X9'], begruendung: 'a' }, { ids: ['O1', 'S1'], begruendung: 'b' }] }) }) soll(qGruppe(e, 'mehrheit').length === 0, 'Mehrheit aus einer doppelt vergebenen ID') soll(e && e.hinweise.some(h => /unbekannte ID X9/.test(h)) && e.hinweise.some(h => /ID O1 steht in zwei Gruppen/.test(h)), 'Hinweise: ' + JSON.stringify(e && e.hinweise)) soll(JSON.stringify(qNachgeprueft(p)) === '["O1","S1"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p))) return fehler }) fall('quorum', 'Zusammenführen ohne Ergebnis: jeder belegte Befund eine Gruppe, alle Einzelbefunde über Hinweis nachgeprüft', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { ohne: ['zusammenfuehren'] }) soll(qGruppe(e, 'mehrheit').length === 0, 'Mehrheit ohne Zusammenführen') soll(JSON.stringify(qNachgeprueft(p)) === '["O1","O2","S1","C1"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p))) soll(e && e.hinweise.some(h => /Zusammenführen ohne Ergebnis/.test(h)), 'kein Hinweis') return fehler }) for (const [erst, zweit] of [['wichtig', 'blockierend'], ['blockierend', 'wichtig']]) { fall('quorum', `Mehrheitsgruppe aus ${erst} und ${zweit} zählt als blockierend`, async () => { const { fehler, soll } = pruefe() const stimmen = { opus: [qB('src/a.py', '10', erst)], sonnet: [qB('src/a.py', '10', zweit)], codex: [] } const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen }) const g = qGruppe(e, 'mehrheit')[0] soll(g && g.schwere === 'blockierend' && e.urteil === 'nicht_frei', JSON.stringify(e && [e.urteil, g && g.schwere])) soll(mitLabel(p, 'nachpruefung').length === 0, 'Mehrheitsgruppe nachgeprüft') return fehler }) } fall('quorum', 'Reihenfolge: blockierender Einzelbefund an erster und letzter Stelle einer Stimme wird nachgeprüft', async () => { const { fehler, soll } = pruefe() const stimmen = { opus: [qB('src/a.py', '1', 'blockierend'), qB('src/x.py', '2', 'hinweis'), qB('src/y.py', '3', 'hinweis'), qB('src/a.py', '99', 'blockierend')], sonnet: [qB('src/z.py', '1', 'hinweis')], codex: [] } const { protokoll: p } = await QR(Q_ARGS, { stimmen }) soll(JSON.stringify(qNachgeprueft(p)) === '["O1","O4"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p))) return fehler }) for (const [name, k, grund] of [ ['Status vor dem Lauf nicht gelesen', { codexStart: 'Fehler beim Lesen\nEXIT status: 1' }, /Status vor dem Lauf/], ['Auftrag ohne jobId', { codexStart: '{\n "running": []\n}\nEXIT status: 0\nFehler\nEXIT task: 1' }, /nicht gestartet/], ['usage limit im Log', { codexEnde: { usage_limit_treffer: 1 } }, /usage limit/], ['Lesebeleg mit fremder Lesemarke', { codexEnde: { lesebeleg: 'Lesemarke: ffffffffffffffff' } }, /Lesebeleg/], ['Job endet failed', { codexEnde: { job_status: 'failed' } }, /failed/], ['Log nicht gelesen', { codexEnde: { log_gelesen: false } }, /Log nicht gelesen/], ['keine Antwort im Format', { codexEnde: { antwort_gefunden: false } }, /Format/], ['Auswertung mit Exit 2', { codexEnde: { exit_codex: 2 } }, /Exit 2/], ['Auswertung ohne Ergebnis', { ohne: ['codex:ende'] }, /ohne Ergebnis/], ]) { fall('quorum', `Codex ungültig (${name}): Haiku springt nach den übrigen Stimmen ein, Bericht sagt es`, async () => { const { fehler, soll } = pruefe() const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, k) const haiku = mitLabel(p, 'stimme:haiku')[0] soll(haiku && haiku.modell === 'haiku' && haiku.agentType === 'quorum-stimme', 'Haiku nicht eingesprungen') soll(e && e.stimmen.codex.abgegeben === false && grund.test(e.stimmen.codex.grund), 'grund ' + JSON.stringify(e && e.stimmen.codex.grund)) soll(e && e.bericht_md && e.bericht_md.includes('\ncodex: ersetzt\n') && e.bericht_md.includes('| Haiku (Ersatz für Codex) | ja |'), 'Bericht ohne Ersatz') const vorher = p.agents.filter(x => /^(stimme:opus|stimme:sonnet|codex:)/.test(x.label || '')) soll(haiku && vorher.every(x => x.ende !== null && x.ende < haiku.start), 'Haiku startete vor dem Ende der übrigen Stimmen') soll(!/Status|jobId/.test(name) || mitLabel(p, 'codex:warten').length === 0, 'gewartet ohne gestarteten Auftrag') return fehler }) } fall('quorum', 'Codex nie fertig: so viele Warterunden wie codex_runden, dann Abbruch und Haiku', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, protokoll: p } = await QR({ ...Q_ARGS, codex_runden: 2 }, { codexWarten: () => Q_WARTEN_OFFEN }) soll(mitLabel(p, 'codex:warten:').length === 2, 'Warterunden: ' + mitLabel(p, 'codex:warten:').length) const ab = mitLabel(p, 'codex:abbruch')[0] soll(ab && ab.prompt.includes("cancel 'task-q1' --json"), 'Abbruch fehlt') soll(mitLabel(p, 'codex:ende').length === 0 && mitLabel(p, 'stimme:haiku').length === 1, 'Ende oder Haiku: ' + JSON.stringify(p.agents.map(x => x.label))) soll(e && /nach 2 Warterunden/.test(e.stimmen.codex.grund), 'grund ' + JSON.stringify(e && e.stimmen.codex.grund)) return fehler }) fall('quorum', 'Warten scheitert: Abbruch nach der ersten Runde und Haiku', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexWarten: () => 'kein Job\nEXIT warten: 1' }) soll(mitLabel(p, 'codex:warten:').length === 1 && mitLabel(p, 'codex:abbruch').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label))) soll(e && /Warten auf Codex gescheitert/.test(e.stimmen.codex.grund) && mitLabel(p, 'stimme:haiku').length === 1, 'grund ' + JSON.stringify(e && e.stimmen.codex.grund)) return fehler }) fall('quorum', 'codex false: kein Codex-Agent, Haiku springt ein', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, protokoll: p } = await QR({ ...Q_ARGS, codex: false }, {}) soll(mitLabel(p, 'codex:').length === 0 && mitLabel(p, 'stimme:haiku').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label))) soll(e && /abgeschaltet/.test(e.stimmen.codex.grund) && e.bericht_md.includes('\ncodex: ersetzt\n'), 'grund oder Bericht: ' + JSON.stringify(e && e.stimmen.codex.grund)) return fehler }) fall('quorum', 'Codex-Befehle: Plugin 1.0.6, nur lesend, Auftrag aus dem Paket, Warten mit zehn Minuten', async () => { const { fehler, soll } = pruefe() const { protokoll: p } = await QR(Q_ARGS, {}) const start = mitLabel(p, 'codex:start')[0] const warten = mitLabel(p, 'codex:warten:1')[0] const ende = mitLabel(p, 'codex:ende')[0] for (const z of ['openai-codex/codex/1.0.6/scripts/codex-companion.mjs', `status --json --cwd 'D:/x'`, `task --background --json --cwd 'D:/x' --prompt-file 'C:/tmp/q/codex-auftrag.md'`]) soll(start && start.prompt.includes(z), 'Start ohne ' + z) soll(p.agents.every(x => !String(x.prompt).includes('--write')), 'ein Auftrag enthält --write') soll(warten && warten.prompt.includes(`status 'task-q1' --wait --timeout-ms 540000 --json --cwd 'D:/x'`) && /timeout auf 600000/.test(warten.prompt), 'Warten: ' + (warten && warten.prompt)) for (const z of [`result 'task-q1' --json --cwd 'D:/x' > 'C:/tmp/q/codex-ergebnis.json'`, `quorum_werkzeug.py' codex --ergebnis 'C:/tmp/q/codex-ergebnis.json'`]) soll(ende && ende.prompt.includes(z), 'Ende ohne ' + z) return fehler }) fall('quorum', 'gebremst vor den Stimmen: keine Stimme, kein Urteil, kein Bericht', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { ablesungen: [ablesung(90, 10)] }) soll(f === null, 'Fehler: ' + f) soll(mitLabel(p, 'stimme:').length === 0 && mitLabel(p, 'codex:').length === 0, 'Stimme trotz Bremse: ' + JSON.stringify(p.agents.map(x => x.label))) soll(e && e.status === 'gebremst' && e.urteil === 'kein_urteil' && e.bericht_md === null && e.bericht_pfad === null, JSON.stringify(e && [e.status, e.urteil, e.bericht_pfad])) soll(e && /neu starten/.test(e.naechster_schritt), 'naechster_schritt ' + (e && e.naechster_schritt)) return fehler }) fall('quorum', 'Ablesung ohne Ergebnis: Opus-Stimme läuft nach der alten Obergrenze', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { ablesungen: [OHNE] }) soll(mitLabel(p, 'stimme:opus').length === 1 && e && e.status === 'fertig' && e.verbrauch[0].status === 'nicht_gelesen', JSON.stringify(e && [e.status, e.verbrauch[0]])) return fehler }) fall('quorum', 'als Kind: verbrauch.js wirft, Rückfall, Stimmen laufen', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, {}, { tiefe: 1 }) soll(f === null && e && e.status === 'fertig', 'Fehler/status: ' + f + ' ' + (e && e.status)) soll(mitLabel(p, 'stimme:opus').length === 1 && mitLabel(p, 'verbrauch:').length === 0, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label))) soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch)) return fehler }) for (const vorUrteil of ['frei', 'nicht_frei']) { fall('quorum', `leerer Unterschied: nur der Paket-Agent, Urteil ${vorUrteil} übernommen`, async () => { const { fehler, soll } = pruefe() const paket = { leer: true, dateien: [], stand: 'a'.repeat(40), vorher: { pfad: 'D:/x/docs/quorum/2026-09-20-version.md', nummer: 1, urteil: vorUrteil } } const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { paket }) soll(f === null, 'Fehler: ' + f) soll(p.agents.length === 1 && p.agents[0].label === 'werkzeug:paket', 'Agenten ' + JSON.stringify(p.agents.map(x => x.label))) soll(e && e.status === 'leer' && e.urteil === vorUrteil && e.verbrauch.length === 0, JSON.stringify(e && [e.status, e.urteil, e.verbrauch])) const md = (e && e.bericht_md) || '' for (const z of ['\ncodex: nicht gelaufen\n', `\nurteil: ${vorUrteil}\n`, '2026-09-20-version.md']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z)) return fehler }) } fall('quorum', 'Paket nicht gebaut: Fehler mit der BEFUND-Zeile, keine Stimme', async () => { const { fehler, soll } = pruefe() const { fehler: f, protokoll: p } = await QR(Q_ARGS, { paketAusgabe: 'BEFUND: Arbeitsbaum nicht sauber, die Stimmen lesen ihn statt HEAD: M src/a.py\nEXIT paket: 2' }) soll(f && /Paket nicht gebaut/.test(f) && /Arbeitsbaum nicht sauber/.test(f), 'Fehler: ' + f) soll(p.agents.length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label))) return fehler }) for (const [name, k, text] of [ ['Lesemarke unbrauchbar', { paket: { lesemarke: 'xyz' } }, /unbrauchbarer Lesemarke/], ['Feld fehlt', { paketAusgabe: 'QUORUM_PAKET {"ordner":"C:/tmp/q"}\nEXIT paket: 0' }, /QUORUM_PAKET ohne/], ['kein JSON', { paketAusgabe: 'QUORUM_PAKET {kaputt}\nEXIT paket: 0' }, /kein JSON/], ]) { fall('quorum', `QUORUM_PAKET kaputt (${name}): Fehler, keine Stimme`, async () => { const { fehler, soll } = pruefe() const { fehler: f, protokoll: p } = await QR(Q_ARGS, k) soll(f && text.test(f), 'Fehler: ' + f) soll(p.agents.length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label))) return fehler }) } fall('quorum', 'weniger als zwei Stimmen: kein Urteil, kein Bericht, Ablesung am Ende', async () => { const { fehler, soll } = pruefe() const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { ohne: ['stimme:sonnet', 'stimme:haiku'], codexEnde: { usage_limit_treffer: 1 } }) soll(e && e.status === 'unvollstaendig' && e.urteil === 'kein_urteil' && e.bericht_md === null && e.bericht_pfad === null, JSON.stringify(e && [e.status, e.urteil, e.bericht_pfad])) soll(mitLabel(p, 'zusammenfuehren').length === 0 && mitLabel(p, 'nachpruefung').length === 0, 'Zusammenführen oder Nachprüfung trotz einer Stimme') soll(mitLabel(p, 'verbrauch:').length === 2, 'Ablesungen: ' + mitLabel(p, 'verbrauch:').length) return fehler }) fall('quorum', 'zwei Stimmen reichen: Opus ohne Ergebnis, Bericht nennt es', async () => { const { fehler, soll } = pruefe() const { ergebnis: e } = await QR(Q_ARGS, { ohne: ['stimme:opus'] }) soll(e && e.status === 'fertig' && e.zaehlung.opus.abgegeben === false, JSON.stringify(e && [e.status, e.zaehlung])) soll(e && e.bericht_md.includes('| Opus | nein: ohne Ergebnis |'), 'Bericht ohne Zeile für Opus') return fehler }) fall('quorum', 'Paketbefehl für Code: basis, kontext, suite mit zehn Minuten', async () => { const { fehler, soll } = pruefe() const { protokoll: p } = await QR({ ...Q_ARGS, basis: 'v1.2.0', suite: 'pytest -q', kontext: ['D:/x/docs/plans/p/00-uebersicht.md'] }, {}) const pk = mitLabel(p, 'werkzeug:paket')[0] for (const z of [ `T=$(cygpath -m -l "$(mktemp -d)")`, `(cd 'D:/x' && pytest -q) > "$T/suite.txt" 2>&1; echo "EXIT suite: $?" >> "$T/suite.txt"`, `paket --anlass einspielen --ziel "$T" --projekt 'D:/x' --basis 'v1.2.0' --kontext 'D:/x/docs/plans/p/00-uebersicht.md' --suite-datei "$T/suite.txt"`, 'timeout auf 600000', ]) soll(pk && pk.prompt.includes(z), 'Paketbefehl ohne ' + z) return fehler }) fall('quorum', 'Kundendokument: Paketbefehl, Wurzel für Stimmen und Codex, Stand-Liste und Link auf die Übersicht', async () => { const { fehler, soll } = pruefe() const args = { anlass: 'kundendokument', ordner: 'D:/k', dateien: ['kapitel/01.md', 'kapitel/02 teil.md'], uebersicht: '00 Übersicht.md' } const stand = ['c'.repeat(64) + ' kapitel/01.md', 'd'.repeat(64) + ' kapitel/02 teil.md'] const paket = { anlass: 'kundendokument', art: 'doku', wurzel: 'D:/k', basis: 'erstes Quorum', stand, vorher: null, nummer: 1, bericht_pfad: 'D:/k/quorum/2026-09-30-kundendokument.md', dateien: [{ pfad: 'kapitel/01.md', art: 'neu' }, { pfad: 'kapitel/02 teil.md', art: 'neu' }] } const { ergebnis: e, fehler: f, protokoll: p } = await QR(args, { paket }) soll(f === null && e && e.status === 'fertig', 'Fehler/status: ' + f + ' ' + (e && e.status)) const pk = mitLabel(p, 'werkzeug:paket')[0] soll(pk && pk.prompt.includes(`paket --anlass kundendokument --ziel "$T" --ordner 'D:/k' --uebersicht '00 Übersicht.md' --dateien 'kapitel/01.md' 'kapitel/02 teil.md'`) && !pk.prompt.includes('--projekt'), 'Paketbefehl: ' + (pk && pk.prompt)) const opus = mitLabel(p, 'stimme:opus')[0] const start = mitLabel(p, 'codex:start')[0] soll(opus && opus.prompt.includes('Wurzel: D:/k') && start && start.prompt.includes(`--cwd 'D:/k'`), 'Wurzel nicht der Ordner') const md = (e && e.bericht_md) || '' for (const z of [`\nstand:\n - "${stand[0]}"\n - "${stand[1]}"\ncodex: `, '\nbasis: "erstes Quorum"\n', 'Übersicht: [00 Übersicht.md](<../00 Übersicht.md>)']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z)) soll(!md.includes('README'), 'Bericht eines Kundendokuments verlinkt ein README') return fehler }) for (const [name, a, text] of [ ['anlass fehlt', { projekt: 'D:/x' }, /anlass muss/], ['anlass unbekannt', { anlass: 'release', projekt: 'D:/x' }, /anlass muss/], ['projekt fehlt', { anlass: 'einspielen' }, /args fehlen: projekt/], ['projekt leer', { anlass: 'version', projekt: ' ' }, /projekt muss ein nicht leerer Text/], ['basis leer', { ...Q_ARGS, basis: '' }, /basis muss ein nicht leerer Text/], ['dateien bei Code', { ...Q_ARGS, dateien: ['a.md'] }, /dateien gilt nur für kundendokument/], ['dateien fehlen', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md' }, /dateien muss/], ['dateien leer', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md', dateien: [] }, /dateien muss/], ['Datei doppelt', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md', dateien: ['a.md', 'a.md'] }, /doppelt in dateien/], ['uebersicht fehlt', { anlass: 'kundendokument', ordner: 'D:/k', dateien: ['a.md'] }, /args fehlen: uebersicht/], ['projekt bei Kundendokument', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md', dateien: ['a.md'], projekt: 'D:/x' }, /projekt gilt nur für einspielen und version/], ['suite und suite_datei', { ...Q_ARGS, suite: 'pytest', suite_datei: 'D:/s.txt' }, /schließen sich aus/], ['kontext als Text', { ...Q_ARGS, kontext: 'D:/a.md' }, /kontext muss/], ['codex als Text', { ...Q_ARGS, codex: 'nein' }, /codex muss/], ['codex_runden 0', { ...Q_ARGS, codex_runden: 0 }, /codex_runden muss/], ['codex_runden 7', { ...Q_ARGS, codex_runden: 7 }, /codex_runden muss/], ['codex_runden 1.5', { ...Q_ARGS, codex_runden: 1.5 }, /codex_runden muss/], ]) { fall('quorum', `${name} wirft vor dem ersten Agenten`, async () => { const { fehler, soll } = pruefe() const { fehler: f, protokoll: p } = await QR(a, {}) soll(f && text.test(f), 'Fehler: ' + f) soll(p.agents.length === 0, 'Agent gestartet') return fehler }) } // ---------------------------------------------------------------- Lauf ``` ### Quorum-Mutanten in `.vault-tools/workflow-pruefung/mutanten_workflows.py` Aus: ``` ("plankritik", "Exit 2 wird wie 0 behandelt (N7)", "exitCode === 2 || exitCode === 3", "false"), ] ``` wird: ```python ("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", "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", "Gruppe ueber verschiedene Dateien bleibt", "if (new Set(mitglieder.map(b => pfadnorm(b.datei))).size > 1) {", "if (false) {"), ("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',"), ] ``` ## Schritt 2 — Rot-Nachweis Die Fälle gegen eine Kopie der Workflows ohne `quorum.js`: ``` T=$(mktemp -d) && cp D:/Claude/.claude/workflows/*.js "$T"/ && rm "$T/quorum.js" && node D:/Claude/.vault-tools/workflow-pruefung/faelle_workflows.mjs "$(cygpath -m "$T")" > "$T/aus.txt"; echo "EXIT $?"; grep -c '^FEHLER quorum:' "$T/aus.txt"; grep '^FEHLER' "$T/aus.txt" | grep -v '^FEHLER quorum:'; echo "ENDE" ``` Erwartet: `EXIT 1`, dann eine Zahl größer als null (die gescheiterten Quorum-Fälle), dann direkt `ENDE`: Keine andere `FEHLER`-Zeile als die des Quorums. ## Schritt 3 — Verifikation ``` node D:/Claude/.vault-tools/workflow-pruefung/faelle_workflows.mjs; echo "EXIT $?" ``` Erwartet: Jede Zeile `quorum:` beginnt mit `OK`, die letzte Zeile enthält `0 gescheitert`, `EXIT 0`. ``` python D:/Claude/.vault-tools/workflow-pruefung/mutanten_workflows.py; echo "EXIT $?" ``` Erwartet: Jede Zeile `quorum:` beginnt mit `GEFANGEN`, die letzte Zeile endet auf `0 ueberlebt, 0 veraltet`, `EXIT 0`. Der Lauf dauert einige Minuten. ``` python D:/Claude/.vault-tools/plan-kopie/plan_bloecke.py nach D:/Claude/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/task-03-faelle-und-mutanten.md --projekt D:/Claude ``` Erwartet: `0 Befund(e).` Melde alle Ausgaben vollständig. Läuft etwas anders als erwartet: nichts anpassen, melden, mit jeder `FEHLER`- oder `UEBERLEBT`-Zeile und ihrem Text.