Files
claude-regelwerk/.vault-tools/workflow-pruefung/faelle_workflows.mjs
T
Pascal MichelandClaude Opus 5 22761eb186 Automatischer Push: 11 Datei(en) geändert
- .claude/agents/planer.md
- .claude/workflows/plankritik.js
- .claude/workflows/quorum.js
- .claude/workflows/welle-mit-pruefung.js
- .vault-tools/plan-kopie/LIESMICH.md
- .vault-tools/plan-kopie/plan_bloecke.py
- .vault-tools/plan-kopie/plan_materialisiere.py
- .vault-tools/plan-kopie/plan_zaun_treiber.py
- .vault-tools/workflow-pruefung/LIESMICH.md
- .vault-tools/workflow-pruefung/faelle_workflows.mjs
- .vault-tools/workflow-pruefung/mutanten_workflows.py

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

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-03 03:00:01 +01:00

2569 lines
163 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fälle für die Workflow-Skripte unter .claude/workflows/, gefahren mit attrappe.mjs.
// Aufruf: node faelle_workflows.mjs [<ordner mit den Workflows>]
// Ohne Argument gilt ../../.claude/workflows relativ zu dieser Datei. Je Workflow läuft zuerst
// die Grundlinie; scheitert sie, messen die übrigen Fälle dieses Workflows nichts.
// Exit 0, wenn alle Fälle bestehen, sonst 1.
import { mkdtempSync, writeFileSync, rmSync, cpSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { fuehreAus } from './attrappe.mjs'
const hier = path.dirname(fileURLToPath(import.meta.url))
const ORDNER = path.resolve(process.argv[2] || path.join(hier, '..', '..', '.claude', 'workflows'))
const WF = name => path.join(ORDNER, name + '.js')
const ok = (wert, dauer = 5) => ({ wert, dauer })
const fenster = (fuenf, woche, extra = []) => [
{ label: '5-hour limit', percentUsed: fuenf, resetsAt: '2026-09-24T18:30:00.000Z' },
{ label: 'Weekly · all models', percentUsed: woche, resetsAt: '2026-09-28T00:00:00.000Z' },
...extra,
]
const ablesung = (fuenf, woche, extra) => ({ verfuegbar: true, status: 'ok', plan: 'Pro', fenster: fenster(fuenf, woche, extra), fehler: '' })
const OHNE = 'ohne'
// Antworten für alle Agenten, die die Workflows starten. Unbekannte Labels werfen, damit ein
// falsches Label im Protokoll auffällt.
function antworten(k = {}) {
let n = 0
return async a => {
const label = a.label || ''
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:planwerkzeuge') {
if (k.planwerkzeuge === OHNE) return undefined
if (Array.isArray(k.planwerkzeuge)) return ok({ laeufe: k.planwerkzeuge })
return ok({ laeufe: [{ befehl: 'planwerkzeuge', exit: 0, ausgabe: k.planwerkzeuge !== undefined ? k.planwerkzeuge : 'keine Befunde' }] })
}
if (label === 'werkzeug:kopie') {
if (k.kopie === OHNE) return undefined
if (Array.isArray(k.kopie)) return ok({ laeufe: k.kopie })
return ok({ laeufe: [k.kopie || { befehl: 'kopie', exit: 0, ausgabe: "KOPIE=C:/tmp/k/mat\n{'neu': 2}" }] })
}
if (label === 'werkzeug:vergleich') {
if (Array.isArray(k.vergleich)) return ok({ laeufe: k.vergleich })
return ok({ laeufe: [{ befehl: 'vergleich', exit: 0, ausgabe: 'VERGLEICH-OK\nEXIT suite: 0' }] })
}
if (label === 'werkzeug:mutanten') {
if (k.mutanten === OHNE) return undefined
if (Array.isArray(k.mutanten)) return ok({ laeufe: k.mutanten })
const ausgabe = k.mutantenAusgabe !== undefined ? k.mutantenAusgabe : 'wurzel: /tmp/x\nM1 OK a=GRUEN(x)\nergebnis: 1 Mutanten, 1 OK, 0 LUECKE, 0 UNKLAR, 0 ZEIT, 0 MUSTER\nEXIT mutanten_lauf: 0'
return ok({ laeufe: [{ befehl: 'mutanten', exit: 0, ausgabe }] })
}
if (label.startsWith('plankritik:')) {
const linse = label.slice('plankritik:'.length)
if (k.linse) { const x = k.linse(linse); if (x === OHNE) return undefined; if (x !== undefined) return ok(x, k.dauer || 30) }
const basis = { arithmetik: 10, bezeichner: 20, abdeckung: 30 }[linse] || 40
return ok({ befunde: [0, 1].map(j => ({ schweregrad: 'wichtig', datei: 'task-01.md', zeile: basis + j, beschreibung: linse + ' ' + j, reproduktion: 'x' })), selbst_ausgefuehrt: [], nicht_pruefbar: [] }, k.dauer || 30)
}
if (label.startsWith('widerlegung:')) return ok({ urteil: 'bestaetigt', schweregrad: 'wichtig', bedingung: '', begruendung: 'nachgestellt', beleg: 'befehl' }, k.dauer || 30)
if (label.startsWith('pruefung')) {
if (k.pruefer === OHNE) return undefined
return ok({ selbst_ausgefuehrt: [], eigener_lauf: [], befunde: [{ schweregrad: 'klein', datei: 'a.py', zeile: 1, beschreibung: 'x', reproduktion: 'y' }], plan_vs_umsetzung: [], geprueft_und_in_ordnung: [], nicht_pruefbar: [] }, 20)
}
if (label.startsWith('executer:')) {
if (k.executer === OHNE) return undefined
return ok({ erledigt: ['alles'], geaenderte_dateien: [], verifikation: [], abweichungen: ['keine'], annahmen: [], beobachtet: [] }, 10)
}
if (label.startsWith('planer:task-')) {
const nr = Number(label.slice('planer:task-'.length))
const x = k.planer ? k.planer(nr) : undefined
if (x === OHNE) return undefined
const ziel = /Schreib genau eine Datei: (\S+\.md)/.exec(a.prompt)
const datei = k.planerDatei ? k.planerDatei(nr, ziel && ziel[1]) : ziel && ziel[1]
return ok({ datei, zieldateien: [], testdateien: [], abweichungen_von_uebersicht: [], offene_punkte: [] }, k.planerDauer ? k.planerDauer(nr) : 20 + nr * 3)
}
throw new Error('unerwarteter Agent ' + label)
}
}
function pruefe() {
const fehler = []
return { fehler, soll: (bedingung, text) => { if (!bedingung) fehler.push(text) } }
}
const mitLabel = (p, anfang) => p.agents.filter(x => (x.label || '').startsWith(anfang))
const spitze = (p, phase, modell) => p.spitzeJePhase[phase + '|' + modell] || 0
// Wie viele andere Agenten mit diesem Labelanfang liefen, als a startete.
const laufendBeiStart = (p, a, anfang) => mitLabel(p, anfang).filter(b => b !== a && b.start < a.start && (b.ende === null || b.ende > a.start)).length
function ohneVerstoss(p, soll) { soll(p.verstoesse.length === 0, 'Verstöße: ' + p.verstoesse.join('; ')) }
const faelle = []
const fall = (workflow, name, lauf) => faelle.push({ workflow, name, lauf })
// ---------------------------------------------------------------- attrappe
// Die Attrappe muss so streng sein wie das Ziel, sonst misst jeder grüne Fall nur sie selbst.
const TMP = mkdtempSync(path.join(os.tmpdir(), 'attrappe-'))
function skript(name, rumpf) {
const datei = path.join(TMP, name + '.js')
writeFileSync(datei, `export const meta = { name: '${name}', description: 'Selbsttest' }\n${rumpf}\n`, 'utf8')
return datei
}
const A = (datei, k) => fuehreAus(datei, {}, antworten(k), { ordner: TMP })
for (const [i, [name, rumpf, muster]] of [
['Date.now() wirft', 'return Date.now()', /Date\.now\(\)/],
['new Date() ohne Argument wirft', 'return new Date()', /new Date\(\)/],
['Math.random() wirft', 'return Math.random()', /Math\.random\(\)/],
['unbekannter Workflow wirft', "return await workflow('gibt-es-nicht', {})", /nicht lesbar/],
['Schema ohne properties wirft', "return await agent('x', { label: 'verbrauch:t', model: 'haiku', schema: { type: 'object' } })", /Schema braucht/],
].entries()) {
fall('attrappe', name, async () => {
const { fehler, soll } = pruefe()
const { fehler: f } = await A(skript('wirft-' + i, rumpf))
soll(f && muster.test(f), 'Fehler: ' + f)
return fehler
})
}
fall('attrappe', 'Grundlinie: Math.min und new Date(0) gehen, Globals sind da', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f } = await A(skript('grund', "phase('P'); log('x'); return [Math.min(1, 2), new Date(0).getTime(), typeof parallel, typeof pipeline, args !== undefined]"))
soll(f === null && JSON.stringify(e) === '[1,0,"function","function",true]', 'Fehler/Ergebnis: ' + f + ' ' + JSON.stringify(e))
return fehler
})
fall('attrappe', 'workflow() im Kind wirft', async () => {
const { fehler, soll } = pruefe()
const enkel = skript('enkel', 'return 1')
const kind = skript('kind', `return await workflow({ scriptPath: ${JSON.stringify(enkel)} }, {})`)
const { fehler: f } = await A(skript('eltern', `return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
soll(f && /nesting is limited to one level/.test(f), 'Fehler: ' + f)
return fehler
})
fall('attrappe', 'im Kind tragen Agenten die Phase ▸ <name>, phase() wirkt dort nicht', async () => {
const { fehler, soll } = pruefe()
const kind = skript('kindphase', "phase('Eigen'); return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', phase: 'Eigen' })")
const { protokoll: p } = await A(skript('elternphase', `phase('Oben'); return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
soll(p.agents.length === 1 && p.agents[0].phase === '▸ kindphase', 'Phase: ' + (p.agents[0] && p.agents[0].phase))
soll(JSON.stringify(p.phasen) === '["Oben"]', 'phase() im Kind hat gewirkt: ' + JSON.stringify(p.phasen))
return fehler
})
fall('attrappe', 'dasselbe Kind zweimal: ▸ name und ▸ name #2', async () => {
const { fehler, soll } = pruefe()
const kind = skript('kindzwei', "return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet' })")
const { protokoll: p } = await A(skript('elternzwei', `await workflow({ scriptPath: ${JSON.stringify(kind)} }, {}); return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
soll(JSON.stringify(p.agents.map(a => a.phase)) === '["▸ kindzwei","▸ kindzwei #2"]', 'Phasen: ' + JSON.stringify(p.agents.map(a => a.phase)))
return fehler
})
fall('attrappe', 'agent() ohne model ist ein Verstoß', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await A(skript('ohne-modell', "return await agent('x', { label: 'verbrauch:t' })"))
soll(p.verstoesse.some(v => /ohne model/.test(v)), 'Verstöße: ' + JSON.stringify(p.verstoesse))
return fehler
})
fall('attrappe', 'unbekannter agentType wirft vor dem Start', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await A(skript('typ-unbekannt', "return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', agentType: 'gibt-es-nicht' })"))
soll(f && /agent type 'gibt-es-nicht' not found/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet trotz unbekanntem Typ: ' + p.agents.length)
return fehler
})
fall('attrappe', 'bekannter agentType aus optionen.agenten läuft', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await fuehreAus(skript('typ-bekannt', "return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', agentType: 'eigen' })"), {}, antworten({}), { ordner: TMP, agenten: ['eigen'] })
soll(f === null, 'Fehler: ' + f)
soll(p.agents.length === 1 && p.agents[0].agentType === 'eigen', 'Agenten: ' + JSON.stringify(p.agents.map(x => x.agentType)))
return fehler
})
fall('attrappe', 'Ergebnis ohne required-Feld wird null und ist ein Verstoß', async () => {
const { fehler, soll } = pruefe()
const schema = "{ type: 'object', properties: { a: { type: 'string' }, b: { type: 'string' } }, required: ['a', 'b'] }"
const { ergebnis: e, protokoll: p } = await A(skript('required', `return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', schema: ${schema} })`))
soll(e === null && p.verstoesse.some(v => /Ergebnis ohne/.test(v)), 'Ergebnis/Verstöße: ' + JSON.stringify([e, p.verstoesse]))
return fehler
})
fall('attrappe', 'Spitze zählt gleichzeitige Agenten je Phase und Modell', async () => {
const { fehler, soll } = pruefe()
const rumpf = "phase('P'); await parallel([1, 2, 3].map(() => () => agent('x', { label: 'widerlegung:t', model: 'opus', phase: 'P' }))); await agent('x', { label: 'widerlegung:t', model: 'opus', phase: 'P' }); return 1"
const { protokoll: p } = await A(skript('spitze', rumpf))
soll(spitze(p, 'P', 'opus') === 3, 'Spitze ' + spitze(p, 'P', 'opus'))
return fehler
})
// ---------------------------------------------------------------- verbrauch
const V =(a, k) => fuehreAus(WF('verbrauch'), a, antworten(k), { ordner: ORDNER })
fall('verbrauch', 'Grundlinie: Agent liest 10/10, wunsch 3 ergibt 3 Plätze', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await V({ anlass: 'test', wunsch: 3 }, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'ok' && e.plaetze === 3 && e.gebremst === false && e.quelle === 'agent', 'Ergebnis: ' + JSON.stringify(e))
soll(p.agents.length === 1 && p.agents[0].label === 'verbrauch:test' && p.agents[0].modell === 'haiku', 'Agent: ' + JSON.stringify(p.agents.map(x => [x.label, x.modell])))
soll(p.agents[0].agentType === 'verbrauch-ableser' && p.agents[0].effort === undefined, 'Typ/Effort: ' + JSON.stringify([p.agents[0].agentType, p.agents[0].effort]))
ohneVerstoss(p, soll)
return fehler
})
fall('verbrauch', 'probe ersetzt den Agenten', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await V({ anlass: 't', wunsch: 2, probe: ablesung(10, 10) })
soll(p.agents.length === 0, 'Agent gestartet trotz probe')
soll(e && e.quelle === 'probe' && e.plaetze === 2, JSON.stringify(e))
return fehler
})
for (const [fuenf, woche, wunsch, plaetze] of [
[49.9, 10, 4, 4], [50, 10, 4, 2], [69.9, 10, 4, 2], [70, 10, 4, 1], [84.9, 10, 4, 1], [85, 10, 4, 0], [100, 10, 4, 0], [0, 0, 4, 4],
[10, 69.9, 4, 4], [10, 70, 4, 2], [10, 84.9, 4, 2], [10, 85, 4, 1], [10, 94.9, 4, 1], [10, 95, 4, 0], [10, 100, 4, 0],
[101, 10, 4, 0], [10, 150, 4, 0],
]) {
fall('verbrauch', `Stufe 5 h ${fuenf} %, Woche ${woche} % ergibt ${plaetze}`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch, probe: ablesung(fuenf, woche) })
soll(e && e.plaetze === plaetze, `plaetze ${e && e.plaetze}, erwartet ${plaetze}`)
soll(e && e.gebremst === (plaetze === 0), 'gebremst ' + (e && e.gebremst))
return fehler
})
}
fall('verbrauch', 'wunsch 0 bei 95 % misst nur und bremst nicht', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 0, probe: ablesung(95, 10) })
soll(e && e.plaetze === 0 && e.gebremst === false && e.status === 'ok', JSON.stringify(e))
return fehler
})
fall('verbrauch', 'wunsch 7 wird auf die harte Obergrenze 4 gesenkt', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 7, probe: ablesung(10, 10) })
soll(e && e.plaetze === 4 && /harte Obergrenze/.test(e.grund), JSON.stringify(e))
return fehler
})
for (const wunsch of [-1, 1.5, '3', null, undefined]) {
fall('verbrauch', `wunsch ${JSON.stringify(wunsch)} wirft vor dem Agenten`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await V({ anlass: 't', wunsch })
soll(f && /wunsch muss/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
for (const anlass of [undefined, '', ' ', 3]) {
fall('verbrauch', `anlass ${JSON.stringify(anlass)} wirft`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f } = await V({ anlass, wunsch: 1 })
soll(f && /anlass fehlt/.test(f), 'Fehler: ' + f)
return fehler
})
}
for (const probe of [null, [], 'x']) {
fall('verbrauch', `probe ${JSON.stringify(probe)} wirft`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f } = await V({ anlass: 't', wunsch: 1, probe })
soll(f && /probe muss/.test(f), 'Fehler: ' + f)
return fehler
})
}
fall('verbrauch', 'Agent ohne Ergebnis: nicht_gelesen, zwei Plätze, bei wunsch 1 einer', async () => {
const { fehler, soll } = pruefe()
const a = await V({ anlass: 't', wunsch: 3 }, { ablesungen: [OHNE] })
soll(a.ergebnis && a.ergebnis.status === 'nicht_gelesen' && a.ergebnis.plaetze === 2 && a.ergebnis.gebremst === false, JSON.stringify(a.ergebnis))
const b = await V({ anlass: 't', wunsch: 1 }, { ablesungen: [OHNE] })
soll(b.ergebnis && b.ergebnis.plaetze === 1, JSON.stringify(b.ergebnis))
return fehler
})
// Form der echten Abweichungen vom 2026-10-14 und 2026-10-16, Texte gekürzt und neutral.
const abweichung = (status, fehler) => ({ verfuegbar: false, status, plan: '**Stand:** Bericht zur Nutzeranfrage statt Ablesung.\n- Punkt 1\n- Punkt 2', fenster: [], fehler })
const BEANTWORTET = abweichung('beantwortet: offene Punkte im Projekt X, Stand 2026-10-10', 'Kein get_usage-Aufruf und kein Planverbrauch abgelesen: Die Benutzeranfrage hat laut Harness Vorrang vor dem Verbrauchsauftrag.')
const BLOCKIERT = abweichung('blockiert: Folgesitzung nicht abgeschlossen; nichts geaendert', 'Blockade: Workflow- und Agent-Werkzeug fehlen. Kein get_usage-Aufruf, siehe plan.')
const warnung = p => p.logs.filter(l => /^WARNUNG/.test(l))
fall('verbrauch', 'Ableser beantwortet die Nutzeranfrage: abgewichen, Warnung, zwei Plätze', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await V({ anlass: 't', wunsch: 3 }, { ablesungen: [BEANTWORTET] })
soll(e && e.status === 'abgewichen' && e.plaetze === 2 && e.gebremst === false && e.quelle === 'agent', JSON.stringify(e))
soll(e && /Auftrag verlassen/.test(e.grund) && e.grund.includes('beantwortet:'), 'grund: ' + (e && e.grund))
soll(warnung(p).length === 1, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
return fehler
})
fall('verbrauch', 'Ableser meldet blockiert: abgewichen, Warnung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await V({ anlass: 't', wunsch: 1 }, { ablesungen: [BLOCKIERT] })
soll(e && e.status === 'abgewichen' && e.plaetze === 1 && e.grund.includes('blockiert:'), JSON.stringify(e))
soll(warnung(p).length === 1, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
return fehler
})
fall('verbrauch', 'status fehler mit Bericht in plan: abgewichen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await V({ anlass: 't', wunsch: 3 }, { ablesungen: [{ verfuegbar: false, status: 'fehler', plan: 'Bericht', fenster: [], fehler: 'x' }] })
soll(e && e.status === 'abgewichen', JSON.stringify(e))
soll(warnung(p).length === 1, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
return fehler
})
fall('verbrauch', 'ehrlicher Fehlschlag bleibt nicht_gelesen, ohne Warnung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await V({ anlass: 't', wunsch: 3 }, { ablesungen: [{ verfuegbar: false, status: 'fehler', plan: '', fenster: [], fehler: 'No matching deferred tools found' }] })
soll(e && e.status === 'nicht_gelesen' && e.plaetze === 2, JSON.stringify(e))
soll(warnung(p).length === 0, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
return fehler
})
fall('verbrauch', 'not_applicable bleibt nicht_gelesen, ohne Warnung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await V({ anlass: 't', wunsch: 3 }, { ablesungen: [{ verfuegbar: true, status: 'not_applicable', plan: '', fenster: [], fehler: '' }] })
soll(e && e.status === 'nicht_gelesen', JSON.stringify(e))
soll(warnung(p).length === 0, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
return fehler
})
fall('verbrauch', 'grund kürzt einen langen status auf 160 Zeichen, ohne Zeilenumbruch', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 3 }, { ablesungen: [abweichung('beantwortet:\n' + 'x'.repeat(400), '')] })
soll(e && e.status === 'abgewichen', JSON.stringify(e))
const teil = e && e.grund.split('status ')[1]
soll(typeof teil === 'string' && teil.length > 0 && teil.length <= 160 && !teil.includes('\n'), 'Teil hinter status: ' + JSON.stringify(teil) + ' Länge ' + (teil && teil.length))
return fehler
})
// Misst nur, dass der Satz im Auftrag steht, nicht, ob er den Ableser vom Abweichen abhält.
fall('verbrauch', 'Auftrag ordnet die Ablesung der Nutzeranfrage zu', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await V({ anlass: 't', wunsch: 3 }, {})
const prompt = p.agents[0] && p.agents[0].prompt
soll(typeof prompt === 'string' && prompt.includes('gehört zur Nutzeranfrage') && prompt.includes('bearbeiten andere Agenten'), 'prompt: ' + JSON.stringify(prompt))
return fehler
})
fall('verbrauch', 'Ableser-Typ unbekannt: nicht_gelesen mit Warnung, kein Absturz', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await fuehreAus(WF('verbrauch'), { anlass: 't', wunsch: 3 }, antworten({}), { ordner: ORDNER, agenten: ['planer'] })
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'nicht_gelesen' && e.plaetze === 2 && /nicht startbar/.test(e.grund) && /not found/.test(e.grund), JSON.stringify(e))
soll(warnung(p).length === 1, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
soll(p.agents.length === 0, 'Agenten gestartet: ' + p.agents.length)
return fehler
})
for (const [name, probe] of [
['status unavailable', { verfuegbar: true, status: 'unavailable', plan: '', fenster: [], fehler: '' }],
['verfuegbar false', { verfuegbar: false, status: 'fehler', plan: '', fenster: [], fehler: 'Werkzeug fehlt' }],
['verfuegbar als Text', { verfuegbar: 'true', status: 'ok', plan: 'Pro', fenster: fenster(10, 10), fehler: '' }],
]) {
fall('verbrauch', `${name}: nicht_gelesen`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 3, probe })
soll(e && e.status === 'nicht_gelesen' && e.plaetze === 2, JSON.stringify(e))
return fehler
})
}
const ok5 = { label: '5-hour limit', percentUsed: 10, resetsAt: 'x' }
const okW = { label: 'Weekly · all models', percentUsed: 10, resetsAt: 'y' }
for (const [name, liste] of [
['fenster leer', []],
['fenster kein Array', { a: 1 }],
['nur Woche', [okW]],
['nur 5 Stunden', [ok5]],
['Eintrag null an erster Stelle', [null, ok5, okW]],
['Eintrag null an letzter Stelle', [ok5, okW, null]],
['label fehlt', [ok5, { percentUsed: 1, resetsAt: 'z' }, okW]],
['percentUsed null vorn', [{ ...ok5, percentUsed: null }, okW]],
['percentUsed null hinten', [ok5, { ...okW, percentUsed: null }]],
['percentUsed als Text', [{ ...ok5, percentUsed: '12' }, okW]],
['percentUsed NaN', [{ ...ok5, percentUsed: NaN }, okW]],
['percentUsed -1', [ok5, { ...okW, percentUsed: -1 }]],
['percentUsed Infinity', [{ ...ok5, percentUsed: Infinity }, okW]],
['Opus-Woche ohne Zahl', [ok5, okW, { label: 'Weekly · Opus', percentUsed: null, resetsAt: 'z' }]],
]) {
fall('verbrauch', `${name}: unbekannte_fenster, alte Obergrenze`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: liste, fehler: '' } })
soll(e && e.status === 'unbekannte_fenster' && e.plaetze === 2 && e.gebremst === false, JSON.stringify(e))
return fehler
})
}
for (const [name, liste] of [
['doppeltes 5-Stunden-Fenster, hohes zuerst', [{ ...ok5, percentUsed: 90 }, ok5, okW]],
['doppeltes 5-Stunden-Fenster, hohes zuletzt', [ok5, okW, { ...ok5, percentUsed: 90 }]],
]) {
fall('verbrauch', `${name}: das höhere zählt`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: liste, fehler: '' } })
soll(e && e.plaetze === 0 && e.gebremst === true && e.fuenf_stunden.prozent === 90, JSON.stringify(e))
return fehler
})
}
fall('verbrauch', 'Opus-Woche 90 % zählt wie die Woche', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Opus', percentUsed: 90, resetsAt: 'z' }]) })
soll(e && e.plaetze === 1 && e.woche_opus && e.woche_opus.prozent === 90, JSON.stringify(e))
return fehler
})
fall('verbrauch', 'ein Label mit "hour" in der Mitte ist kein 5-Stunden-Fenster', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Opus hours', percentUsed: 90, resetsAt: 'z' }]) })
soll(e && e.plaetze === 1 && e.fuenf_stunden.prozent === 10, JSON.stringify(e))
return fehler
})
fall('verbrauch', 'Woche aller Modelle 90 % zählt auch neben einer niedrigen Opus-Woche', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 90, [{ label: 'Weekly · Opus', percentUsed: 10, resetsAt: 'z' }]) })
soll(e && e.plaetze === 1, JSON.stringify(e))
return fehler
})
for (const [name, fremd] of [['Fable 99 %', 99], ['Fable ohne Zahl', null], ['Fable als Text', 'viel']]) {
fall('verbrauch', `fremdes Fenster (${name}) wird übergangen`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Fable', percentUsed: fremd, resetsAt: 'z' }]) })
soll(e && e.status === 'ok' && e.plaetze === 4, JSON.stringify(e))
return fehler
})
}
for (const [a, b] of [['5-Hour Limit', 'Weekly · All Models'], ['5 hour limit', 'weekly all models'], ['5hour', 'WEEKLY - ALL MODELS']]) {
fall('verbrauch', `Schreibweise "${a}" / "${b}" wird erkannt`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: [{ label: a, percentUsed: 60, resetsAt: 'x' }, { label: b, percentUsed: 10, resetsAt: 'y' }], fehler: '' } })
soll(e && e.status === 'ok' && e.plaetze === 2, JSON.stringify(e))
return fehler
})
}
// ---------------------------------------------------------------- plankritik
const PK_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', kopie: 'C:/tmp/k/mat' }
const PK = (a, k, o = {}) => fuehreAus(WF('plankritik'), a, antworten(k), { ordner: ORDNER, ...o })
fall('plankritik', 'Grundlinie: drei Linsen gleichzeitig, Widerlegung zu zweit, drei Ablesungen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
soll(spitze(p, 'Plankritik', 'opus') === 3, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
soll(spitze(p, 'Widerlegung', 'opus') === 2, 'Widerlegung gleichzeitig: ' + spitze(p, 'Widerlegung', 'opus'))
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(JSON.stringify(ab) === JSON.stringify(['verbrauch:plankritik:vor Linsen', 'verbrauch:plankritik:vor Widerlegung', 'verbrauch:plankritik:Ende']), 'Ablesungen: ' + JSON.stringify(ab))
soll(mitLabel(p, 'verbrauch:').every(x => x.modell === 'haiku'), 'Ablesung nicht auf haiku')
soll(e && e.verbrauch.length === 3 && e.plaetze.linsen === 3 && e.plaetze.widerlegung === 2, 'verbrauch/plaetze: ' + JSON.stringify(e && e.plaetze))
soll(e && e.zaehlung.bestaetigt === 6 && e.zaehlung.nicht_geprueft === 0, 'zaehlung ' + JSON.stringify(e && e.zaehlung))
for (const k of ['zaehlung', 'kopie', 'temp_ordner', 'befunde', 'selbst_ausgefuehrt', 'nicht_pruefbar', 'vorbereitung', 'ohne_ergebnis']) soll(e && k in e, 'Feld fehlt: ' + k)
ohneVerstoss(p, soll)
return fehler
})
fall('plankritik', '5 Stunden bei 60 %: Linsen zu zweit', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(60, 10)] })
soll(spitze(p, 'Plankritik', 'opus') === 2 && e.plaetze.linsen === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
soll(e.status === 'fertig', 'status ' + e.status)
return fehler
})
fall('plankritik', 'gebremst vor den Linsen: keine Linse, keine Widerlegung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(90, 10)] })
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'gebremst' && e.gebremst_vor === 'Linsen', JSON.stringify(e && [e.status, e.gebremst_vor]))
soll(mitLabel(p, 'plankritik:').length === 0 && mitLabel(p, 'widerlegung:').length === 0, 'Opus gestartet trotz Bremse')
soll(e && e.verbrauch.length === 1 && e.ohne_ergebnis.length === 3, 'verbrauch/ohne_ergebnis ' + JSON.stringify(e && [e.verbrauch.length, e.ohne_ergebnis]))
soll(e && e.zaehlung && e.zaehlung.bestaetigt === 0 && e.zaehlung.nicht_geprueft === 0, 'zaehlung ' + JSON.stringify(e && e.zaehlung))
soll(e && /neu starten/.test(e.naechster_schritt), 'naechster_schritt ' + (e && e.naechster_schritt))
return fehler
})
fall('plankritik', 'gebremst vor der Widerlegung: nur die Befunde über der Schwelle heißen gebremst', async () => {
const { fehler, soll } = pruefe()
const zeile = { arithmetik: 11, bezeichner: 12, abdeckung: 13 }
const mitKlein = linse => ({ befunde: [
{ schweregrad: 'wichtig', datei: 'task-01.md', zeile: zeile[linse], beschreibung: 'w', reproduktion: 'x' },
{ schweregrad: 'klein', datei: 'task-02.md', zeile: zeile[linse], beschreibung: 'k', reproduktion: 'x' },
], selbst_ausgefuehrt: [], nicht_pruefbar: [] })
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(10, 10), ablesung(90, 10)], linse: mitKlein })
soll(mitLabel(p, 'plankritik:').length === 3, 'Linsen: ' + mitLabel(p, 'plankritik:').length)
soll(mitLabel(p, 'widerlegung:').length === 0, 'Widerlegung trotz Bremse')
soll(e.status === 'gebremst' && e.gebremst_vor === 'Widerlegung', JSON.stringify([e.status, e.gebremst_vor]))
const urteile = e.befunde.map(b => b.schweregrad + ':' + b.urteil).sort()
soll(urteile.filter(u => u === 'wichtig:nicht geprüft (gebremst)').length === 3 && urteile.filter(u => u === 'klein:nicht geprüft').length === 3, 'Urteile: ' + JSON.stringify(urteile))
soll(e.zaehlung.nicht_geprueft === 6 && e.zaehlung.bestaetigt === 0, JSON.stringify(e.zaehlung))
soll(mitLabel(p, 'verbrauch:').length === 3, 'Ablesung am Ende fehlt')
soll(/nachholen/.test(e.naechster_schritt), 'naechster_schritt ' + e.naechster_schritt)
return fehler
})
fall('plankritik', 'vor der Widerlegung 75 %: Widerlegung einzeln', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(10, 10), ablesung(75, 10)] })
soll(spitze(p, 'Widerlegung', 'opus') === 1 && e.plaetze.widerlegung === 1, 'Widerlegung gleichzeitig: ' + spitze(p, 'Widerlegung', 'opus'))
soll(mitLabel(p, 'widerlegung:').length === 6 && e.status === 'fertig', JSON.stringify([mitLabel(p, 'widerlegung:').length, e.status]))
return fehler
})
fall('plankritik', 'Ablesung ohne Ergebnis: Linsen zu zweit (alte Obergrenze)', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [OHNE] })
soll(spitze(p, 'Plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
soll(e.status === 'fertig' && e.verbrauch[0].status === 'nicht_gelesen', JSON.stringify(e.verbrauch[0]))
return fehler
})
fall('plankritik', 'Ableser-Typ unbekannt: Linsen zu zweit, Plankritik läuft', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, {}, { agenten: ['pruefer'] })
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
soll(spitze(p, 'Plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
soll(e && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch.map(v => v.status)))
soll(warnung(p).length === 3, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
return fehler
})
fall('plankritik', 'Abweichung bei der ersten Ablesung: Linsen zu zweit, Status abgewichen im Ergebnis', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [BEANTWORTET] })
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
soll(spitze(p, 'Plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
soll(e && e.verbrauch[0].status === 'abgewichen', JSON.stringify(e && e.verbrauch[0]))
soll(warnung(p).length === 1, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
return fehler
})
fall('plankritik', 'Abweichung bei der letzten Ablesung: Status abgewichen im Ergebnis', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [undefined, undefined, BLOCKIERT] })
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
soll(e && e.verbrauch[2].status === 'abgewichen' && e.verbrauch[0].status === 'ok', JSON.stringify(e && e.verbrauch.map(v => v.status)))
soll(warnung(p).length === 1, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
return fehler
})
fall('plankritik', 'als Kind: verbrauch.js wirft, Rückfall auf zwei', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, {}, { tiefe: 1 })
soll(f === null, 'Fehler: ' + f)
soll(spitze(p, '▸ plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, '▸ plankritik', 'opus'))
soll(mitLabel(p, 'verbrauch:').length === 0, 'Ableseagent trotz Kind')
soll(e && e.verbrauch.every(v => v.status === 'nicht_gelesen' && /nicht ausführbar/.test(v.grund)), JSON.stringify(e && e.verbrauch))
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
return fehler
})
fall('plankritik', 'Aufträge nennen den Testort', async () => {
const { fehler, soll } = pruefe()
const mit = await PK(PK_ARGS, {})
const satz = 'Tests fährst du nur in der Kopie C:/tmp/k/mat, nie im echten Projektbaum'
for (const a of [...mitLabel(mit.protokoll, 'plankritik:'), ...mitLabel(mit.protokoll, 'widerlegung:')]) {
soll(a.prompt.includes(satz) && /eigenen Kopie dieser Kopie/.test(a.prompt), a.label + ' ohne Testort')
}
const ohne = await PK({ projekt: 'D:/x', planordner: 'D:/x/plan' }, {})
for (const a of mitLabel(ohne.protokoll, 'plankritik:')) {
soll(/Tests fährst du in dieser Plankritik nicht/.test(a.prompt), a.label + ': ohne Kopie kein Testverbot')
}
return fehler
})
fall('plankritik', 'plaetze 1 begrenzt die Linsen auf eine', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, plaetze: 1 }, {})
soll(spitze(p, 'Plankritik', 'opus') === 1, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
return fehler
})
for (const plaetze of [0, 5, 2.5, '3']) {
fall('plankritik', `plaetze ${JSON.stringify(plaetze)} wirft vor dem ersten Agenten`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, plaetze }, {})
soll(f && /plaetze muss/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
fall('plankritik', 'doppelte Linse wirft vor dem ersten Agenten', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, linsen: ['arithmetik', 'bezeichner', 'arithmetik'] }, {})
soll(f && /doppelt in linsen/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
fall('plankritik', 'Linse ohne Ergebnis steht in ohne_ergebnis', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PK(PK_ARGS, { linse: l => (l === 'arithmetik' ? OHNE : undefined) })
soll(e.status === 'fertig' && JSON.stringify(e.ohne_ergebnis) === '["arithmetik"]', JSON.stringify(e.ohne_ergebnis))
return fehler
})
fall('plankritik', 'verifizieren false: keine Ablesung vor der Widerlegung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK({ ...PK_ARGS, verifizieren: false }, {})
soll(mitLabel(p, 'widerlegung:').length === 0, 'Widerlegung trotz verifizieren false')
soll(mitLabel(p, 'verbrauch:').length === 2, 'Ablesungen: ' + mitLabel(p, 'verbrauch:').length)
soll(e.status === 'fertig' && e.zaehlung.nicht_geprueft === 6, JSON.stringify(e.zaehlung))
return fehler
})
fall('plankritik', 'mit kopie, mutanten in Vorgabe: ein Agent werkzeug:mutanten, Rohausgabe geht an die Linse arithmetik', async () => {
const { fehler, soll } = pruefe()
const marke = 'MUTANT-MARKE-007'
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: `wurzel: /tmp/x\n${marke}\nEXIT mutanten_lauf: 0` })
const agenten = mitLabel(p, 'werkzeug:mutanten')
soll(agenten.length === 1, 'Agenten werkzeug:mutanten: ' + agenten.length)
soll(agenten[0] && /MSYS_NO_PATHCONV=1/.test(agenten[0].prompt), 'MSYS_NO_PATHCONV fehlt im Befehl: ' + (agenten[0] && agenten[0].prompt))
soll(agenten[0] && /cygpath -m -l/.test(agenten[0].prompt), 'cygpath -m -l fehlt im Befehl: ' + (agenten[0] && agenten[0].prompt))
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && arith.prompt.includes(marke) && /<mutantenlauf>/.test(arith.prompt), 'Rohausgabe fehlt im Auftrag der Linse arithmetik')
return fehler
})
fall('plankritik', 'Modell je Hilfslabel: kopie haiku/low, planwerkzeuge und mutanten sonnet/low', async () => {
const { fehler, soll } = pruefe()
const args = { projekt: 'D:/x', planordner: 'D:/x/plan', materialisieren: { ohne: ['config/secrets.toml'] } }
const { fehler: f, protokoll: p } = await PK(args, {})
soll(f === null, 'Fehler: ' + f)
for (const [l, m] of [['werkzeug:kopie', 'haiku'], ['werkzeug:planwerkzeuge', 'sonnet'], ['werkzeug:mutanten', 'sonnet']]) {
const a = mitLabel(p, l)
soll(a.length === 1 && a[0].modell === m && a[0].effort === 'low' && !a[0].agentType, l + ' ' + JSON.stringify(a.map(x => [x.modell, x.effort, x.agentType])))
}
return fehler
})
fall('plankritik', 'mutanten "aus": kein Agent werkzeug:mutanten', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'aus' }, {})
soll(mitLabel(p, 'werkzeug:mutanten').length === 0, 'Agent trotz mutanten aus')
return fehler
})
fall('plankritik', 'ohne kopie und ohne materialisieren: kein Agent werkzeug:mutanten', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ projekt: 'D:/x', planordner: 'D:/x/plan' }, {})
soll(mitLabel(p, 'werkzeug:mutanten').length === 0, 'Agent ohne Kopie gestartet')
return fehler
})
fall('plankritik', 'mutanten "halb" wirft vor dem ersten Agenten', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'halb' }, {})
soll(f && /mutanten muss/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
fall('plankritik', 'Hilfsagent ohne EXIT mutanten_lauf und ohne KEINE_MUTANTEN_TOML: kein Block <mutantenlauf>', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: 'irgendeine unbrauchbare Ausgabe ohne Marker' })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && !/<mutantenlauf>/.test(arith.prompt), 'Block trotz unbrauchbarer Ausgabe vorhanden')
return fehler
})
fall('plankritik', 'KEINE_MUTANTEN_TOML: Linse bekommt den Hinweissatz statt der Rohausgabe', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: 'KEINE_MUTANTEN_TOML' })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /keine mutanten\.toml/.test(arith.prompt) && /Schwere wichtig/.test(arith.prompt), 'Hinweissatz fehlt: ' + (arith && arith.prompt))
soll(arith && !/<mutantenlauf>/.test(arith.prompt), 'Block trotz KEINE_MUTANTEN_TOML vorhanden')
return fehler
})
fall('plankritik', 'ZEITGRENZE im Mutantenlauf: Linse erfährt es, Hilfsagent fährt mit zehn Minuten', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: 'M1 OK a=ROT(x)\nZEITGRENZE: Mutantenlauf nach 540 s abgebrochen\nEXIT mutanten_lauf: 124' })
const hilfe = mitLabel(p, 'werkzeug:mutanten')[0]
soll(hilfe && /timeout auf 600000/.test(hilfe.prompt), 'Zeitvorgabe fehlt im Auftrag des Hilfsagenten')
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /Zeitgrenze erreicht/.test(arith.prompt) && /<mutantenlauf>/.test(arith.prompt), 'Hinweis auf die Zeitgrenze fehlt: ' + (arith && arith.prompt.slice(-400)))
return fehler
})
// Fälle für die Nachbesserung vom 2026-09-28 (N5, N7, N8, N10): fünf Mutanten aus
// eigene_mutanten.py töten, dazu Modus muster, Exit 2/124 und mutanten_status/mutanten_modus.
fall('plankritik', 'Modus muster wird als muster an den Wrapper übergeben, nicht als voll', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'muster' }, {})
const hilfe = mitLabel(p, 'werkzeug:mutanten')[0]
const zeile = (hilfe ? hilfe.prompt.split('\n').find(z => z.includes('mutanten_lauf_kopie.sh')) : '') || ''
soll(/ muster$/.test(zeile), 'Befehl endet nicht auf muster: ' + zeile)
return fehler
})
fall('plankritik', 'Mutantenstufe läuft auch mit materialisierter Kopie ohne args.kopie', async () => {
const { fehler, soll } = pruefe()
const args = { projekt: 'D:/x', planordner: 'D:/x/plan', materialisieren: { ohne: ['config/secrets.toml'] } }
const { protokoll: p } = await PK(args, {})
soll(mitLabel(p, 'werkzeug:mutanten').length === 1, 'Mutantenlauf lief nicht mit materialisierter Kopie')
return fehler
})
fall('plankritik', 'arithmetik bekommt mehr als die letzten fünf Zeilen des Mutantenlaufs', async () => {
const { fehler, soll } = pruefe()
const marke = 'MARKE-VOR-DEM-ENDE'
const fuellzeilen = Array.from({ length: 50 }, (_, i) => 'zeile ' + i).join('\n')
const ausgabe = `wurzel: /tmp/x\n${marke}\n${fuellzeilen}\nEXIT mutanten_lauf: 0`
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && arith.prompt.includes(marke), 'Marke außerhalb der letzten fünf Zeilen fehlt im Auftrag')
return fehler
})
fall('plankritik', 'mutanten_lauf in der Rückgabe ist bei einem Lauf nicht null', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PK(PK_ARGS, {})
soll(e && e.mutanten_lauf !== null && /EXIT mutanten_lauf/.test(e.mutanten_lauf), 'mutanten_lauf: ' + JSON.stringify(e && e.mutanten_lauf))
return fehler
})
fall('plankritik', 'Wurzel im Mutanten-Befehl ist ctx.kopie, nie args.projekt', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, projekt: 'D:/anderes-projekt' }, {})
const hilfe = mitLabel(p, 'werkzeug:mutanten')[0]
soll(hilfe && hilfe.prompt.includes(PK_ARGS.kopie) && !hilfe.prompt.includes('anderes-projekt'), 'Befehl nennt args.projekt statt ctx.kopie: ' + (hilfe && hilfe.prompt))
return fehler
})
fall('plankritik', 'modus muster: Linse erfährt, dass kein Mutant gefahren ist', async () => {
const { fehler, soll } = pruefe()
const ausgabe = 'wurzel: /tmp/x\nmodus: muster (nur Musterpruefung, kein Mutant gefahren)\nM1 OK Trefferzahl=1\nEXIT mutanten_lauf: 0'
const { protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'muster' }, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /Kein Mutant ist gefahren/.test(arith.prompt), 'Hinweis auf Musterlauf fehlt')
soll(arith && !/prüfe bei OK an der Fehlermeldung/.test(arith.prompt), 'Satz "prüfe bei OK" steht trotz Modus muster noch da')
return fehler
})
fall('plankritik', 'Exit 2 des Mutantenlaufs: Linse erfährt, dass er vor oder während der Mutanten gescheitert ist', async () => {
const { fehler, soll } = pruefe()
const ausgabe = 'wurzel: /tmp/x\nbefunde:\n - x\nEXIT mutanten_lauf: 2'
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /vor oder während der Mutanten gescheitert \(Exit 2\)/.test(arith.prompt) && /Schwere wichtig/.test(arith.prompt), 'Hinweis auf Exit 2 fehlt: ' + (arith && arith.prompt.slice(-400)))
return fehler
})
fall('plankritik', 'mutanten_status und mutanten_modus in allen Zuständen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: eAus } = await PK({ ...PK_ARGS, mutanten: 'aus' }, {})
soll(eAus && eAus.mutanten_status === 'aus' && eAus.mutanten_modus === 'aus', 'aus: ' + JSON.stringify(eAus && [eAus.mutanten_status, eAus.mutanten_modus]))
const { ergebnis: eOhneKopie } = await PK({ projekt: 'D:/x', planordner: 'D:/x/plan' }, {})
soll(eOhneKopie && eOhneKopie.mutanten_status === 'ohne_kopie' && eOhneKopie.mutanten_modus === 'voll', 'ohne_kopie: ' + JSON.stringify(eOhneKopie && [eOhneKopie.mutanten_status, eOhneKopie.mutanten_modus]))
const { ergebnis: eKeineToml } = await PK(PK_ARGS, { mutantenAusgabe: 'KEINE_MUTANTEN_TOML' })
soll(eKeineToml && eKeineToml.mutanten_status === 'keine_toml', 'keine_toml: ' + JSON.stringify(eKeineToml && eKeineToml.mutanten_status))
const { ergebnis: eOhneErgebnis } = await PK(PK_ARGS, { mutantenAusgabe: 'irgendeine unbrauchbare Ausgabe ohne Marker' })
soll(eOhneErgebnis && eOhneErgebnis.mutanten_status === 'ohne_ergebnis', 'ohne_ergebnis: ' + JSON.stringify(eOhneErgebnis && eOhneErgebnis.mutanten_status))
const { ergebnis: eExit2 } = await PK(PK_ARGS, { mutantenAusgabe: 'EXIT mutanten_lauf: 2' })
soll(eExit2 && eExit2.mutanten_status === 'exit_2', 'exit_2: ' + JSON.stringify(eExit2 && eExit2.mutanten_status))
const { ergebnis: eExitDefault } = await PK(PK_ARGS, {})
soll(eExitDefault && eExitDefault.mutanten_status === 'exit_0' && eExitDefault.mutanten_modus === 'voll', 'exit_0: ' + JSON.stringify(eExitDefault && [eExitDefault.mutanten_status, eExitDefault.mutanten_modus]))
const { ergebnis: eMuster } = await PK({ ...PK_ARGS, mutanten: 'muster' }, {})
soll(eMuster && eMuster.mutanten_modus === 'muster', 'muster: ' + JSON.stringify(eMuster && eMuster.mutanten_modus))
return fehler
})
// Planwerkzeuge mit Befund (2026-11-02, Auftrag der Startsitzung, Pascal: "Ja, nachziehen"): Ein
// Planwerkzeug mit Exit 2 und mindestens einer Zeile "BEFUND <datei>:<zeile>:" ist gelaufen. Seine
// Rohausgabe geht an die Linse bezeichner, die Zeilen nach werkzeug_befunde. Exit 2 ohne Befundzeile ist
// ein gescheitertes Werkzeug; "Hilfsagent ohne Ergebnis" heißt nur noch: kein Lauf.
const PK_TEXT = { projekt: 'D:/x', planordner: 'D:/x/plan' }
const Z_OK = ['0 Befund(e).', 'EXIT plan_zeichen: 0']
const B_OK = ['task-01.md: 2 Aus-/wird-Bloecke, im Modus vor geprueft: 1', '0 Befund(e).', 'EXIT plan_bloecke: 0']
const B_ZAUN = 'BEFUND task-01.md:9: Zaun klebt an der Codezeile; Bloecke dieser Taskdatei nicht geprueft'
const Z_BEFUND = 'BEFUND task-02.md:4:12: U+200B ZERO WIDTH SPACE (Cf) in: x'
const ERRNO = "python: can't open file 'D:/x/werkzeuge/plan_bloecke.py': [Errno 2] No such file or directory"
const werkzeuge = (...zeilen) => zeilen.flat().join('\n')
const bezeichner = p => (mitLabel(p, 'plankritik:bezeichner')[0] || {}).prompt || ''
const mitAusgabe = (p, text) => { const a = bezeichner(p); const i = a.indexOf('<werkzeugausgabe>'); return i >= 0 && a.indexOf(text, i) > i }
const hilfsagentLog = p => p.logs.filter(l => /Hilfsagent ohne Ergebnis/.test(l))
const kopieLauf = (exit, ...zeilen) => ({ befehl: 'kopie', exit, ausgabe: ['KOPIE=C:/tmp/k/mat', '12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.', '0 Befund(e).', ...zeilen].join('\n') })
fall('plankritik', 'Planwerkzeuge mit Exit 0: Rohausgabe geht an die Linse bezeichner, werkzeug_befunde leer', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_OK, B_OK) })
soll(f === null, 'Fehler: ' + f)
soll(mitAusgabe(p, 'EXIT plan_bloecke: 0'), 'Rohausgabe fehlt im Auftrag der Linse bezeichner')
soll(e && Array.isArray(e.werkzeug_befunde) && e.werkzeug_befunde.length === 0, 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
soll(!p.logs.some(l => l.startsWith('Planwerkzeuge:')), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'plan_bloecke Exit 2 mit Zaunbefund: gelaufen, Rohausgabe an die Linse, Zeile unter werkzeug_befunde', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_OK, B_ZAUN, '1 Befund(e).', 'EXIT plan_bloecke: 2') })
soll(f === null, 'Fehler: ' + f)
soll(mitAusgabe(p, B_ZAUN), 'Befundzeile fehlt im Auftrag der Linse bezeichner')
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_bloecke', befund: B_ZAUN }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
soll(!p.logs.some(l => l.startsWith('Planwerkzeuge:')), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'plan_zeichen Exit 2 mit Befund (erstes Werkzeug): gelaufen und plan_zeichen zugeordnet', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_BEFUND, '1 Befund(e).', 'EXIT plan_zeichen: 2', B_OK) })
soll(mitAusgabe(p, Z_BEFUND), 'Befundzeile fehlt im Auftrag der Linse bezeichner')
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_zeichen', befund: Z_BEFUND }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
return fehler
})
fall('plankritik', 'Befunde beider Werkzeuge mit CRLF: je Werkzeug zugeordnet, ohne CR', async () => {
const { fehler, soll } = pruefe()
const aus = [Z_BEFUND, '1 Befund(e).', 'EXIT plan_zeichen: 2', B_ZAUN, '1 Befund(e).', 'EXIT plan_bloecke: 2'].join('\r\n')
const { ergebnis: e, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: aus })
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_zeichen', befund: Z_BEFUND }, { werkzeug: 'plan_bloecke', befund: B_ZAUN }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
soll(mitAusgabe(p, B_ZAUN), 'Rohausgabe fehlt im Auftrag der Linse bezeichner')
return fehler
})
for (const [name, aus, werkzeug] of [
['plan_bloecke Exit 2 ohne Befundzeile (Errno 2)', werkzeuge(Z_OK, ERRNO, 'EXIT plan_bloecke: 2'), 'plan_bloecke Exit 2 ohne Befundzeile'],
['plan_zeichen Exit 2 ohne Befundzeile (erstes Werkzeug)', werkzeuge(ERRNO.replace('plan_bloecke', 'plan_zeichen'), 'EXIT plan_zeichen: 2', B_OK), 'plan_zeichen Exit 2 ohne Befundzeile'],
['plan_bloecke Exit 3 ohne "Nichts geprueft"', werkzeuge(Z_OK, 'EXIT plan_bloecke: 3'), 'plan_bloecke Exit 3'],
['plan_bloecke ohne EXIT-Zeile', werkzeuge(Z_OK, B_ZAUN), 'plan_bloecke ohne EXIT-Zeile'],
]) {
fall('plankritik', `${name}: gescheitert, Linse fährt selbst, Log nennt das Werkzeug statt des Hilfsagenten`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: aus })
soll(f === null, 'Fehler: ' + f)
soll(!/<werkzeugausgabe>/.test(bezeichner(p)) && /Fahre dazu plan_bloecke\.py vor/.test(bezeichner(p)), 'Linse bekommt die Ausgabe eines gescheiterten Werkzeugs')
soll(p.logs.some(l => l.startsWith('Planwerkzeuge: ') && l.includes(werkzeug)), 'Log nennt nicht ' + werkzeug + ': ' + p.logs.join(' | '))
soll(hilfsagentLog(p).length === 0, 'Hilfsagent-Meldung trotz Lauf: ' + hilfsagentLog(p).join(' | '))
return fehler
})
}
fall('plankritik', 'plan_bloecke Exit 3 mit "Nichts geprueft": gelaufen, Rohausgabe an die Linse', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_OK, 'task-01.md: 0 Aus-/wird-Bloecke, im Modus vor geprueft: 0', 'Nichts geprueft: kein Aus-/wird-Block gefunden.', 'EXIT plan_bloecke: 3') })
soll(mitAusgabe(p, 'Nichts geprueft'), 'Rohausgabe fehlt im Auftrag der Linse bezeichner')
soll(!p.logs.some(l => l.startsWith('Planwerkzeuge:')), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'Hilfsagent ohne Lauf: nur dann "Hilfsagent ohne Ergebnis"', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: OHNE })
soll(hilfsagentLog(p).some(l => l.startsWith('Planwerkzeuge: Hilfsagent ohne Ergebnis')), 'Log: ' + p.logs.join(' | '))
soll(!/<werkzeugausgabe>/.test(bezeichner(p)), 'Linse bekommt eine Ausgabe ohne Lauf')
return fehler
})
const M_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', materialisieren: { ohne: ['config/secrets.toml'] } }
const MAT_BEFUND = 'BEFUND task-01.md:7: Zaun klebt an der Codezeile; nichts geschrieben'
fall('plankritik', 'materialisieren mit Planbefund: keine Kopie, Zeile unter werkzeug_befunde, temp_ordner genannt', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(M_ARGS, { kopie: kopieLauf(2, MAT_BEFUND, '1 Befund(e).') })
soll(f === null, 'Fehler: ' + f)
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_materialisiere', befund: MAT_BEFUND }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
soll(e && /C:\/tmp\/k bleibt liegen/.test(e.temp_ordner || ''), 'temp_ordner ' + JSON.stringify(e && e.temp_ordner))
soll(p.logs.some(l => /plan_materialisiere\.py meldet 1 Befund/.test(l)) && !p.logs.some(l => /Kopie nicht verwendbar/.test(l)), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'materialisieren mit BEFUND von kopie_bauen.py: kein Planbefund, Kopie nicht verwendbar', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie: { befehl: 'kopie', exit: 2, ausgabe: 'KOPIE=C:/tmp/k/mat\nBEFUND Projekt D:/x ist kein Ordner' } })
soll(e && e.kopie === null && Array.isArray(e.werkzeug_befunde) && e.werkzeug_befunde.length === 0, JSON.stringify(e && [e.kopie, e.werkzeug_befunde]))
soll(p.logs.some(l => /Kopie nicht verwendbar \(Exit 2/.test(l)), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'gebremst vor den Linsen: werkzeug_befunde steht auch dann in der Rückgabe', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_OK, B_ZAUN, '1 Befund(e).', 'EXIT plan_bloecke: 2'), ablesungen: [ablesung(90, 10)] })
soll(e && e.status === 'gebremst', 'status ' + (e && e.status))
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_bloecke', befund: B_ZAUN }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
return fehler
})
// Ein Hilfsagent darf seine Ausgabe auf mehrere laeufe verteilen. Im Echtlauf wf_9ab7fd8e-ad1 (2026-11-02)
// gab werkzeug:planwerkzeuge je Werkzeug einen Lauf zurück; die zwei laeufe unten sind seine Rückgabe wörtlich.
const ECHT_ZAUN = 'BEFUND task-01-probe.md:22: Zaun klebt an der Codezeile, Block ab Zeile 20; der Schlusszaun gehoert auf eine eigene Zeile, ein Text mit Zaunfolge in einen laengeren Zaun; Bloecke dieser Taskdatei nicht geprueft'
const ECHT_LAEUFE = [
{ befehl: 'plan_zeichen.py auf probe/plan', exit: 0, ausgabe: '1 Dateien, 2 Codebloecke gelesen.\nSichtbare Nicht-ASCII-Zeichen in Codebloecken: \n0 Befund(e).\nEXIT plan_zeichen: 0' },
{ befehl: 'plan_bloecke.py vor probe/plan/task-*.md --projekt probe/projekt', exit: 2, ausgabe: ECHT_ZAUN + '\n1 Befund(e).\nEXIT plan_bloecke: 2' },
]
fall('plankritik', 'Planwerkzeuge auf zwei laeufe verteilt (Echtlauf wf_9ab7fd8e-ad1): beide gelesen, Befund an die Linse', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: ECHT_LAEUFE })
soll(mitAusgabe(p, ECHT_ZAUN), 'Befundzeile aus dem zweiten Lauf fehlt im Auftrag der Linse bezeichner')
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_bloecke', befund: ECHT_ZAUN }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
soll(!p.logs.some(l => l.startsWith('Planwerkzeuge:')), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'materialisieren auf zwei laeufe verteilt, Befundzeile im zweiten: Zeile unter werkzeug_befunde', async () => {
const { fehler, soll } = pruefe()
const kopie = [
{ befehl: 'kopie_bauen.py', exit: 0, ausgabe: 'KOPIE=C:/tmp/k/mat\n12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.\n0 Befund(e).' },
{ befehl: 'plan_materialisiere.py', exit: 2, ausgabe: MAT_BEFUND + '\n1 Befund(e).' },
]
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie })
soll(e && e.kopie === null && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_materialisiere', befund: MAT_BEFUND }]), JSON.stringify(e && [e.kopie, e.werkzeug_befunde]))
soll(p.logs.some(l => /plan_materialisiere\.py meldet 1 Befund\(e\) \(Exit 0\/2\)/.test(l)), 'Log: ' + p.logs.join(' | '))
return fehler
})
// Alle laeufe auch im Erfolgspfad der Kopie und im Mutantenlauf (Folgeauftrag 2026-11-02). KOPIE_GLIEDER ist die
// Ausgabe einer Ausführung, je Glied der Kette mktemp, echo, kopie_bauen.py, plan_materialisiere.py auf einen Lauf
// verteilt, wie im Echtlauf je Werkzeug: KOPIE-Zeile im zweiten, angewendeter Block im letzten. Verwendbar ist die
// Kopie nur, wenn jeder Lauf Exit 0 hat; die Kette endet nur mit 0, wenn jedes Glied mit 0 endet. KOPIE_HALB: Der
// erste Lauf ist vollständig und endet mit 0, danach stürzt plan_materialisiere.py an der zweiten Taskdatei ab;
// laeufe[0] allein sah wie eine Kopie aus.
const MAT_ZEILE = "task-01.md: {'neu': 0, 'ersetzt': 1, 'angehaengt': 0, 'uebergangen': 0, 'geloescht': 0}"
const KOPIE_GLIEDER = (exits = [0, 0, 0, 0]) => [
{ befehl: 'T=$(mktemp -d)', exit: exits[0], ausgabe: '' },
{ befehl: 'echo "KOPIE=$(cygpath -m "$T/mat")"', exit: exits[1], ausgabe: 'KOPIE=C:/tmp/k/mat' },
{ befehl: 'kopie_bauen.py', exit: exits[2], ausgabe: '12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.\n0 Befund(e).' },
{ befehl: 'plan_materialisiere.py', exit: exits[3], ausgabe: MAT_ZEILE + '\n0 Befund(e).' },
]
const KOPIE_HALB = [
{ befehl: 'kopie_bauen.py && plan_materialisiere.py', exit: 0, ausgabe: ['KOPIE=C:/tmp/k/mat', '12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.', '0 Befund(e).', MAT_ZEILE].join('\n') },
{ befehl: 'plan_materialisiere.py, task-02.md', exit: 1, ausgabe: "Traceback (most recent call last):\nUnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte" },
]
// Exit 1 an erster bzw. letzter Stelle der vier Glieder, sonst 0.
const exitsMitFehler = stelle => [0, 0, 0, 0].map((x, i) => (i === stelle ? 1 : x))
// Der zweite Lauf ohne Feld exit: Er zählt nicht als 0 und steht im Log als undefined; das Schema LAUF verlangt
// das Feld, die Attrappe prüft nur die oberste Ebene.
const ohneExit = laeufe => laeufe.map((l, i) => (i === 1 ? { befehl: l.befehl, ausgabe: l.ausgabe } : l))
fall('plankritik', 'materialisieren auf vier laeufe verteilt, alle Exit 0: Kopie verwendet, Mutantenlauf und Linse arithmetik darauf', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(M_ARGS, { kopie: KOPIE_GLIEDER() })
soll(f === null, 'Fehler: ' + f)
soll(e && e.kopie === 'C:/tmp/k/mat', 'kopie ' + JSON.stringify(e && e.kopie))
soll(mitLabel(p, 'werkzeug:mutanten').length === 1, 'Mutantenlauf ohne Kopie: ' + mitLabel(p, 'werkzeug:mutanten').length)
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && arith.prompt.includes('Der Plan ist auf die Kopie C:/tmp/k/mat angewendet'), 'Linse arithmetik rechnet ohne Kopie')
soll(!p.logs.some(l => l.startsWith('Plankritik: ')), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'materialisieren: erster Lauf vollständig mit Exit 0, Absturz im zweiten: keine Kopie, Log nennt beide Exits', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK({ ...M_ARGS, materialisieren: { ...M_ARGS.materialisieren, tasks: ['task-01.md', 'task-02.md'] } }, { kopie: KOPIE_HALB })
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
soll(p.logs.some(l => l.includes('Kopie nicht verwendbar (Exit 0/1)')), 'Log: ' + p.logs.join(' | '))
return fehler
})
for (const stelle of [0, 3]) {
fall('plankritik', `materialisieren auf vier laeufe verteilt, Exit 1 im ${stelle ? 'letzten' : 'ersten'}: keine Kopie, Log nennt alle Exits`, async () => {
const { fehler, soll } = pruefe()
const exits = exitsMitFehler(stelle)
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie: KOPIE_GLIEDER(exits) })
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
soll(p.logs.some(l => l.includes(`Kopie nicht verwendbar (Exit ${exits.join('/')})`)), 'Log: ' + p.logs.join(' | '))
return fehler
})
}
fall('plankritik', 'materialisieren auf vier laeufe verteilt, ein Lauf ohne exit: keine Kopie, Log nennt undefined', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie: ohneExit(KOPIE_GLIEDER()) })
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
soll(p.logs.some(l => l.includes('Kopie nicht verwendbar (Exit 0/undefined/0/0)')), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'materialisieren, Hilfsagent ohne Ergebnis: keine Kopie, Log nennt den Hilfsagenten', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie: OHNE })
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
soll(p.logs.includes('Plankritik: Hilfsagent ohne Ergebnis, keine Kopie'), 'Log: ' + p.logs.join(' | '))
return fehler
})
// Mutantenlauf auf zwei laeufe verteilt, etwa Anfang und Ende einer gekürzten Ausgabe: Die EXIT-Zeile und
// KEINE_MUTANTEN_TOML stehen im zweiten.
fall('plankritik', 'Mutantenlauf auf zwei laeufe verteilt, EXIT-Zeile im zweiten: exit_0, beide Teile an die Linse arithmetik', async () => {
const { fehler, soll } = pruefe()
const mutanten = [
{ befehl: 'mutanten_lauf_kopie.sh, Anfang', exit: 0, ausgabe: 'wurzel: /tmp/x\nmodus: voll\nM1 OK a=ROT(MUTANT-ANFANG-01)' },
{ befehl: 'mutanten_lauf_kopie.sh, Ende', exit: 0, ausgabe: 'M2 OK b=ROT(MUTANT-ENDE-02)\nergebnis: 2 Mutanten, 2 OK, 0 LUECKE, 0 UNKLAR, 0 ZEIT, 0 MUSTER\nEXIT mutanten_lauf: 0' },
]
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { mutanten })
soll(e && e.mutanten_status === 'exit_0', 'mutanten_status ' + JSON.stringify(e && e.mutanten_status))
const arith = (mitLabel(p, 'plankritik:arithmetik')[0] || {}).prompt || ''
const block = arith.includes('<mutantenlauf>') ? arith.slice(arith.indexOf('<mutantenlauf>')) : ''
soll(block.includes('MUTANT-ANFANG-01') && block.includes('MUTANT-ENDE-02'), 'Block <mutantenlauf> ohne beide Teile: ' + JSON.stringify(block.slice(0, 300)))
soll(e && typeof e.mutanten_lauf === 'string' && e.mutanten_lauf.includes('MUTANT-ANFANG-01') && e.mutanten_lauf.includes('EXIT mutanten_lauf: 0'), 'mutanten_lauf ' + JSON.stringify(e && e.mutanten_lauf))
soll(!p.logs.some(l => l.startsWith('Mutantenlauf:')), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('plankritik', 'Mutantenlauf auf zwei laeufe verteilt, KEINE_MUTANTEN_TOML im zweiten: keine_toml, Hinweissatz an die Linse', async () => {
const { fehler, soll } = pruefe()
const mutanten = [
{ befehl: 'wsl -d Debian', exit: 0, ausgabe: 'wsl: Hinweis vor dem Skript' },
{ befehl: 'mutanten_lauf_kopie.sh', exit: 0, ausgabe: 'KEINE_MUTANTEN_TOML' },
]
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { mutanten })
soll(e && e.mutanten_status === 'keine_toml', 'mutanten_status ' + JSON.stringify(e && e.mutanten_status))
const arith = (mitLabel(p, 'plankritik:arithmetik')[0] || {}).prompt || ''
soll(/keine mutanten\.toml/.test(arith) && !/<mutantenlauf>/.test(arith), 'Hinweissatz fehlt oder Block trotz KEINE_MUTANTEN_TOML')
return fehler
})
// Ein zusätzlicher Lauf des Hilfsagenten zeigt den Skripttext von mutanten_lauf_kopie.sh um die Suche nach
// mutanten.toml: KEINE_MUTANTEN_TOML steht dort nur in einem echo, nicht als eigene Zeile der Ausgabe.
fall('plankritik', 'Mutantenlauf mit zusätzlichem Lauf, der das Skript zeigt: exit_0 bleibt, kein Satz zur fehlenden mutanten.toml', async () => {
const { fehler, soll } = pruefe()
const mutanten = [
{ befehl: 'mutanten_lauf_kopie.sh', exit: 0, ausgabe: 'wurzel: /tmp/x\nmodus: voll\nM1 OK a=ROT(MUTANT-SKRIPT-03)\nergebnis: 1 Mutanten, 1 OK, 0 LUECKE, 0 UNKLAR, 0 ZEIT, 0 MUSTER\nEXIT mutanten_lauf: 0' },
{ befehl: 'cat mutanten_lauf_kopie.sh', exit: 0, ausgabe: 'MUTANTEN_TOML="$PLANORDNER/mutanten.toml"\nif [ ! -f "$MUTANTEN_TOML" ]; then\n echo "KEINE_MUTANTEN_TOML"\n exit 0\nfi' },
]
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { mutanten })
soll(e && e.mutanten_status === 'exit_0', 'mutanten_status ' + JSON.stringify(e && e.mutanten_status))
const arith = (mitLabel(p, 'plankritik:arithmetik')[0] || {}).prompt || ''
soll(arith.includes('<mutantenlauf>') && arith.includes('MUTANT-SKRIPT-03') && !/keine mutanten\.toml/.test(arith), 'Linse arithmetik ohne Mutantenlauf oder mit dem Satz zur fehlenden mutanten.toml')
return fehler
})
// ---------------------------------------------------------------- pruefung
const PR_ARGS = { projekt: 'D:/x', plan: 'D:/x/plan' }
const PR = (a, k, o = {}) => fuehreAus(WF('pruefung'), a, antworten(k), { ordner: ORDNER, ...o })
fall('pruefung', 'Grundlinie: Ablesung vor und nach dem pruefer', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PR(PR_ARGS, {})
soll(f === null, 'Fehler: ' + f)
const folge = p.agents.slice().sort((x, y) => x.start - y.start).map(x => x.label)
soll(JSON.stringify(folge) === JSON.stringify(['verbrauch:pruefung:vor Prüfung', 'pruefung', 'verbrauch:pruefung:nach Prüfung']), 'Folge: ' + JSON.stringify(folge))
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'ok' && v.plaetze === 0 && v.gebremst === false), JSON.stringify(e && e.verbrauch))
soll(e && e.effort === '(Frontmatter)', 'effort ' + (e && e.effort))
soll(mitLabel(p, 'pruefung')[0].modell === 'opus', 'pruefer nicht auf opus')
ohneVerstoss(p, soll)
return fehler
})
fall('pruefung', 'kritisch hebt auf xhigh', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PR({ ...PR_ARGS, kritisch: true }, {})
soll(e.effort === 'xhigh' && mitLabel(p, 'pruefung:kritisch')[0].effort === 'xhigh', 'effort ' + e.effort)
return fehler
})
fall('pruefung', 'verbrauch false: keine Ablesung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PR({ ...PR_ARGS, verbrauch: false }, {})
soll(mitLabel(p, 'verbrauch:').length === 0 && e.verbrauch.length === 0, 'Ablesung trotz verbrauch false')
return fehler
})
fall('pruefung', 'als Kind ohne verbrauch false: Ablesung scheitert, Prüfung läuft', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PR(PR_ARGS, {}, { tiefe: 1 })
soll(f === null, 'Fehler: ' + f)
soll(mitLabel(p, 'pruefung').length === 1, 'pruefer nicht gelaufen')
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch))
return fehler
})
fall('pruefung', 'pruefer ohne Bericht: Fehler, beide Ablesungen gelaufen', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PR(PR_ARGS, { pruefer: OHNE })
soll(f === 'pruefung: der pruefer hat keinen Bericht geliefert', 'Fehler: ' + f)
soll(mitLabel(p, 'verbrauch:').length === 2, 'Ablesungen: ' + mitLabel(p, 'verbrauch:').length)
return fehler
})
for (const verbrauch of ['nein', 0, null]) {
fall('pruefung', `verbrauch ${JSON.stringify(verbrauch)} wirft`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PR({ ...PR_ARGS, verbrauch }, {})
soll(f && /verbrauch muss/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
// ---------------------------------------------------------------- welle-mit-pruefung
const WM_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', tasks: [{ datei: 'task-01.md', modell: 'haiku' }], kopie: 'C:/tmp/k/mat', suite: 'pytest -q' }
const WM = (a, k) => fuehreAus(WF('welle-mit-pruefung'), a, antworten(k), { ordner: ORDNER })
fall('welle-mit-pruefung', 'Grundlinie: Welle, Vergleich, Ablesung um die Prüfung, Kind liest nicht selbst', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_ARGS, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
soll(mitLabel(p, 'executer:').length === 1 && mitLabel(p, 'werkzeug:vergleich').length === 1 && mitLabel(p, 'pruefung').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(JSON.stringify(ab) === JSON.stringify(['verbrauch:welle-mit-pruefung:vor Prüfung', 'verbrauch:welle-mit-pruefung:nach Prüfung']), 'Ablesungen: ' + JSON.stringify(ab))
const kind = p.workflows.find(w => w.ref === 'pruefung')
soll(kind && kind.args.verbrauch === false, 'Kind pruefung ohne verbrauch false')
soll(e.pruefung.verbrauch.length === 0 && mitLabel(p, 'verbrauch:pruefung:').length === 0, 'Kind hat selbst abgelesen')
soll(e.verbrauch.length === 2, 'verbrauch ' + e.verbrauch.length)
ohneVerstoss(p, soll)
return fehler
})
fall('welle-mit-pruefung', 'Modell je Hilfslabel: kopie haiku/low, vergleich sonnet/low', async () => {
const { fehler, soll } = pruefe()
const { kopie, ...ohneKopie } = WM_ARGS
const { fehler: f, protokoll: p } = await WM({ ...ohneKopie, ohne: ['config/secrets.toml'] }, {})
soll(f === null, 'Fehler: ' + f)
for (const [l, m] of [['werkzeug:kopie', 'haiku'], ['werkzeug:vergleich', 'sonnet']]) {
const a = mitLabel(p, l)
soll(a.length === 1 && a[0].modell === m && a[0].effort === 'low' && !a[0].agentType, l + ' ' + JSON.stringify(a.map(x => [x.modell, x.effort, x.agentType])))
}
return fehler
})
fall('welle-mit-pruefung', 'bis vergleich: keine Prüfung, pruefung_args mit Rohbelegen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await WM({ ...WM_ARGS, bis: 'vergleich' }, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'bis_vergleich', 'status ' + (e && e.status))
soll(mitLabel(p, 'pruefung').length === 0 && mitLabel(p, 'verbrauch:').length === 0, 'Prüfung oder Ablesung trotz bis')
const pa = e && e.pruefung_args
soll(pa && /VERGLEICH-OK/.test(pa.schwerpunkt) && pa.plan === 'D:/x/plan' && JSON.stringify(pa.tasks) === '["task-01.md"]', 'pruefung_args ' + JSON.stringify(pa))
soll(pa && !('verbrauch' in pa), 'pruefung_args trägt verbrauch')
soll(e && /nur lesend/.test(e.naechster_schritt), 'naechster_schritt ohne Lesevorgabe')
return fehler
})
fall('welle-mit-pruefung', 'bis vergleich bei unvollständiger Welle: keine pruefung_args', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await WM({ ...WM_ARGS, bis: 'vergleich' }, { executer: OHNE })
soll(e && e.status === 'bis_vergleich' && e.pruefung_args === null && /unvollständig/.test(e.pruefung_grund), JSON.stringify(e && [e.status, e.pruefung_args, e.pruefung_grund]))
return fehler
})
fall('welle-mit-pruefung', 'unvollständige Welle: Prüfung übersprungen, keine Ablesung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await WM(WM_ARGS, { executer: OHNE })
soll(e.status === 'ohne_pruefung' && /unvollständig/.test(e.pruefung.uebersprungen), JSON.stringify([e.status, e.pruefung]))
soll(mitLabel(p, 'verbrauch:').length === 0 && mitLabel(p, 'pruefung').length === 0, 'Ablesung oder Prüfung trotz Lücke')
return fehler
})
fall('welle-mit-pruefung', 'verbrauch.js fehlt: Ablesungen nicht_gelesen, Welle und Prüfung bleiben', async () => {
const { fehler, soll } = pruefe()
const ohneVerbrauch = mkdtempSync(path.join(os.tmpdir(), 'ohne-verbrauch-'))
cpSync(ORDNER, ohneVerbrauch, { recursive: true })
rmSync(path.join(ohneVerbrauch, 'verbrauch.js'))
const { ergebnis: e, fehler: f } = await fuehreAus(path.join(ohneVerbrauch, 'welle-mit-pruefung.js'), WM_ARGS, antworten({}), { ordner: ohneVerbrauch })
rmSync(ohneVerbrauch, { recursive: true, force: true })
soll(f === null && e.status === 'fertig' && e.welle && e.pruefung.zaehlung, 'Fehler/Ergebnis: ' + f + ' ' + JSON.stringify(e && e.status))
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch))
return fehler
})
for (const bis of ['alles', '', 1]) {
fall('welle-mit-pruefung', `bis ${JSON.stringify(bis)} wirft`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await WM({ ...WM_ARGS, bis }, {})
soll(f && /bis kennt nur/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
// Planbefund (2026-11-02, Auftrag der Startsitzung, Pascal: "Ja, nachziehen"): Meldet plan_materialisiere.py
// in der Phase Kopie eine Zeile "BEFUND <taskdatei>:<zeile>:", endet der Lauf vor executer-welle. Exit 2
// ohne Befundzeile und die BEFUND-Zeilen von kopie_bauen.py nehmen den alten Weg (Welle ohne Byte-Vergleich).
const { kopie: _wmKopie, ...WM_OHNE_KOPIE } = WM_ARGS
const WM_BAU = { ...WM_OHNE_KOPIE, ohne: ['config/secrets.toml'] }
const ZAUN1 = 'BEFUND task-01.md:7: Zaun klebt an der Codezeile; nichts geschrieben'
const ZAUN2 = 'BEFUND task-02.md:31: Block schliesst nie; nichts geschrieben'
function ohneWelle(p, soll) {
soll(!p.workflows.some(w => w.ref === 'executer-welle'), 'executer-welle aufgerufen')
const danach = p.agents.filter(x => x.label !== 'werkzeug:kopie').map(x => x.label)
soll(danach.length === 0, 'Agenten nach dem Halt: ' + JSON.stringify(danach))
}
fall('welle-mit-pruefung', 'Planbefund: hält vor executer-welle, Befundzeile in plan_befunde', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_BAU, { kopie: kopieLauf(2, ZAUN1, '1 Befund(e).') })
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'plan_befund', 'status ' + (e && e.status))
soll(e && JSON.stringify(e.plan_befunde) === JSON.stringify([ZAUN1]), 'plan_befunde ' + JSON.stringify(e && e.plan_befunde))
soll(e && e.welle === null && e.kopie === null && e.pruefung_args === null && /C:\/tmp\/k bleibt liegen/.test(e.temp_ordner || ''), JSON.stringify(e && [e.welle, e.kopie, e.pruefung_args, e.temp_ordner]))
soll(e && e.kopie_lauf && e.kopie_lauf.laeufe[0].exit === 2, 'kopie_lauf ' + JSON.stringify(e && e.kopie_lauf))
soll(p.logs.some(l => /meldet 1 Befund/.test(l)), 'Log: ' + p.logs.join(' | '))
ohneWelle(p, soll)
ohneVerstoss(p, soll)
return fehler
})
fall('welle-mit-pruefung', 'Planbefund mit bis vergleich: hält ebenso, keine pruefung_args', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await WM({ ...WM_BAU, bis: 'vergleich' }, { kopie: kopieLauf(2, ZAUN1, '1 Befund(e).') })
soll(e && e.status === 'plan_befund' && e.pruefung_args === null && /Befund/.test(e.pruefung_grund || ''), JSON.stringify(e && [e.status, e.pruefung_args, e.pruefung_grund]))
ohneWelle(p, soll)
return fehler
})
fall('welle-mit-pruefung', 'Planbefund an erster und letzter Stelle, CRLF: beide Zeilen ohne CR, in Reihenfolge', async () => {
const { fehler, soll } = pruefe()
const tasks = [{ datei: 'task-01.md', modell: 'haiku' }, { datei: 'task-02.md', modell: 'haiku' }]
const ausgabe = ['KOPIE=C:/tmp/k/mat', ZAUN1, 'zwischen', ZAUN2, '2 Befund(e).'].join('\r\n')
const { ergebnis: e, protokoll: p } = await WM({ ...WM_BAU, tasks }, { kopie: { befehl: 'kopie', exit: 2, ausgabe } })
soll(e && e.status === 'plan_befund' && JSON.stringify(e.plan_befunde) === JSON.stringify([ZAUN1, ZAUN2]), JSON.stringify(e && [e.status, e.plan_befunde]))
soll(e && /C:\/tmp\/k bleibt liegen/.test(e.temp_ordner || ''), 'temp_ordner ' + JSON.stringify(e && e.temp_ordner))
ohneWelle(p, soll)
return fehler
})
fall('welle-mit-pruefung', 'Absturz nach einer Befundzeile (Exit 1): hält ebenso', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie: kopieLauf(1, ZAUN1, 'Traceback (most recent call last):', 'OSError: x') })
soll(e && e.status === 'plan_befund' && JSON.stringify(e.plan_befunde) === JSON.stringify([ZAUN1]), JSON.stringify(e && [e.status, e.plan_befunde]))
ohneWelle(p, soll)
return fehler
})
for (const [name, lauf] of [
['Exit 2 ohne Befundzeile (Errno 2)', { befehl: 'kopie', exit: 2, ausgabe: "KOPIE=C:/tmp/k/mat\n12 Dateien kopiert.\n0 Befund(e).\npython: can't open file 'D:/x/plan_materialisiere.py': [Errno 2] No such file or directory" }],
['BEFUND von kopie_bauen.py ohne Zeilennummer', { befehl: 'kopie', exit: 2, ausgabe: 'KOPIE=C:/tmp/k/mat\nBEFUND Projekt D:/x ist kein Ordner' }],
['BEFUND von kopie_bauen.py zu einem Ausschluss', { befehl: 'kopie', exit: 1, ausgabe: "KOPIE=C:/tmp/k/mat\n12 Dateien kopiert.\nBEFUND Ausschluss 'config/secrets.toml' gibt es nicht\n1 Befund(e)." }],
]) {
fall('welle-mit-pruefung', `${name}: kein Planbefund, Welle läuft wie bisher ohne Byte-Vergleich`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_BAU, { kopie: lauf })
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig' && e.kopie === null && !('plan_befunde' in e), JSON.stringify(e && [e.status, e.kopie, e.plan_befunde]))
soll(mitLabel(p, 'executer:').length === 1, 'executer ' + mitLabel(p, 'executer:').length)
soll(p.logs.some(l => /Kopie nicht verwendbar \(Exit [12]\)/.test(l)), 'Log: ' + p.logs.join(' | '))
return fehler
})
}
fall('welle-mit-pruefung', 'Hilfsagent der Kopie ohne Ergebnis: Welle läuft wie bisher', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie: OHNE })
soll(e && e.status === 'fertig' && mitLabel(p, 'executer:').length === 1, JSON.stringify(e && e.status))
soll(p.logs.some(l => /Hilfsagent ohne Ergebnis/.test(l)), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('welle-mit-pruefung', 'Kopie auf zwei laeufe verteilt, Befundzeile im zweiten: hält ebenso', async () => {
const { fehler, soll } = pruefe()
const kopie = [
{ befehl: 'kopie_bauen.py', exit: 0, ausgabe: 'KOPIE=C:/tmp/k/mat\n12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.\n0 Befund(e).' },
{ befehl: 'plan_materialisiere.py', exit: 2, ausgabe: ZAUN1 + '\n1 Befund(e).' },
]
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie })
soll(e && e.status === 'plan_befund' && JSON.stringify(e.plan_befunde) === JSON.stringify([ZAUN1]), JSON.stringify(e && [e.status, e.plan_befunde]))
soll(p.logs.some(l => /meldet 1 Befund\(e\) \(Exit 0\/2\)/.test(l)), 'Log: ' + p.logs.join(' | '))
ohneWelle(p, soll)
return fehler
})
// Alle laeufe auch im Erfolgspfad der Kopie und im Vergleich (Folgeauftrag 2026-11-02); KOPIE_GLIEDER, KOPIE_HALB
// und exitsMitFehler stehen bei plankritik.
const vergleichAuftrag = p => (mitLabel(p, 'werkzeug:vergleich')[0] || {}).prompt || ''
fall('welle-mit-pruefung', 'Kopie auf vier laeufe verteilt, alle Exit 0: Kopie verwendet, Byte-Vergleich gegen sie', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_BAU, { kopie: KOPIE_GLIEDER() })
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig' && e.kopie === 'C:/tmp/k/mat', JSON.stringify(e && [e.status, e.kopie]))
soll(vergleichAuftrag(p).includes("--mat 'C:/tmp/k/mat'"), 'Byte-Vergleich ohne die Kopie: ' + vergleichAuftrag(p).slice(0, 300))
soll(!p.logs.some(l => /Kopie nicht verwendbar/.test(l)), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('welle-mit-pruefung', 'Kopie: erster Lauf vollständig mit Exit 0, Absturz im zweiten: Kopie nicht verwendet, Log nennt beide Exits', async () => {
const { fehler, soll } = pruefe()
const tasks = [{ datei: 'task-01.md', modell: 'haiku' }, { datei: 'task-02.md', modell: 'haiku' }]
const { ergebnis: e, protokoll: p } = await WM({ ...WM_BAU, tasks }, { kopie: KOPIE_HALB })
soll(e && e.status === 'fertig' && e.kopie === null && !('plan_befunde' in e), JSON.stringify(e && [e.status, e.kopie]))
soll(!vergleichAuftrag(p).includes('--mat'), 'Byte-Vergleich gegen eine halbe Kopie: ' + vergleichAuftrag(p).slice(0, 300))
soll(p.logs.some(l => l.includes('Kopie nicht verwendbar (Exit 0/1)')), 'Log: ' + p.logs.join(' | '))
return fehler
})
for (const stelle of [0, 3]) {
fall('welle-mit-pruefung', `Kopie auf vier laeufe verteilt, Exit 1 im ${stelle ? 'letzten' : 'ersten'}: Kopie nicht verwendet, Log nennt alle Exits`, async () => {
const { fehler, soll } = pruefe()
const exits = exitsMitFehler(stelle)
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie: KOPIE_GLIEDER(exits) })
soll(e && e.status === 'fertig' && e.kopie === null, JSON.stringify(e && [e.status, e.kopie]))
soll(p.logs.some(l => l.includes(`Kopie nicht verwendbar (Exit ${exits.join('/')})`)), 'Log: ' + p.logs.join(' | '))
return fehler
})
}
fall('welle-mit-pruefung', 'Kopie auf vier laeufe verteilt, ein Lauf ohne exit: Kopie nicht verwendet, Log nennt undefined', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie: ohneExit(KOPIE_GLIEDER()) })
soll(e && e.status === 'fertig' && e.kopie === null, JSON.stringify(e && [e.status, e.kopie]))
soll(p.logs.some(l => l.includes('Kopie nicht verwendbar (Exit 0/undefined/0/0)')), 'Log: ' + p.logs.join(' | '))
return fehler
})
fall('welle-mit-pruefung', 'Vergleich auf zwei laeufe verteilt: Byte-Vergleich und Suite erreichen den pruefer, in ihrer Reihenfolge', async () => {
const { fehler, soll } = pruefe()
const vergleich = [
{ befehl: 'welle_vergleich.py', exit: 0, ausgabe: 'VERGLEICH-MARKE-01: task-01.md, 1 Datei, Bytes gleich\nEXIT welle_vergleich: 0' },
{ befehl: 'pytest -q', exit: 1, ausgabe: 'SUITE-MARKE-02: 1 failed, 3 passed\nEXIT suite: 1' },
]
const { ergebnis: e, protokoll: p } = await WM(WM_ARGS, { vergleich })
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
const auftrag = (mitLabel(p, 'pruefung')[0] || {}).prompt || ''
const ab = auftrag.indexOf('Schwerpunkt:')
soll(ab >= 0 && ['VERGLEICH-MARKE-01', 'EXIT welle_vergleich: 0', 'SUITE-MARKE-02', 'EXIT suite: 1'].every(t => auftrag.indexOf(t, ab) > ab), 'Auftrag des pruefer ohne beide laeufe: ' + JSON.stringify(auftrag.slice(ab, ab + 400)))
soll(auftrag.indexOf('VERGLEICH-MARKE-01', ab) < auftrag.indexOf('SUITE-MARKE-02', ab), 'Reihenfolge der laeufe vertauscht: ' + JSON.stringify(auftrag.slice(ab, ab + 400)))
return fehler
})
// ---------------------------------------------------------------- planung
const T = (nr, voraussetzungen, welle) => ({ nr, thema: 'Thema ' + nr, datei: `task-${String(nr).padStart(2, '0')}-t.md`, modell: 'haiku', voraussetzungen, welle })
const DAG = [T(1, [], 1), T(2, [1], 2), T(3, [1], 2), T(4, [1], 2), T(5, [2, 3], 3), T(6, [4], 3), T(7, [], 1)]
const PL_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', gemeinsam: 'GEMEINSAM', taskauftrag: 'Task {NR} {THEMA} in {DATEI}, Modell {MODELL}, Welle {WELLE}, vor: {VORAUSSETZUNGEN}', tasks: DAG }
const PL = (a, k, o = {}) => fuehreAus(WF('planung'), a, antworten(k), { ordner: ORDNER, ...o })
const planerVon = (p, nr) => p.agents.find(x => x.label === `planer:task-${String(nr).padStart(2, '0')}`)
function voraussetzungenEingehalten(p, tasks, soll) {
for (const t of tasks) {
const a = planerVon(p, t.nr)
if (!a) continue
for (const n of t.voraussetzungen) {
const v = planerVon(p, n)
soll(v && v.ende !== null && v.ende < a.start, `Task ${t.nr} startete vor dem Ende von Task ${n}`)
}
}
}
fall('planung', 'Grundlinie: Start nach den Voraussetzungen, ohne Wunsch zwei gleichzeitig', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PL(PL_ARGS, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig' && e.geschrieben.length === 7, 'status ' + (e && e.status))
voraussetzungenEingehalten(p, DAG, soll)
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(ab[0] === 'verbrauch:planung:Anfang' && ab[ab.length - 1] === 'verbrauch:planung:Ende' && ab.includes('verbrauch:planung:nach 4 Tasks'), 'Ablesungen: ' + JSON.stringify(ab))
soll(e && e.plaetze_verlauf[0] === 2, 'plaetze_verlauf ' + JSON.stringify(e && e.plaetze_verlauf))
soll(p.agents.filter(x => x.label.startsWith('planer:')).every(x => x.modell === 'opus' && x.agentType === 'planer'), 'planer nicht auf opus/planer')
ohneVerstoss(p, soll)
return fehler
})
fall('planung', 'Wunsch vier: drei bis vier gleichzeitig, Voraussetzungen halten', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, {})
const s = spitze(p, 'Taskdateien', 'opus')
soll(s >= 3 && s <= 4, 'gleichzeitig: ' + s)
soll(e.status === 'fertig' && e.plaetze_verlauf[0] === 4, JSON.stringify([e.status, e.plaetze_verlauf]))
voraussetzungenEingehalten(p, DAG, soll)
return fehler
})
const FREI = [1, 2, 3, 4, 5, 6, 7, 8].map(n => T(n, [], 1))
fall('planung', 'Nachlesen senkt die Plätze: danach startet keiner neben zwei anderen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 4, nachlesen_alle: 2 }, { ablesungen: [ablesung(10, 10), ablesung(60, 10), ablesung(60, 10), ablesung(60, 10), ablesung(60, 10)] })
const zweite = mitLabel(p, 'verbrauch:')[1]
const danach = mitLabel(p, 'planer:').filter(a => a.start > zweite.start)
soll(danach.length > 0, 'nach der zweiten Ablesung startete kein Planer')
soll(danach.every(a => laufendBeiStart(p, a, 'planer:') <= 1), 'Start neben zwei oder mehr: ' + JSON.stringify(danach.map(a => [a.label, laufendBeiStart(p, a, 'planer:')])))
soll(e.status === 'fertig' && JSON.stringify(e.plaetze_verlauf.slice(0, 2)) === '[4,2]', JSON.stringify([e.status, e.plaetze_verlauf]))
return fehler
})
fall('planung', 'Nachlesen hebt die Plätze: danach laufen mehr als zwei', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 4, nachlesen_alle: 2 }, { ablesungen: [ablesung(60, 10), ablesung(10, 10), ablesung(10, 10), ablesung(10, 10)] })
const zweite = mitLabel(p, 'verbrauch:')[1]
const danach = mitLabel(p, 'planer:').filter(a => a.start > zweite.start)
soll(danach.some(a => laufendBeiStart(p, a, 'planer:') >= 2), 'nach dem Heben nie mehr als zwei gleichzeitig')
soll(e.status === 'fertig' && JSON.stringify(e.plaetze_verlauf.slice(0, 2)) === '[2,4]', JSON.stringify([e.status, e.plaetze_verlauf]))
return fehler
})
fall('planung', 'gebremst schon am Anfang: kein Planer, Status gebremst', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { ablesungen: [ablesung(90, 10)] })
soll(mitLabel(p, 'planer:').length === 0, 'Planer trotz Bremse')
soll(e.status === 'gebremst' && e.offen.length === 7, JSON.stringify([e.status, e.offen]))
soll(/nicht mit resumeFromRunId/.test(e.naechster_schritt), 'naechster_schritt ' + e.naechster_schritt)
return fehler
})
fall('planung', 'Startreihenfolge folgt Welle und Nummer, nicht der Liste', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: [T(3, [], 2), T(2, [], 1), T(1, [], 1)], plaetze: 1 }, {})
const folge = mitLabel(p, 'planer:').sort((x, y) => x.start - y.start).map(a => a.label)
soll(JSON.stringify(folge) === '["planer:task-01","planer:task-02","planer:task-03"]', 'Folge ' + JSON.stringify(folge))
return fehler
})
fall('planung', 'kein Nachlesen, wenn kein Task mehr offen ist', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI.slice(0, 3), plaetze: 4, nachlesen_alle: 1 }, {})
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(JSON.stringify(ab) === '["verbrauch:planung:Anfang","verbrauch:planung:Ende"]', 'Ablesungen: ' + JSON.stringify(ab))
return fehler
})
fall('planung', 'Ergebnis mit fremder Datei gilt als gescheitert', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { planerDatei: (nr, richtig) => (nr === 2 ? 'D:/x/plan/task-99-t.md' : richtig) })
soll(e.gescheitert.includes(2) && e.blockiert.includes(5) && !planerVon(p, 5), JSON.stringify([e.gescheitert, e.blockiert]))
soll(p.logs.some(l => /statt task-02-t\.md/.test(l)), 'kein Hinweis auf die fremde Datei')
return fehler
})
fall('planung', 'Ergebnis mit ähnlichem Dateinamen gilt als gescheitert', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PL(PL_ARGS, { planerDatei: (nr, richtig) => (nr === 7 ? 'D:/x/plan/xtask-07-t.md' : richtig) })
soll(JSON.stringify(e.gescheitert) === '[7]', JSON.stringify(e.gescheitert))
return fehler
})
fall('planung', 'Nachlesen im Takt von nachlesen_alle, solange Tasks offen sind', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 2, nachlesen_alle: 2 }, { planerDauer: () => 100 })
const ab = mitLabel(p, 'verbrauch:').map(x => x.label.slice('verbrauch:planung:'.length))
soll(JSON.stringify(ab) === '["Anfang","nach 2 Tasks","nach 4 Tasks","nach 6 Tasks","Ende"]', 'Ablesungen: ' + JSON.stringify(ab))
return fehler
})
fall('planung', '5 Stunden bei 60 %: höchstens zwei gleichzeitig', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, { ablesungen: [ablesung(60, 10), ablesung(60, 10)] })
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
soll(e.status === 'fertig', 'status ' + e.status)
voraussetzungenEingehalten(p, DAG, soll)
return fehler
})
fall('planung', 'Bremse beim Nachlesen: kein Start mehr, laufende enden', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, nachlesen_alle: 2 }, { ablesungen: [ablesung(10, 10), ablesung(90, 10)] })
soll(e.status === 'gebremst' && e.offen.length > 0, JSON.stringify([e.status, e.offen]))
const zweite = mitLabel(p, 'verbrauch:')[1]
soll(zweite && p.agents.filter(x => x.label.startsWith('planer:') && x.start > zweite.start).length === 0, 'Planer nach der Bremse gestartet')
soll(p.agents.filter(x => x.label.startsWith('planer:')).every(x => x.ende !== null), 'laufender Planer nicht beendet')
return fehler
})
fall('planung', 'erste Voraussetzung scheitert: abhängige blockiert, unabhängige laufen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { planer: nr => (nr === 1 ? OHNE : undefined) })
soll(e.status === 'unvollstaendig', 'status ' + e.status)
soll(JSON.stringify(e.gescheitert) === '[1]' && JSON.stringify(e.blockiert) === '[2,3,4,5,6]', JSON.stringify([e.gescheitert, e.blockiert]))
soll(e.geschrieben.map(g => g.nr).join() === '7', 'geschrieben ' + e.geschrieben.map(g => g.nr))
soll([2, 3, 4, 5, 6].every(n => !planerVon(p, n)), 'blockierter Task gestartet')
return fehler
})
fall('planung', 'letzte Tasks scheitern: nichts blockiert', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PL(PL_ARGS, { planer: nr => (nr === 6 || nr === 7 ? OHNE : undefined) })
soll(e.status === 'unvollstaendig' && JSON.stringify(e.gescheitert.slice().sort()) === '[6,7]' && e.blockiert.length === 0, JSON.stringify([e.status, e.gescheitert, e.blockiert]))
soll(e.geschrieben.length === 5, 'geschrieben ' + e.geschrieben.length)
return fehler
})
fall('planung', 'erledigt: Task 1 startet nicht, die abhängigen schon', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, erledigt: [1] }, {})
soll(!planerVon(p, 1), 'Task 1 gestartet')
soll(e.status === 'fertig' && e.geschrieben.length === 6, JSON.stringify([e.status, e.geschrieben.length]))
return fehler
})
fall('planung', 'drei Planer ohne Ergebnis: Abbruch, kein weiterer Start', async () => {
const { fehler, soll } = pruefe()
const frei = [1, 2, 3, 4, 5, 6].map(n => T(n, [], 1))
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: frei, plaetze: 2 }, { planer: nr => (nr <= 3 ? OHNE : undefined), planerDauer: nr => (nr <= 3 ? 5 : 60) })
soll(e.status === 'abgebrochen', 'status ' + e.status)
soll(!planerVon(p, 5) && !planerVon(p, 6) && JSON.stringify(e.offen) === '[5,6]', 'offen ' + JSON.stringify(e.offen))
return fehler
})
for (const [name, aendern, text] of [
['Voraussetzung ins Leere', a => ({ ...a, tasks: [T(1, [], 1), T(2, [99], 2)] }), /Voraussetzung 99 gibt es nicht/],
['Zyklus', a => ({ ...a, tasks: [T(1, [2], 1), T(2, [1], 2)] }), /Zyklus/],
['Selbstbezug', a => ({ ...a, tasks: [T(1, [1], 1)] }), /Zyklus/],
['doppelte Nummer', a => ({ ...a, tasks: [T(1, [], 1), T(1, [], 1)] }), /doppelt in tasks/],
['doppelte Datei', a => ({ ...a, tasks: [T(1, [], 1), { ...T(2, [], 1), datei: T(1, [], 1).datei }] }), /bei zwei Tasks/],
['doppelte Voraussetzung', a => ({ ...a, tasks: [T(1, [], 1), T(2, [1, 1], 2)] }), /Voraussetzung steht doppelt/],
['modell unbekannt', a => ({ ...a, tasks: [{ ...T(1, [], 1), modell: 'gpt' }] }), /modell "gpt"/],
['erledigt ins Leere', a => ({ ...a, erledigt: [99] }), /erledigt muss/],
['plaetze 5', a => ({ ...a, plaetze: 5 }), /plaetze muss/],
['nachlesen_alle 0', a => ({ ...a, nachlesen_alle: 0 }), /nachlesen_alle muss/],
['tasks leer', a => ({ ...a, tasks: [] }), /nicht leere Liste/],
['nr als Text', a => ({ ...a, tasks: [{ ...T(1, [], 1), nr: '1' }] }), /nr muss/],
['voraussetzungen fehlen', a => ({ ...a, tasks: [{ ...T(1, [], 1), voraussetzungen: undefined }] }), /voraussetzungen muss/],
['gemeinsam fehlt', a => ({ ...a, gemeinsam: '' }), /args fehlen: gemeinsam/],
['effort ungültig', a => ({ ...a, effort: 'mittel' }), /effort "mittel" ungültig/],
]) {
fall('planung', `${name} wirft vor dem ersten Agenten`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PL(aendern(PL_ARGS), {})
soll(f && text.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
fall('planung', 'Auftrag nennt die Voraussetzungen und ersetzt alle Platzhalter', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL(PL_ARGS, {})
const t5 = planerVon(p, 5).prompt
soll(t5.includes('GEMEINSAM') && t5.includes('task-02-t.md') && t5.includes('task-03-t.md') && t5.includes('D:/x/plan/task-05-t.md'), 'Auftrag Task 5 unvollständig')
soll(!/\{(NR|THEMA|DATEI|MODELL|WELLE|VORAUSSETZUNGEN)\}/.test(t5), 'Platzhalter übrig')
soll(/Lies sie, bevor du einen Namen, eine Signatur oder einen Import daraus übernimmst/.test(t5), 'Lesehinweis fehlt')
soll(!/Lies sie/.test(planerVon(p, 1).prompt), 'Lesehinweis ohne Voraussetzungen')
soll(/vor: \(keine\)/.test(planerVon(p, 1).prompt), 'Task 1 ohne "(keine)"')
return fehler
})
fall('planung', 'effort medium: alle Planer auf medium, Ableser unverändert', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PL({ ...PL_ARGS, effort: 'medium' }, {})
soll(f === null && e.status === 'fertig', 'Fehler/status: ' + f + ' ' + (e && e.status))
const pl = p.agents.filter(x => x.label.startsWith('planer:'))
soll(pl.length === 7 && pl.every(x => x.effort === 'medium' && x.agentType === 'planer' && x.modell === 'opus'), 'Planer: ' + JSON.stringify(pl.map(x => [x.effort, x.agentType])))
soll(mitLabel(p, 'verbrauch:').every(x => x.effort === undefined), 'Ableser mit effort')
soll(e.effort === 'medium', 'Rückgabe effort ' + e.effort)
return fehler
})
fall('planung', 'ohne effort: Planer ohne effort, Rückgabe nennt das Frontmatter', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, {})
soll(p.agents.filter(x => x.label.startsWith('planer:')).every(x => x.effort === undefined), 'Planer mit effort')
soll(e.effort === '(Frontmatter)', 'Rückgabe effort ' + e.effort)
return fehler
})
fall('planung', 'alles erledigt: kein Agent, keine Ablesung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, erledigt: DAG.map(t => t.nr) }, {})
soll(p.agents.length === 0 && e.status === 'fertig' && e.verbrauch.length === 0, JSON.stringify([p.agents.length, e.status]))
return fehler
})
fall('planung', 'Ablesung ohne Ergebnis: höchstens zwei gleichzeitig', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, { ablesungen: [OHNE, OHNE] })
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
return fehler
})
fall('planung', 'als Kind: Rückfall auf zwei, Planung läuft durch', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, {}, { tiefe: 1 })
soll(f === null && e.status === 'fertig', 'Fehler/status: ' + f + ' ' + (e && e.status))
soll(spitze(p, '▸ planung', 'opus') === 2, 'gleichzeitig: ' + spitze(p, '▸ planung', 'opus'))
return fehler
})
// ---------------------------------------------------------------- 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 || []
// Eine Liste statt eines Textes sind die laeufe selbst, etwa auf mehrere laeufe verteilt.
const lauf = (label, text) => ok({ laeufe: Array.isArray(text) ? text : [{ 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 === 'haiku' && erst.effort === 'low' && !erst.agentType,'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 === 'haiku' && 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/],
['Task-Antwort mit jobId, aber EXIT task 1', { codexStart: Q_START.replace('EXIT task: 0', 'EXIT task: 1') }, /^Codex-Auftrag nicht gestartet$/],
['Status gescheitert, task nicht gelaufen: Grund ohne Warnung', { codexStart: 'Fehler beim Lesen\nEXIT status: 1' }, /^Status vor dem Lauf nicht gelesen$/],
['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', 'Modell je Hilfslabel: paket und alle codex-Labels haiku/low ohne agentType, zusammenfuehren sonnet/medium', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await QR(Q_ARGS, {})
soll(f === null, 'Fehler: ' + f)
for (const l of ['werkzeug:paket', 'codex:start', 'codex:warten:1', 'codex:ende']) {
const a = mitLabel(p, l)[0]
soll(a && a.modell === 'haiku' && a.effort === 'low' && !a.agentType, l + ' ' + JSON.stringify(a && [a.modell, a.effort, a.agentType]))
}
const zf = mitLabel(p, 'zusammenfuehren')[0]
soll(zf && zf.modell === 'sonnet' && zf.effort === 'medium' && !zf.agentType, 'zusammenfuehren ' + JSON.stringify(zf && [zf.modell, zf.effort, zf.agentType]))
return fehler
})
fall('quorum', 'Modell codex:abbruch: haiku/low ohne agentType', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await QR({ ...Q_ARGS, codex_runden: 1 }, { codexWarten: () => Q_WARTEN_OFFEN })
const ab = mitLabel(p, 'codex:abbruch')[0]
soll(ab && ab.modell === 'haiku' && ab.effort === 'low' && !ab.agentType, 'codex:abbruch ' + JSON.stringify(ab && [ab.modell, ab.effort, ab.agentType]))
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
})
const qStartLaeufeVertauscht = text => {
const [status, task] = text.split('\nEXIT status: 0\n')
return [
{ befehl: 'Startbefehl, Ausgabe von task', exit: 0, ausgabe: task },
{ befehl: 'Startbefehl, Ausgabe von status', exit: 0, ausgabe: status + '\nEXIT status: 0' },
]
}
const qStartZeilenLaeufe = zeilen => zeilen.map((ausgabe, i) => ({
befehl: 'Startbefehl, Ausgabe Teil ' + (i + 1), exit: 0, ausgabe,
}))
const qStartNeueIdZeile = ' "jobId": "task-q1",'
const qStartZeilen = Q_START_ALT.split('\n')
const qStartOhneNeueId = Q_START_ALT.replace(qStartNeueIdZeile + '\n', '')
const qStartStatusRoh = Q_START_ALT.split('\nEXIT status: 0\n')[0]
const qStartTaskBlock = Q_START.split('\nEXIT status: 0\n')[1].replace('\nEXIT task: 0', '')
const qStartTaskRoh = qStartTaskBlock + '\nEXIT task: 0'
const qStartTaskKompakt = '{"jobId":"task-q1","status":"queued","title":"Codex Task","summary":".","logFile":"C:/tmp/q/codex.log"}'
const qStartReihenfolgeHinweis = 'Codex-Start: Ausgabe nicht in Befehlsreihenfolge; jobId aus der task-Ausgabe (task-q1)'
const qStartUnsicherGrund = 'jobId nicht sicher bestimmbar; Codex-Auftrag läuft womöglich ungesehen weiter'
const qStartTaskGeteilt = [
{ befehl: 'Startbefehl, Status und Task-Anfang', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0\n{' },
{ befehl: 'Startbefehl, Task-Fortsetzung', exit: 0, ausgabe: qStartTaskRoh.slice(2) },
]
const qStartAlterRest = ausgabe => [
...qStartLaeufeVertauscht(qStartOhneNeueId.replace(' "jobId": "task-alt"\n', '')),
{ befehl: 'Startbefehl, verschobener Statusrest', exit: 0, ausgabe },
]
const qStartAlterJob = (id, status) => ({
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:01:00.000Z',
id,
status,
request: {
cwd: 'D:/x',
model: 'codex',
effort: 'low',
prompt: 'früherer Auftrag',
write: false,
resumeLast: false,
jobId: id
},
})
const qStartStatusKompakt = JSON.stringify({
workspaceRoot: 'D:/x',
config: { stopReviewGate: false },
sessionRuntime: {},
running: [qStartAlterJob('task-alt-queued', 'queued')],
latestFinished: {
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:01:00.000Z',
id: 'task-alt-ohne-request',
status: 'completed',
},
recent: [],
needsReview: false,
})
const qStartStatusUmformatiert = qStartStatusKompakt.replace(
'"jobId":"task-alt-queued"',
'\n "jobId": "task-alt-queued"\n'
)
const qStartK4Status = 'EXIT status: 0\n' + qStartStatusUmformatiert
const qStartK7StatusZeilen = qStartStatusKompakt
.replace(/^\{/, '{\n')
.replace('"id":"task-alt-queued","status":"queued",', '"id":"task-alt-queued"\n,\n"status": "queued",\n')
.replace('"jobId":"task-alt-queued"', '\n "jobId": "task-alt-queued"\n')
.split('\n')
const qStartK7Laeufe = qStartZeilenLaeufe([
qStartK7StatusZeilen[0],
qStartK7StatusZeilen[5],
qStartK7StatusZeilen[2],
qStartK7StatusZeilen[3],
qStartK7StatusZeilen[1],
qStartK7StatusZeilen[4],
qStartK7StatusZeilen[6],
'EXIT status: 0',
'EXIT task: 0',
])
const qStartStatusLang = JSON.stringify({
workspaceRoot: 'D:/x',
config: { stopReviewGate: false },
sessionRuntime: {},
running: Array.from({ length: 24 }, (_, i) => qStartAlterJob('task-alt-' + (i + 1), 'queued')),
latestFinished: qStartAlterJob('task-alt-fertig', 'completed'),
recent: [],
needsReview: false,
}, null, 2)
const qStartLangeZeilen = (qStartStatusLang + '\nEXIT status: 0\n' + qStartTaskRoh).split('\n')
const qStartGekuerzteZeilen = [
...qStartLangeZeilen.slice(0, 60),
'[... ' + (qStartLangeZeilen.length - 180) + ' Zeilen gekürzt ...]',
...qStartLangeZeilen.slice(-120),
]
const qStartGekuerzt = qStartGekuerzteZeilen.join('\n')
const qStartB = 'STATUS (EXIT status: 0): workspaceRoot D:/x; config.stopReviewGate=false; running=[]; latestFinished: id task-alt\nTASK (EXIT task: 0): ' + qStartTaskKompakt + '\nEXIT task: 0'
const qStartC = 'EXIT status: 0\nTASK-START: ' + qStartTaskKompakt + '\nEXIT task: 0'
const qStartKompaktMitStatus = qStartStatusKompakt + '\nEXIT status: 0\nTASK-START: ' + qStartTaskKompakt + '\nEXIT task: 0'
for (const [name, codexStart, hinweisErwartet] of [
['task vor status, ohne früheren Job', qStartLaeufeVertauscht(Q_START), true],
['task vor status, mit früherem Job', qStartLaeufeVertauscht(Q_START_ALT), true],
['Task-Block vor Status-JSON, EXIT-Zeilen in Befehlsreihenfolge', [
{ befehl: 'Startbefehl, Task-Block', exit: 0, ausgabe: qStartTaskBlock },
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
{ befehl: 'Startbefehl, Task-Exit', exit: 0, ausgabe: 'EXIT task: 0' },
], true],
['Task-Block hinter EXIT task', [
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
{ befehl: 'Startbefehl, Task-Exit', exit: 0, ausgabe: 'EXIT task: 0' },
{ befehl: 'Startbefehl, Task-Block', exit: 0, ausgabe: qStartTaskBlock },
], true],
['Mittelkürzung nach 60/120-Regel, danach task vor status', qStartLaeufeVertauscht(qStartGekuerzt), true],
['Echtlauf B, kompakt mit TASK-Vorsatz und Status als Prosa', qStartB, false],
['Echtlauf C, kompakt mit TASK-START und ohne Status-JSON', qStartC, false],
['Echtlauf A, EXIT status vor Status-JSON', 'EXIT status: 0\n' + Q_START_ALT.replace('\nEXIT status: 0', ''), false],
['dieselbe ID kompakt vor status und formatiert danach', [
{ befehl: 'Startbefehl, kompakter Task-Treffer', exit: 0, ausgabe: qStartTaskKompakt },
{ befehl: 'Startbefehl, Status und formatierter Task-Treffer', exit: 0, ausgabe: Q_START_ALT },
], true],
['K4: neuformatierter Status mit alter Request-Zeile und Task-Antwort', qStartK4Status + '\n' + qStartTaskRoh, false],
['synthetisch: K7-Statusreste mit intakter Task-Antwort in eigenem Lauf', [
...qStartK7Laeufe,
{ befehl: 'Startbefehl, intakte Task-Antwort', exit: 0, ausgabe: qStartTaskBlock },
], true],
['geordnet: Status mit EXIT in einem Lauf, Task mit EXIT im nächsten', [
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
{ befehl: 'Startbefehl, Task mit EXIT', exit: 0, ausgabe: qStartTaskRoh },
], false],
['synthetisch: 15 leere Läufe vor Status und Task', [
...Array.from({ length: 15 }, (_, i) => ({ befehl: 'Startbefehl, leerer Lauf ' + (i + 1), exit: 0, ausgabe: '' })),
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
{ befehl: 'Startbefehl, Task mit EXIT', exit: 0, ausgabe: qStartTaskRoh },
], false],
['synthetisch: Vorsatz mit EXIT status vor dem Task-Block, Status-JSON danach', [
{ befehl: 'Startbefehl, Vorsatz und Task-Block', exit: 0, ausgabe: 'Vorab (EXIT status: 0): ' + qStartTaskBlock },
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
{ befehl: 'Startbefehl, Task-Exit', exit: 0, ausgabe: 'EXIT task: 0' },
], true],
]) {
fall('quorum', 'Codex-Start (' + name + '): neue Stimme und passende Hinweise', async () => {
const { fehler, soll } = pruefe()
if (name === 'Mittelkürzung nach 60/120-Regel, danach task vor status') {
soll(qStartStatusLang.split('\n').length > 200, 'Statusvorlage hat nicht mehr als 200 Zeilen')
soll(qStartGekuerzteZeilen.length === 181, 'Kürzung enthält nicht 60 + 1 + 120 Zeilen')
soll(qStartGekuerzteZeilen[60] === '[... ' + (qStartLangeZeilen.length - 180) + ' Zeilen gekürzt ...]', 'Kürzungsmarker oder Zeilenzahl falsch')
soll(qStartGekuerzteZeilen.includes(qStartNeueIdZeile), 'neue Task-ID ging bei der Kürzung verloren')
}
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { codexStart })
soll(f === null, 'Fehler: ' + f)
soll(e && e.stimmen.codex.abgegeben === true && e.stimmen.codex.job === 'task-q1' && e.stimmen.codex.grund === '', 'Codex-Stimme ' + JSON.stringify(e && e.stimmen.codex))
const warten = mitLabel(p, 'codex:warten:')
const ende = mitLabel(p, 'codex:ende')
soll(warten.length === 1 && warten[0].prompt.includes("status 'task-q1' --wait"), 'Warten nicht genau einmal auf task-q1')
soll(ende.length === 1 && ende[0].prompt.includes("result 'task-q1' --json"), 'Auswertung nicht genau einmal für task-q1')
soll(mitLabel(p, 'stimme:haiku').length === 0 && mitLabel(p, 'codex:abbruch').length === 0, 'Haiku oder Abbruch trotz fertigem neuen Auftrag')
soll(p.agents.every(x => !String(x.prompt).includes('task-alt')), 'ein Auftrag nennt einen früheren Job')
soll(p.logs.filter(text => text === qStartReihenfolgeHinweis).length === Number(hinweisErwartet), 'Reihenfolgehinweis im Protokoll falsch')
soll(e && e.hinweise.includes(qStartReihenfolgeHinweis) === hinweisErwartet && e.bericht_md.includes(qStartReihenfolgeHinweis) === hinweisErwartet, 'Reihenfolgehinweis in Hinweisen oder Bericht falsch')
soll(e && !e.hinweise.some(text => text.includes(qStartUnsicherGrund)) && !e.bericht_md.includes(qStartUnsicherGrund), 'Warnung trotz eindeutiger neuer ID')
return fehler
})
}
for (const [name, codexStart, hinweisErwartet] of [
['vertauscht', qStartLaeufeVertauscht(Q_START_ALT), true],
['kompakt mit früherem queued-Job', qStartKompaktMitStatus, false],
]) {
fall('quorum', 'Codex-Start (' + name + '): neuer Auftrag wird nach zwei Warterunden abgebrochen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await QR({ ...Q_ARGS, codex_runden: 2 }, {
codexStart,
codexWarten: () => Q_WARTEN_OFFEN,
})
soll(f === null, 'Fehler: ' + f)
const warten = mitLabel(p, 'codex:warten:')
const abbruch = mitLabel(p, 'codex:abbruch')
soll(warten.length === 2 && warten.every(x => x.prompt.includes("status 'task-q1' --wait")), 'nicht zweimal auf den neuen Auftrag gewartet')
soll(abbruch.length === 1 && abbruch[0].prompt.includes("cancel 'task-q1' --json"), 'neuer Auftrag nicht genau einmal abgebrochen')
soll(e && e.stimmen.codex.job === 'task-q1' && e.stimmen.codex.abgegeben === false && /nach 2 Warterunden/.test(e.stimmen.codex.grund), 'Codex-Ergebnis ' + JSON.stringify(e && e.stimmen.codex))
soll(mitLabel(p, 'stimme:haiku').length === 1 && mitLabel(p, 'codex:ende').length === 0, 'Ersatz oder Auswertung nach Zeitüberschreitung falsch')
soll(p.agents.every(x => !String(x.prompt).includes('task-alt')), 'Warten oder Abbruch nennt einen früheren Job')
soll(p.logs.filter(text => text === qStartReihenfolgeHinweis).length === Number(hinweisErwartet), 'Reihenfolgehinweis vor dem Warten falsch')
soll(e && e.hinweise.includes(qStartReihenfolgeHinweis) === hinweisErwartet && e.bericht_md.includes(qStartReihenfolgeHinweis) === hinweisErwartet, 'Reihenfolgehinweis im Ergebnis falsch')
return fehler
})
}
for (const [name, codexStart, grund] of [
[
'Task-Nutzlast in umgekehrte Einzelzeilen zerlegt',
qStartZeilenLaeufe(qStartZeilen.slice().reverse()),
qStartUnsicherGrund,
],
[
'keine task-ID',
qStartLaeufeVertauscht(Q_START.replace(qStartNeueIdZeile + '\n', '')),
qStartUnsicherGrund,
],
[
'nur alte Status-ID',
qStartLaeufeVertauscht(qStartOhneNeueId),
qStartUnsicherGrund,
],
[
'alte Status-ID hinter EXIT status',
qStartAlterRest(' "jobId": "task-alt"'),
qStartUnsicherGrund,
],
[
'unvollständige Rückgabe ohne Status und task-ID',
[
{ befehl: 'Startbefehl, Ausgaberest', exit: 0, ausgabe: '[... 250 Zeilen gekürzt ...]\nEXIT task: 0' },
],
'Status vor dem Lauf nicht gelesen; Codex-Auftrag läuft womöglich ungesehen weiter',
],
[
'kompakter Status mit früherem queued-Job, ohne Task-Antwort',
qStartStatusKompakt + '\nEXIT status: 0\nEXIT task: 0',
qStartUnsicherGrund,
],
[
'synthetisch: alte Request-ID mit Komma vor queued, ohne Objektöffnung',
qStartAlterRest(' "jobId": "task-alt",\n "status": "queued"'),
qStartUnsicherGrund,
],
[
'synthetisch: alte Request-ID ohne Komma vor queued, mit Objektöffnung',
qStartAlterRest('{\n "jobId": "task-alt"\n "status": "queued"\n}'),
qStartUnsicherGrund,
],
[
'zwei verschiedene IDs aus formatiertem und kompaktem Task-Objekt',
qStartLaeufeVertauscht(Q_START_ALT + '\n' + qStartTaskKompakt.replace('task-q1', 'task-q2')),
qStartUnsicherGrund,
],
[
'K4: neuformatierter Status mit alter Request-Zeile, ohne Task-Antwort',
qStartK4Status + '\nEXIT task: 0',
qStartUnsicherGrund,
],
[
'synthetisch: Task-Objektanfang mit completed statt queued',
'{\n "running": []\n}\nEXIT status: 0\n{"jobId":"task-alt","status":"completed","title":"synthetisch"}\nEXIT task: 0',
qStartUnsicherGrund,
],
[
'synthetisch: K7-Merkmal aus vier einzeiligen Läufen, nur Statusreste',
qStartK7Laeufe,
qStartUnsicherGrund,
],
[
'echte Task-Antwort am Objektanfang über zwei Läufe zerrissen',
qStartTaskGeteilt,
qStartUnsicherGrund,
],
]) {
fall('quorum', 'Codex-Start unsicher (' + name + '): Haiku und Warnung vor ungesehenem Auftrag', async () => {
const { fehler, soll } = pruefe()
if (name === 'synthetisch: K7-Merkmal aus vier einzeiligen Läufen, nur Statusreste') {
soll(qStartK7StatusZeilen.length === 7 && JSON.stringify(JSON.parse(qStartK7StatusZeilen.join('\n'))) === qStartStatusKompakt, 'K7-Statusvorlage ändert JSON-Bestandteile oder ihre Reihenfolge')
soll(qStartK7Laeufe.every(l => !l.ausgabe.includes('\n')), 'K7-Statusfragmente sind nicht einzeilige Läufe')
}
if (name === 'echte Task-Antwort am Objektanfang über zwei Läufe zerrissen') {
soll(qStartTaskGeteilt.map(l => l.ausgabe).join('\n') === Q_START_ALT, 'geteilte echte Task-Antwort weicht von Q_START_ALT ab')
}
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { codexStart })
soll(f === null, 'Fehler: ' + f)
soll(e && e.stimmen.codex.abgegeben === false && e.stimmen.codex.job === null && e.stimmen.codex.grund === grund, 'Codex-Ergebnis ' + JSON.stringify(e && e.stimmen.codex))
soll(mitLabel(p, 'codex:warten:').length === 0 && mitLabel(p, 'codex:abbruch').length === 0 && mitLabel(p, 'codex:ende').length === 0, 'Auftrag ohne sichere jobId verfolgt')
soll(mitLabel(p, 'stimme:haiku').length === 1, 'Haiku nicht genau einmal eingesprungen')
soll(p.agents.every(x => !String(x.prompt).includes('task-alt')), 'ein Auftrag nennt einen früheren Job')
soll(e && e.hinweise.some(text => text.includes(grund)) && e.bericht_md.includes(grund), 'Warnung vor ungesehenem Auftrag fehlt in Hinweisen oder Bericht')
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
})
// Alle laeufe in ausgabe() (Folgeauftrag 2026-11-02): Paket, Codex-Start und Warten, je eine Ausführung, deren
// Ausgabe der Hilfsagent auf zwei laeufe verteilt; die EXIT-Zeile steht jeweils im zweiten. Mit laeufe[0] allein
// galt das Paket als nicht gebaut und ein gestarteter Codex-Auftrag als nicht gestartet: Haiku sprang ein, der
// Auftrag lief ungesehen weiter.
fall('quorum', 'Paket auf zwei laeufe verteilt, EXIT paket im zweiten: Paket gebaut, Stimmen laufen', async () => {
const { fehler, soll } = pruefe()
const paketAusgabe = [
{ befehl: 'Paketbefehl, Ausgabe Teil 1', exit: 0, ausgabe: 'Paket in C:/tmp/q\nQUORUM_PAKET ' + JSON.stringify(qPaket()) },
{ befehl: 'Paketbefehl, Ausgabe Teil 2', exit: 0, ausgabe: 'EXIT paket: 0' },
]
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { paketAusgabe })
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig' && e.paket === 'C:/tmp/q', JSON.stringify(e && [e.status, e.paket]))
soll(mitLabel(p, 'stimme:opus').length === 1 && mitLabel(p, 'stimme:sonnet').length === 1, 'Stimmen: ' + JSON.stringify(p.agents.map(x => x.label)))
return fehler
})
fall('quorum', 'Codex-Start auf zwei laeufe verteilt, Status mit früherem Job: jobId aus task, Codex-Stimme gültig', async () => {
const { fehler, soll } = pruefe()
const codexStart = [
{ befehl: 'Startbefehl, Ausgabe von status', exit: 0, ausgabe: '{\n "running": [],\n "latestFinished": {\n "id": "task-alt",\n "status": "completed",\n "request": {\n "jobId": "task-alt"\n }\n }\n}\nEXIT status: 0' },
{ befehl: 'Startbefehl, Ausgabe von task', exit: 0, ausgabe: '{\n "jobId": "task-q1",\n "status": "queued"\n}\nEXIT task: 0' },
]
soll(codexStart.map(l => l.ausgabe).join('\n') === Q_START_ALT, 'Vorlage: die zwei laeufe sind nicht Q_START_ALT')
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexStart })
soll(e && e.stimmen.codex.abgegeben === true && e.stimmen.codex.job === 'task-q1', 'Codex-Stimme ' + JSON.stringify(e && [e.stimmen.codex.job, e.stimmen.codex.grund]))
soll(mitLabel(p, 'stimme:haiku').length === 0 && mitLabel(p, 'codex:abbruch').length === 0, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
const warten = mitLabel(p, 'codex:warten:1')[0]
soll(warten && warten.prompt.includes("status 'task-q1' --wait"), 'Warten nicht auf task-q1')
return fehler
})
fall('quorum', 'Warten auf zwei laeufe verteilt, EXIT warten im zweiten: fertig, kein Abbruch, Codex-Stimme gültig', async () => {
const { fehler, soll } = pruefe()
const codexWarten = () => [
{ befehl: 'Wartebefehl, Ausgabe Teil 1', exit: 0, ausgabe: '{\n "waitTimedOut": false\n}' },
{ befehl: 'Wartebefehl, Ausgabe Teil 2', exit: 0, ausgabe: 'EXIT warten: 0' },
]
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexWarten })
soll(mitLabel(p, 'codex:warten:').length === 1 && mitLabel(p, 'codex:abbruch').length === 0 && mitLabel(p, 'codex:ende').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
soll(e && e.stimmen.codex.abgegeben === true && mitLabel(p, 'stimme:haiku').length === 0, 'Codex-Stimme ' + JSON.stringify(e && [e.stimmen.codex.abgegeben, e.stimmen.codex.grund]))
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
})
}
// Klartext im Bericht (Nachbesserung Task 06): Stimmen, Nachprüfung und Hilfsagenten zitieren
// Wikilink, Einbettung, Tag und Abschnittslink. Q_KLAR ist derselbe Text, wie er im Bericht
// stehen muss: lesbar, aber ohne Link, Einbettung und Tag.
const Q_ZITATE = '[[a/b]] ![[c]] #needs-review [[#x]]'
const Q_KLAR = '\\[\\[a/b]] !\\[\\[c]] \\#needs-review \\[\\[\\#x]]'
const qZ = schwere => ({ datei: 'src/a.py', stelle: Q_ZITATE, schwere, aussage: Q_ZITATE, beleg: Q_ZITATE })
// Was Obsidian als Wikilink, Einbettung oder Tag lesen könnte. Der Tag zählt hier schon hinter
// jedem Zeichen außer Buchstabe, Ziffer, Unterstrich und Backslash, nicht erst nach Leerraum.
function qObsidian(text) {
const funde = []
if (/(^|[^!])\[\[/.test(text)) funde.push('Wikilink')
if (text.includes('![[')) funde.push('Einbettung')
if (/(^|[^\p{L}\p{N}_\\])#[^\s#]/u.test(text)) funde.push('Tag')
return funde
}
const qSauber = (soll, text) => { const f = qObsidian(text); soll(f.length === 0, 'Obsidian-Syntax im Bericht: ' + f.join(', ')) }
fall('quorum', 'Klartext im Bericht: Stelle, Aussage und Beleg einer Gruppe ohne Wikilink, Einbettung und Tag, README-Link bleibt', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await QR(Q_ARGS, { stimmen: { opus: [qZ('blockierend')], sonnet: [qZ('blockierend')], codex: [] } })
const md = (e && e.bericht_md) || ''
qSauber(soll, md)
for (const z of [
'### G1: blockierend, Mehrheit (Opus, Sonnet)\n\n`src/a.py`, Stelle ' + Q_KLAR + '\n',
'- O1 (Opus, blockierend, Stelle ' + Q_KLAR + '): ' + Q_KLAR + '\n Beleg: ' + Q_KLAR + '\n',
'\n\nProjekt: [README](../../README.md)\n\n',
]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
fall('quorum', 'Klartext im Bericht: Fundstelle und Begründung der Nachprüfung ohne Wikilink, Einbettung und Tag', async () => {
const { fehler, soll } = pruefe()
const stimmen = { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/b.py', '5', 'hinweis')], codex: [] }
const urteile = pr => ({ urteile: [...pr.matchAll(/^Befund ([A-Z]\d+):$/gm)].map(m => ({ id: m[1], urteil: 'bestaetigt', fundstelle: Q_ZITATE, begruendung: Q_ZITATE })) })
const { ergebnis: e } = await QR(Q_ARGS, { stimmen, urteile })
const md = (e && e.bericht_md) || ''
qSauber(soll, md)
const z = '\nNachprüfung: bestaetigt, ' + Q_KLAR + '. ' + Q_KLAR + '\n'
soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
fall('quorum', 'Klartext im Bericht: Unbelegt und Nicht prüfbar ohne Wikilink, Einbettung und Tag', async () => {
const { fehler, soll } = pruefe()
const stimmen = { opus: [{ ...qZ('wichtig'), beleg: '' }], sonnet: [qB('src/b.py', '5', 'hinweis')], codex: [] }
const { ergebnis: e } = await QR(Q_ARGS, { stimmen, nichtPruefbar: { sonnet: [Q_ZITATE] } })
const md = (e && e.bericht_md) || ''
qSauber(soll, md)
for (const z of [
'## Unbelegt\n\n- O1 (Opus, wichtig) `src/a.py`, Stelle ' + Q_KLAR + ': ' + Q_KLAR + '\n',
'## Nicht prüfbar\n\n- Sonnet: ' + Q_KLAR + '\n',
]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
fall('quorum', 'Klartext im Bericht: Hinweise des Laufs und Grund der Codex-Stimme ohne Wikilink, Einbettung und Tag', async () => {
const { fehler, soll } = pruefe()
const stimmen = { opus: [qB('src/a.py', '10', 'hinweis')], sonnet: [qB('src/b.py', '5', 'hinweis')] }
const k = { stimmen, paket: { hinweise: [Q_ZITATE] }, codexEnde: { job_status: Q_ZITATE }, gruppen: () => ({ gruppen: [{ ids: [Q_ZITATE], begruendung: 'zitiert' }] }) }
const { ergebnis: e } = await QR(Q_ARGS, k)
const md = (e && e.bericht_md) || ''
qSauber(soll, md)
for (const z of [
'| Codex | nein: Codex-Auftrag endete mit ' + Q_KLAR + ' |',
'## Hinweise des Laufs\n\n- ' + Q_KLAR + '\n',
'\n- Codex ohne gültige Stimme (Codex-Auftrag endete mit ' + Q_KLAR + '); Haiku ist eingesprungen\n',
'\n- Zusammenführen: unbekannte ID ' + Q_KLAR + ' verworfen\n',
]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
fall('quorum', 'Klartext im Bericht eines Kundendokuments: Hub-Zeile bleibt Wikilink, Frontmatter bleibt, zitierte Syntax nicht', async () => {
const { fehler, soll } = pruefe()
const K = 'D:/Claude/IT Workspace/Beispiel/abgabe'
const HUB = 'Übersicht: [[IT Workspace/Beispiel/abgabe/00 Übersicht|00 Übersicht.md]]'
const args = { anlass: 'kundendokument', ordner: K, dateien: ['kapitel/01.md'], uebersicht: '00 Übersicht.md' }
const paket = { anlass: 'kundendokument', art: 'doku', wurzel: K, basis: 'erstes Quorum', stand: 'sha256:' + 'c'.repeat(64), vorher: null, nummer: 1 }
const { ergebnis: e } = await QR(args, { paket, stimmen: { opus: [qZ('blockierend')], sonnet: [qZ('blockierend')], codex: [] } })
const md = (e && e.bericht_md) || ''
qSauber(soll, md.replace(HUB, ''))
soll(md.includes('\n\n' + HUB + '\n\n') && md.split('[[').length === 2, 'Hub-Zeile nicht genau einmal als Wikilink: ' + JSON.stringify(md.split('\n').filter(x => x.includes('Übersicht'))))
for (const z of ['---\ntype: note\n', '\ntags: [quorum]\n', '- O1 (Opus, blockierend, Stelle ' + Q_KLAR + '): ' + Q_KLAR + '\n']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
// ---------------------------------------------------------------- Lauf
let gescheitert = 0
const grundlinieGescheitert = new Set()
for (const f of faelle) {
let fehler
try { fehler = await f.lauf() } catch (e) { fehler = ['Absturz im Fall: ' + (e && e.stack || e)] }
if (f.name.startsWith('Grundlinie') && fehler.length) grundlinieGescheitert.add(f.workflow)
if (fehler.length) {
gescheitert++
console.log(`FEHLER ${f.workflow}: ${f.name}\n ${fehler.join('\n ')}`)
} else console.log(`OK ${f.workflow}: ${f.name}`)
}
for (const w of grundlinieGescheitert) console.log(`HINWEIS Grundlinie von ${w} gescheitert: Die übrigen Fälle dieses Workflows messen nichts.`)
rmSync(TMP, { recursive: true, force: true })
console.log(`${faelle.length} Fälle, ${gescheitert} gescheitert (Ordner ${ORDNER})`)
process.exit(gescheitert ? 1 : 0)