Files
claude-regelwerk/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/task-03-faelle-und-mutanten.md
T
Pascal MichelandClaude Opus 5 eb48f8822d 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]>
2026-10-01 04:00:02 +02:00

58 KiB
Raw Blame History

type, title, status, summary, tags, created, updated, modell, effort, modell_begruendung
type title status summary tags created updated modell effort modell_begruendung
plan-task Task 03 — Grundlinie, Fälle und Mutanten für quorum.js draft 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.
plan-task
vault-tools
codeworkflow
2026-09-29 2026-09-30 sonnet low Abschrift: Beide Blöcke stehen wörtlich im Plan und werden per plan_materialisiere.py angewendet, Zieldateien und Verbote sind genannt; sonnet mit effort low statt haiku, weil ein Fall und ein Mutant einen Gedankenstrich (U+2013) tragen, den Haiku still ersetzt.

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 und die Nachbesserung Task 05 sind angewendet: .claude/workflows/quorum.js liegt im Baum und trägt Teilgruppen nach Datei und Zeilen, --summe im ablegen_befehl und GIT_OPTIONAL_LOCKS=0 im Arbeitsbaum-Vergleich. Deshalb nach Task 05. Schritt 2 braucht außerdem die alte quorum.js, die Task 05 in Schritt 2 unter /tmp/quorum-task05-alt (Git-Bash) gesichert hat.

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/.
  • Den Sicherungsordner /tmp/quorum-task05-alt nur lesen, nicht ändern und nicht löschen.
  • 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:

// ---------------------------------------------------------------- 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), anzahl_dateien: 1, 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: [], paket_summe: 'e'.repeat(64), ...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'
// status --json mit einem früheren Hintergrundjob derselben Sitzung: enrichJob gibt dessen request
// samt jobId mit aus (Plugin codex 1.0.6, scripts/lib/job-control.mjs 161-181).
const Q_START_ALT = '{\n  "running": [],\n  "latestFinished": {\n    "id": "task-alt",\n    "status": "completed",\n    "request": {\n      "jobId": "task-alt"\n    }\n  }\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:')) {
      const name = label.slice('stimme:'.length)
      return ok({ lesemarke: (k.lesemarken || {})[name] || Q_MARKE, befunde: stimmen[name] || [], nicht_pruefbar: (k.nichtPruefbar || {})[name] || [] }, 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 <ID>:" 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))
  soll(e && e.paket === 'C:/tmp/q' && e.ablegen_befehl === "PYTHONIOENCODING=utf-8 python -X utf8 'D:/Claude/.vault-tools/quorum/quorum_werkzeug.py' ablegen --paket 'C:/tmp/q' --bericht 'C:/tmp/q/bericht.md' --ziel 'D:/x/docs/quorum/2026-09-30-einspielen.md' --summe '" + 'e'.repeat(64) + "'", 'paket/ablegen_befehl ' + JSON.stringify(e && [e.paket, e.ablegen_befehl]))
  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: PAKET\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
})

fall('quorum', 'Bericht: Befundzeilen mit Stelle und Beleg, Nachprüfung mit Fundstelle, Unbelegt, Nicht prüfbar, Hinweise und Lesemarke', async () => {
  const { fehler, soll } = pruefe()
  const stimmen = { opus: [qB('src/a.py', '10', 'blockierend', 'if x > 0:'), qB('src/a.py', '30', 'wichtig', 'return y'), qB('src/d.py', '7', 'blockierend', '')], sonnet: [qB('src/a.py', '10', 'wichtig', 'if x > 0:  # S')], codex: [] }
  const k = { stimmen, paket: { hinweise: ['basis weicht vom Stand des letzten Quorums ab'] }, nichtPruefbar: { sonnet: ['tests/ nicht lesbar'] }, lesemarken: { opus: 'ffffffffffffffff', sonnet: 'Lesemarke: ' + Q_MARKE } }
  const { ergebnis: e } = await QR(Q_ARGS, k)
  const md = (e && e.bericht_md) || ''
  for (const z of [
    '### G1: blockierend, Mehrheit (Opus, Sonnet)\n\n`src/a.py`, Stelle 10\n',
    '- O1 (Opus, blockierend, Stelle 10): blockierend in src/a.py 10\n  Beleg: if x > 0:\n',
    '- S1 (Sonnet, wichtig, Stelle 10): wichtig in src/a.py 10\n  Beleg: if x > 0:  # S\n',
    '- O2 (Opus, wichtig, Stelle 30): wichtig in src/a.py 30\n  Beleg: return y\n\nNachprüfung: bestaetigt, src/a.py:1, zitiert. nachgestellt\n',
    '## Unbelegt\n\n- O3 (Opus, blockierend) `src/d.py`, Stelle 7: blockierend in src/d.py 7\n',
    '## Nicht prüfbar\n\n- Sonnet: tests/ nicht lesbar\n',
    '## Hinweise des Laufs\n\n- basis weicht vom Stand des letzten Quorums ab\n- Opus: Lesemarke fehlt oder weicht ab',
  ]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
  soll(e && !e.hinweise.some(h => h.startsWith('Sonnet:')), 'Sonnet zitiert die Lesemarke samt Zeilenanfang und gilt trotzdem als abweichend: ' + JSON.stringify(e && e.hinweise))
  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 aufgeteilt: 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 => /Gruppe O1, S1 aufgeteilt in O1 \| S1 \(/.test(h)), 'kein Hinweis auf die aufgeteilte Gruppe: ' + JSON.stringify(e && e.hinweise))
  return fehler
})

// Zwei Stellen mit Zeilennummern gehören zusammen, wenn sich ihre Zeilenbereiche mit einer
// Toleranz von drei Zeilen überschneiden; ohne Zeilennummer gilt das Urteil des Agenten.
for (const [name, stelleO, stelleS, mehrheit] of [
  ['Zeile 10 und Zeile 250', '10', '250', false],
  ['Zeile 42 und Zeile 43', 'Zeile 42', 'Zeile 43', true],
  ['Zeile 10 und Zeile 13, Toleranz drei', '10', '13', true],
  ['Zeile 10 und Zeile 14, jenseits der Toleranz', '10', '14', false],
  ['Zeilen 10-20 und Zeile 22', 'Zeilen 10-20', 'Zeile 22', true],
  ['Zeilen 10–20 und Z. 22', 'Zeilen 10–20', 'Z. 22', true],
  ['L12 und line 200', 'L12', 'line 200', false],
  ['a.py:42 und a.py:250', 'a.py:42', 'a.py:250', false],
  ['Zeile 42 (Funktion f) und Funktion g, Zeile 250', 'Zeile 42 (Funktion f)', 'Funktion g, Zeile 250', false],
  ['Zeile 10 und ein Abschnitt', '10', 'Abschnitt Einleitung', true],
  ['zwei Abschnitte ohne Zeilennummer', 'Abschnitt Einleitung', 'Abschnitt Einleitung, Satz 2', true],
]) {
  fall('quorum', `Gruppe in derselben Datei, ${name}: ${mehrheit ? 'eine Gruppe, Mehrheit' : 'aufgeteilt, beide nachgeprüft'}`, async () => {
    const { fehler, soll } = pruefe()
    const stimmen = { opus: [qB('src/a.py', stelleO, 'blockierend')], sonnet: [qB('src/a.py', stelleS, 'wichtig')], codex: [] }
    const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen, gruppen: () => ({ gruppen: [{ ids: ['O1', 'S1'], begruendung: 'zusammen' }] }), urteile: pr => qUrteile(pr, () => 'widerlegt') })
    soll(e && qGruppe(e, 'mehrheit').length === (mehrheit ? 1 : 0) && e.urteil === (mehrheit ? 'nicht_frei' : 'frei'), JSON.stringify(e && [e.urteil, e.gruppen.map(g => g.status)]))
    soll(JSON.stringify(qNachgeprueft(p)) === (mehrheit ? 'null' : '["O1","S1"]'), 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
    soll(e && e.hinweise.some(h => /Gruppe O1, S1 aufgeteilt in O1 \| S1 \(/.test(h)) === !mehrheit, 'Hinweise: ' + JSON.stringify(e && e.hinweise))
    const md = (e && e.bericht_md) || ''
    for (const z of [`(Opus, blockierend, Stelle ${stelleO}): `, `(Sonnet, wichtig, Stelle ${stelleS}): `]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
    return fehler
  })
}

// Drei Stimmen, eine Gruppe des Agenten: Sie zerfällt in zusammenhängende Teilgruppen, jede mit
// zwei Stimmen ist eine Mehrheit, der Rest geht in die Nachprüfung, jede Aufteilung ist ein Hinweis.
for (const [name, stellen, mehrheiten, nachgeprueft, hinweis] of [
  ['Zeilen 10-14, Zeile 12 und 11 bilden eine Mehrheit', [['src/a.py', 'Zeilen 10-14'], ['src/a.py', 'Zeile 12'], ['src/a.py', '11']], ['codex,opus,sonnet'], null, null],
  ['Zeile 42 und 43 sind eine Mehrheit, Zeile 250 steht für sich', [['src/a.py', 'Zeile 42'], ['src/a.py', 'Zeile 43'], ['src/a.py', 'Zeile 250']], ['opus,sonnet'], '["C1"]', /Gruppe O1, S1, C1 aufgeteilt in O1, S1 \| C1 \(/],
  ['Zeile 10, 13 und 16 hängen über die transitive Hülle zusammen', [['src/a.py', 'Zeile 10'], ['src/a.py', 'Zeile 13'], ['src/a.py', 'Zeile 16']], ['codex,opus,sonnet'], null, null],
  ['Stelle ohne Zeilennummer neben getrennten Zeilen steht für sich', [['src/a.py', 'Zeile 10'], ['src/a.py', 'Funktion f'], ['src/a.py', 'Zeile 250']], [], '["O1","S1","C1"]', /Gruppe O1, S1, C1 aufgeteilt in O1 \| S1 \| C1 \(/],
  ['zwei Stellen in a.py sind eine Mehrheit, b.py steht für sich', [['src/a.py', 'Zeile 10'], ['src/a.py', 'Zeile 11'], ['src/b.py', 'Zeile 10']], ['opus,sonnet'], '["C1"]', /Gruppe O1, S1, C1 aufgeteilt in O1, S1 \| C1 \(/],
]) {
  fall('quorum', `Teilgruppen einer Gruppe des Agenten: ${name}`, async () => {
    const { fehler, soll } = pruefe()
    const [opus, sonnet, codex] = stellen.map(([datei, stelle]) => [qB(datei, stelle, 'blockierend')])
    const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen: { opus, sonnet, codex }, gruppen: () => ({ gruppen: [{ ids: ['O1', 'S1', 'C1'], begruendung: 'zusammen' }] }), urteile: pr => qUrteile(pr, () => 'widerlegt') })
    const m = qGruppe(e, 'mehrheit').map(g => g.stimmen.slice().sort().join(','))
    soll(JSON.stringify(m) === JSON.stringify(mehrheiten), 'Mehrheiten: ' + JSON.stringify(m))
    soll(JSON.stringify(qNachgeprueft(p)) === String(nachgeprueft), 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
    const h = e ? e.hinweise.filter(x => /aufgeteilt in/.test(x)) : []
    soll(hinweis ? h.length === 1 && hinweis.test(h[0]) : h.length === 0, 'Hinweise: ' + JSON.stringify(h))
    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
})

fall('quorum', 'Unabhängigkeit: Stimmen kennen einander nicht, Zusammenführen liest keine Dateien', async () => {
  const { fehler, soll } = pruefe()
  const { protokoll: p } = await QR(Q_ARGS, { codexEnde: { job_status: 'failed' } })
  const stimmen = mitLabel(p, 'stimme:')
  soll(stimmen.length === 3, 'Stimmen: ' + JSON.stringify(stimmen.map(x => x.label)))
  for (const s of stimmen) soll(s.prompt.includes('Die anderen Stimmen kennst du nicht.') && !/Opus|Sonnet|Codex|Haiku/.test(s.prompt), s.label + ': ' + s.prompt)
  const zf = mitLabel(p, 'zusammenfuehren')[0]
  soll(zf && !zf.agentType && zf.prompt.includes('Lies keine Dateien und fahre keine Befehle; du arbeitest nur mit dieser Liste.'), 'Zusammenführen: ' + (zf && zf.prompt.slice(0, 300)))
  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', 'Codex nie fertig ohne codex_runden: Vorgabe drei Warterunden', async () => {
  const { fehler, soll } = pruefe()
  const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexWarten: () => Q_WARTEN_OFFEN })
  soll(mitLabel(p, 'codex:warten:').length === 3 && e && /nach 3 Warterunden/.test(e.stimmen.codex.grund), 'Warterunden: ' + mitLabel(p, 'codex:warten:').length + ', grund ' + JSON.stringify(e && e.stimmen.codex.grund))
  return fehler
})

fall('quorum', 'codex_runden 6 ist zugelassen: sechs Warterunden', async () => {
  const { fehler, soll } = pruefe()
  const { fehler: f, protokoll: p } = await QR({ ...Q_ARGS, codex_runden: 6 }, { codexWarten: () => Q_WARTEN_OFFEN })
  soll(f === null && mitLabel(p, 'codex:warten:').length === 6, 'Fehler ' + f + ', Warterunden: ' + mitLabel(p, 'codex:warten:').length)
  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', 'Codex-Start: jobId nur aus der Ausgabe von task, nicht aus einem früheren Job im Status', async () => {
  const { fehler, soll } = pruefe()
  const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexStart: Q_START_ALT })
  soll(e && e.stimmen.codex.job === 'task-q1' && e.stimmen.codex.abgegeben === true, 'Codex-Stimme ' + JSON.stringify(e && [e.stimmen.codex.job, e.stimmen.codex.grund]))
  const warten = mitLabel(p, 'codex:warten:1')[0]
  const ende = mitLabel(p, 'codex:ende')[0]
  soll(warten && warten.prompt.includes("status 'task-q1' --wait") && ende && ende.prompt.includes("result 'task-q1' --json"), 'Warten oder Ende nicht auf task-q1')
  soll(p.agents.every(x => !String(x.prompt).includes('task-alt')), 'ein Auftrag nennt den früheren Job')
  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 && e.ablegen_befehl === null, JSON.stringify(e && [e.status, e.urteil, e.bericht_pfad, e.ablegen_befehl]))
  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, anzahl_dateien: 0, 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]))
    soll(e && typeof e.ablegen_befehl === 'string' && e.ablegen_befehl.endsWith(" ablegen --paket 'C:/tmp/q' --bericht 'C:/tmp/q/bericht.md' --ziel 'D:/x/docs/quorum/2026-09-30-einspielen.md' --summe '" + 'e'.repeat(64) + "'"), 'ablegen_befehl ' + (e && e.ablegen_befehl))
    const md = (e && e.bericht_md) || ''
    for (const z of ['\ncodex: nicht gelaufen\n', `\nurteil: ${vorUrteil}\n`, '\nstand: PAKET\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/],
  ['paket_summe fehlt', { paket: { paket_summe: undefined } }, /QUORUM_PAKET ohne paket_summe$/],
  ['paket_summe kein Hex', { paket: { paket_summe: 'xyz' } }, /unbrauchbarer paket_summe/],
  ['anzahl_dateien negativ', { paket: { anzahl_dateien: -1 } }, /anzahl_dateien muss eine ganze Zahl ab 0/],
  ['anzahl_dateien als Text', { paket: { anzahl_dateien: '1' } }, /anzahl_dateien muss eine ganze Zahl ab 0/],
  ['hinweise keine Liste', { paket: { hinweise: 'x' } }, /hinweise muss eine Liste/],
  ['Exit 2 trotz Paketzeile', { paketAusgabe: 'QUORUM_PAKET ' + JSON.stringify(qPaket()) + '\nBEFUND: nachgestellt\nEXIT paket: 2' }, /Paket nicht gebaut: BEFUND: nachgestellt/],
]) {
  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, Arbeitsbaum vor und nach der Suite', 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)")`,
    `V=$(GIT_OPTIONAL_LOCKS=0 git -C 'D:/x' status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum'); (cd 'D:/x' && pytest -q) > "$T/suite.txt" 2>&1; echo "EXIT suite: $?" >> "$T/suite.txt"`,
    `[ "$V" = "$(GIT_OPTIONAL_LOCKS=0 git -C 'D:/x' status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum')" ] || echo "BEFUND: Die Suite hat den Arbeitsbaum verändert`,
    `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 PAKET und Wikilink auf die Übersicht', async () => {
  const { fehler, soll } = pruefe()
  const K = 'D:/Claude/IT Workspace/Beispiel/abgabe'
  const args = { anlass: 'kundendokument', ordner: K, dateien: ['kapitel/01.md', 'kapitel/02 teil.md'], uebersicht: '00 Übersicht.md' }
  const stand = 'sha256:' + 'c'.repeat(64)
  const paket = { anlass: 'kundendokument', art: 'doku', wurzel: K, basis: 'erstes Quorum', stand, vorher: null, nummer: 1, bericht_pfad: K + '/quorum/2026-09-30-kundendokument.md', anzahl_dateien: 2 }
  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 '${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: ' + K) && start && start.prompt.includes(`--cwd '${K}'`), 'Wurzel nicht der Ordner')
  const md = (e && e.bericht_md) || ''
  for (const z of ['\nstand: PAKET\ncodex: ', '\nbasis: "erstes Quorum"\n', `Basis erstes Quorum, Stand ${stand}; 2 Datei(en) im Unterschied.`, 'Übersicht: [[IT Workspace/Beispiel/abgabe/00 Übersicht|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, ordner, uebersicht, zeile] of [
  ['IT Workspace, Übersicht im Unterordner', 'D:/Claude/IT Workspace/Beispiel/abgabe', 'teil/00 Übersicht.md', 'Übersicht: [[IT Workspace/Beispiel/abgabe/teil/00 Übersicht|00 Übersicht.md]]'],
  ['Backslashes und Schrägstrich am Ende', 'D:\\Claude\\Security - Compliance Projekte\\active - Beispiel\\', '00-uebersicht.md', 'Übersicht: [[Security - Compliance Projekte/active - Beispiel/00-uebersicht|00-uebersicht.md]]'],
  ['Code-Projekt, zwei Ebenen unter der Wurzel', 'D:/Claude/Code Projekte/active - Beispiel/doku/kunde', '00-uebersicht.md', 'Projekt: [README](../../../README.md)'],
  ['Code-Projekt, Wurzel', 'D:/Claude/Code Projekte/active - Beispiel', '00-uebersicht.md', 'Projekt: [README](../README.md)'],
]) {
  fall('quorum', `Kundendokument, Hub-Zeile (${name})`, async () => {
    const { fehler, soll } = pruefe()
    const args = { anlass: 'kundendokument', ordner, dateien: ['kapitel/01.md'], uebersicht }
    const paket = { anlass: 'kundendokument', art: 'doku', wurzel: ordner, basis: 'erstes Quorum', stand: 'sha256:' + 'c'.repeat(64), vorher: null, nummer: 1 }
    const { ergebnis: e, fehler: f } = await QR(args, { paket })
    const md = (e && e.bericht_md) || ''
    soll(f === null && md.includes('\n\n' + zeile + '\n\n'), 'Fehler ' + f + ', Bericht ohne ' + JSON.stringify(zeile))
    soll(md.split('\n').filter(z => /^(Projekt|Übersicht): /.test(z)).length === 1, 'nicht genau eine Hub-Zeile')
    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/],
  ['ordner außerhalb des Vaults', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md', dateien: ['a.md'] }, /ordner muss im Vault/],
  ['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:

    ("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", "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", "start.slice(start.lastIndexOf('EXIT status:'))", "start"),
    ("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 ${b.stelle}): ${b.aussage}", ", ${b.schwere}): ${b.aussage}"),
    ("quorum", "Nachpruefungszeile fehlt", "if (g.nachpruefung) z.push(''", "if (false) z.push(''"),
    ("quorum", "Nachpruefung ohne Fundstelle", "${g.nachpruefung.urteil}, ${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 ${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."),
]

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.

Dann die Fälle gegen eine Kopie der Workflows, in der quorum.js die Fassung vor Task 05 ist (Sicherung aus Task 05, Schritt 2):

ALT="$(cygpath -m -l /tmp)/quorum-task05-alt" && T=$(mktemp -d) && cp D:/Claude/.claude/workflows/*.js "$T"/ && cp "$ALT/quorum.js" "$T/quorum.js" && node D:/Claude/.vault-tools/workflow-pruefung/faelle_workflows.mjs "$(cygpath -m "$T")" > "$T/aus.txt"; echo "EXIT $?"; grep '^FEHLER' "$T/aus.txt"; echo "ENDE"

Erwartet: EXIT 1, dann genau diese FEHLER-Zeilen in dieser Reihenfolge, dann ENDE. Jede steht für eine Entscheidung, die Task 05 geändert hat (Teilgruppen, --summe, paket_summe, GIT_OPTIONAL_LOCKS=0):

FEHLER quorum: Grundlinie: Paket, drei Stimmen gleichzeitig, Mehrheit sperrt, Einzelbefund nachgeprüft, Bericht
FEHLER quorum: Gruppe über verschiedene Dateien wird aufgeteilt: keine Mehrheit
FEHLER quorum: Gruppe in derselben Datei, Zeile 10 und Zeile 250: aufgeteilt, beide nachgeprüft
FEHLER quorum: Gruppe in derselben Datei, Zeile 42 und Zeile 43: eine Gruppe, Mehrheit
FEHLER quorum: Gruppe in derselben Datei, Zeile 10 und Zeile 13, Toleranz drei: eine Gruppe, Mehrheit
FEHLER quorum: Gruppe in derselben Datei, Zeile 10 und Zeile 14, jenseits der Toleranz: aufgeteilt, beide nachgeprüft
FEHLER quorum: Gruppe in derselben Datei, Zeilen 10-20 und Zeile 22: eine Gruppe, Mehrheit
FEHLER quorum: Gruppe in derselben Datei, Zeilen 10–20 und Z. 22: eine Gruppe, Mehrheit
FEHLER quorum: Gruppe in derselben Datei, L12 und line 200: aufgeteilt, beide nachgeprüft
FEHLER quorum: Gruppe in derselben Datei, a.py:42 und a.py:250: aufgeteilt, beide nachgeprüft
FEHLER quorum: Gruppe in derselben Datei, Zeile 42 (Funktion f) und Funktion g, Zeile 250: aufgeteilt, beide nachgeprüft
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: Zeilen 10-14, Zeile 12 und 11 bilden eine Mehrheit
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: Zeile 42 und 43 sind eine Mehrheit, Zeile 250 steht für sich
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: Zeile 10, 13 und 16 hängen über die transitive Hülle zusammen
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: Stelle ohne Zeilennummer neben getrennten Zeilen steht für sich
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: zwei Stellen in a.py sind eine Mehrheit, b.py steht für sich
FEHLER quorum: leerer Unterschied: nur der Paket-Agent, Urteil frei übernommen
FEHLER quorum: leerer Unterschied: nur der Paket-Agent, Urteil nicht_frei übernommen
FEHLER quorum: QUORUM_PAKET kaputt (paket_summe kein Hex): Fehler, keine Stimme
FEHLER quorum: Paketbefehl für Code: basis, kontext, suite mit zehn Minuten, Arbeitsbaum vor und nach der Suite

Die beiden Fälle „Zeile 10 und ein Abschnitt“ und „zwei Abschnitte ohne Zeilennummer“ sind auch gegen die alte Fassung grün; sie sichern, dass Stellen ohne Zeilennummer beim Urteil des Agenten bleiben. Scheitert cp "$ALT/quorum.js", fehlt die Sicherung aus Task 05: anhalten und melden.

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.