Files
claude-werkzeuge/.vault-tools/workflow-pruefung/docs/plans/2026-09-24-parallelitaet/task-06-attrappe-und-faelle.md
T
Pascal MichelandClaude Opus 5.5 fa1c26b407 Erste Fassung: Claude-Werkzeuge aus dem Vault
.vault-tools (ohne trading-plattform-pruefung und watermarks-remover),
.claude/agents, .claude/skills, .claude/workflows, .claude/settings.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 11:27:59 +02:00

63 KiB

type, title, status, summary, tags, created, updated, modell, effort, modell_begruendung
type title status summary tags created updated modell effort modell_begruendung
plan-task Task 06 — Attrappe und Fälle für die Workflows draft Node-Attrappe mit den Grenzen des Workflow-Laufzeitsystems und die Fälle für verbrauch, plankritik, pruefung, welle-mit-pruefung und planung samt Selbsttests der Attrappe.
plan-task
vault-tools
codeworkflow
2026-09-24 2026-09-24 sonnet low Abschrift: jede Zeile steht wörtlich im Plan, Zieldateien und Verbote sind genannt. Sonnet low statt Haiku, weil der Code das Zeichen · (U+00B7) trägt und Haiku in der Messung vom 2026-09-23 typografische Zeichen still ersetzt hat.

Task 06 — Attrappe und Fälle für die Workflows

Die globalen Vorgaben stehen in 00-uebersicht.md (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner D:/Claude; alle Pfade gelten von dort.

Was danach anders ist:

  • attrappe.mjs führt ein Workflow-Skript mit nachgebauten Globals aus: workflow() im Kind wirft, im Kind tragen Agenten die Phase ▸ <name>, Date.now() und Math.random() werfen, ein Agent ohne model ist ein Verstoß. Sie zählt gleichzeitige Agenten je Phase und Modell.
  • faelle_workflows.mjs fährt je Workflow zuerst die Grundlinie, dann die Angriffsliste nach AI/Regelwerk/pruefwerkzeuge.md, einschließlich gesenkter und gehobener Plätze beim Nachlesen.
  • mutanten_workflows.py fährt die Mutantenliste gegen die Workflows; ein veraltetes Muster bricht mit Exit 2 ab, ein wirkungsloser Mutant steht mit Grund in WIRKUNGSLOS.

Zieldateien

  • .vault-tools/workflow-pruefung/attrappe.mjs (neu)
  • .vault-tools/workflow-pruefung/faelle_workflows.mjs (neu)
  • .vault-tools/workflow-pruefung/mutanten_workflows.py (neu)

Verbote

  • Keine andere Datei, auch nicht die übrigen Workflows unter .claude/workflows/ und nichts unter AI/Regelwerk/, .claude/agents/ oder CLAUDE.md.
  • Kein Commit, kein Push, kein Lauf eines echten Workflows.
  • Kein Umformulieren, auch nicht von Kommentaren oder Sonderzeichen; ein Widerspruch zwischen Plan und Baum wird gemeldet.

Umsetzung

.vault-tools/workflow-pruefung/attrappe.mjs

Die ganze Datei ist neu:

// Attrappe für die Workflow-Skripte unter .claude/workflows/: führt ein Skript mit nachgebauten
// Globals aus und protokolliert jeden agent()-Aufruf mit Modell, Phase, Start und Ende.
//
// Grenzen wie im Ziel (Workflow-Referenz der Claude-Code-Sitzung vom 2026-09-24, dazu die
// Plankritik vom selben Tag am Laufzeitsystem 2.1.280):
// - workflow() in einem Kind wirft; Verschachtelung geht nur eine Ebene tief. Das Ziel meldet
//   "workflow() cannot be called from within a child workflow", dann einen Gedankenstrich und
//   "nesting is limited to one level"; die Attrappe setzt einen Doppelpunkt statt des Strichs.
// - Im Kind ist phase() wirkungslos, und jeder agent() bekommt die Phase "▸ <meta.name>".
// - workflow() mit unbekanntem Namen oder unlesbarem scriptPath wirft.
// - Date.now(), new Date() ohne Argument und Math.random() werfen.
// - agent() mit schema braucht type object und properties, required muss in properties stehen.
//   Ein Ergebnis ohne die required-Felder wird wie ein gescheiterter Agent zu null.
// - parallel() ist eine Barriere; ein werfender Thunk wird zu null.
// Zusätzlich als Verstoß protokolliert (Stamm, "Agenten & Modelle"): agent() ohne model.
// Was die Attrappe nicht nachbildet: die Obergrenze gleichzeitiger Agenten je Lauf, Budget,
// Resume und das Verhalten echter Modelle. Sie ist ein Fangnetz für die Steuerlogik der
// Skripte, kein Nachweis für einen echten Lauf.
import { readFileSync, existsSync } from 'node:fs'
import path from 'node:path'

const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
// Aufrufe über den echten Pfad werden in den Ordner unter Test umgeleitet.
const ECHTER_ORDNER = 'D:/Claude/.claude/workflows/'

class DatumAttrappe extends Date {
  constructor(...a) {
    if (a.length === 0) throw new Error('new Date() ohne Argument ist im Workflow nicht erlaubt')
    super(...a)
  }
  static now() { throw new Error('Date.now() ist im Workflow nicht erlaubt') }
}
const MatheAttrappe = Object.create(Math, {
  random: { value: () => { throw new Error('Math.random() ist im Workflow nicht erlaubt') } },
})

function lade(datei) {
  if (!existsSync(datei)) throw new Error('Workflow-Datei nicht lesbar: ' + datei)
  const text = readFileSync(datei, 'utf8')
  if (!/^export const meta = /m.test(text)) throw new Error(datei + ': export const meta fehlt')
  const name = /^export const meta = \{[\s\S]*?\bname:\s*'([^']+)'/m.exec(text)
  const quelle = text.replace(/^export const meta = /m, 'const meta = ')
  return { name: name ? name[1] : path.basename(datei, '.js'), fn: new AsyncFunction('agent', 'parallel', 'pipeline', 'phase', 'log', 'workflow', 'args', 'budget', 'Date', 'Math', quelle) }
}

const warte = ms => new Promise(r => setTimeout(r, ms))

// antworte(aufruf) liefert { wert, dauer } oder undefined (Agent ohne Ergebnis) oder wirft.
// aufruf trägt label, phase, modell, effort, agentType, prompt, tiefe und schema.
export async function fuehreAus(datei, args, antworte, optionen = {}) {
  const ordner = optionen.ordner || path.dirname(datei)
  const protokoll = { agents: [], logs: [], phasen: [], verstoesse: [], spitze: {}, spitzeJePhase: {}, workflows: [] }
  const laufend = {}
  const laufendJePhase = {}
  let uhr = 0

  function aufloesen(ref) {
    if (typeof ref === 'string') return path.join(ordner, ref + '.js')
    if (ref && typeof ref.scriptPath === 'string') {
      const s = ref.scriptPath.replace(/\\/g, '/')
      return s.startsWith(ECHTER_ORDNER) ? path.join(ordner, s.slice(ECHTER_ORDNER.length)) : s
    }
    throw new Error('workflow(): Name oder { scriptPath } erwartet')
  }

  async function lauf(datei, a, tiefe) {
    const { name, fn } = lade(datei)
    const kindPhase = tiefe >= 1 ? '▸ ' + name : null
    let aktuellePhase = null

    async function agent(prompt, opts = {}) {
      if (typeof prompt !== 'string' || !prompt.trim()) protokoll.verstoesse.push('agent() ohne Auftragstext: ' + (opts.label || '?'))
      if (!opts.model) protokoll.verstoesse.push('agent() ohne model: ' + (opts.label || '?'))
      if (opts.schema !== undefined) {
        if (!opts.schema || opts.schema.type !== 'object' || !opts.schema.properties) throw new Error('Schema braucht type object und properties: ' + opts.label)
        if ((opts.schema.required || []).some(k => !(k in opts.schema.properties))) throw new Error('required nicht in properties: ' + opts.label)
      }
      const modell = opts.model || '(erbt)'
      const phase = kindPhase || opts.phase || aktuellePhase || '(ohne)'
      const eintrag = { label: opts.label || null, phase, modell, effort: opts.effort, agentType: opts.agentType, prompt, tiefe, start: ++uhr, ende: null, ergebnis: undefined }
      protokoll.agents.push(eintrag)
      const k = phase + '|' + modell
      laufend[modell] = (laufend[modell] || 0) + 1
      laufendJePhase[k] = (laufendJePhase[k] || 0) + 1
      protokoll.spitze[modell] = Math.max(protokoll.spitze[modell] || 0, laufend[modell])
      protokoll.spitzeJePhase[k] = Math.max(protokoll.spitzeJePhase[k] || 0, laufendJePhase[k])
      let wert = null
      try {
        const antwort = await antworte({ ...eintrag, schema: opts.schema })
        await warte(antwort && typeof antwort.dauer === 'number' ? antwort.dauer : 5)
        wert = antwort && typeof antwort === 'object' && 'wert' in antwort ? antwort.wert : null
        if (wert !== null && opts.schema) {
          const fehlt = (opts.schema.required || []).filter(f => !wert || typeof wert !== 'object' || !(f in wert))
          if (fehlt.length) {
            protokoll.verstoesse.push(`${opts.label}: Ergebnis ohne ${fehlt.join(', ')}`)
            wert = null
          }
        }
      } catch (e) {
        protokoll.logs.push(`agent ${opts.label} geworfen: ${e && e.message}`)
        wert = null
      } finally {
        laufend[modell]--
        laufendJePhase[k]--
        eintrag.ende = ++uhr
      }
      eintrag.ergebnis = wert
      return wert
    }

    const parallel = thunks => Promise.all(thunks.map(t => Promise.resolve().then(t).catch(e => {
      protokoll.logs.push('parallel: ' + (e && e.message))
      return null
    })))
    const pipeline = (items, ...stufen) => Promise.all(items.map(async (item, i) => {
      try {
        let r = item
        for (const stufe of stufen) r = await stufe(r, item, i)
        return r
      } catch (e) {
        protokoll.logs.push('pipeline: ' + (e && e.message))
        return null
      }
    }))
    const phase = titel => { if (!kindPhase) { aktuellePhase = titel; protokoll.phasen.push(titel) } }
    const log = text => { protokoll.logs.push(String(text)) }
    const workflow = async (ref, kindArgs) => {
      if (tiefe >= 1) throw new Error('workflow() cannot be called from within a child workflow: nesting is limited to one level')
      const ziel = aufloesen(ref)
      protokoll.workflows.push({ ref: typeof ref === 'string' ? ref : ref.scriptPath, args: kindArgs, tiefe: tiefe + 1 })
      return lauf(ziel, kindArgs, tiefe + 1)
    }
    const budget = { total: null, spent: () => 0, remaining: () => Infinity }
    return fn(agent, parallel, pipeline, phase, log, workflow, a, budget, DatumAttrappe, MatheAttrappe)
  }

  try {
    const ergebnis = await lauf(datei, args, optionen.tiefe || 0)
    return { ergebnis, fehler: null, protokoll }
  } catch (e) {
    return { ergebnis: null, fehler: (e && e.message) || String(e), protokoll }
  }
}

.vault-tools/workflow-pruefung/faelle_workflows.mjs

Die ganze Datei ist neu:

// 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 } 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') return ok({ laeufe: [{ befehl: 'planwerkzeuge', exit: 0, ausgabe: 'keine Befunde' }] })
    if (label === 'werkzeug:kopie') return ok({ laeufe: [{ befehl: 'kopie', exit: 0, ausgabe: "KOPIE=C:/tmp/k/mat\n{'neu': 2}" }] })
    if (label === 'werkzeug:vergleich') return ok({ laeufe: [{ befehl: 'vergleich', exit: 0, ausgabe: 'VERGLEICH-OK\nEXIT suite: 0' }] })
    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', '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', '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])))
  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
})

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', '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', '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', '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
})

// ---------------------------------------------------------------- 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', '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 === 'fertig' && /unvollständig/.test(e.pruefung.uebersprungen), JSON.stringify(e.pruefung))
  soll(mitLabel(p, 'verbrauch:').length === 0 && mitLabel(p, 'pruefung').length === 0, 'Ablesung oder Prüfung trotz Lücke')
  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
  })
}

// ---------------------------------------------------------------- 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', '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/],
]) {
  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(/vor: \(keine\)/.test(planerVon(p, 1).prompt), 'Task 1 ohne "(keine)"')
  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
})

// ---------------------------------------------------------------- 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)

.vault-tools/workflow-pruefung/mutanten_workflows.py

Die ganze Datei ist neu:

"""Mutantenlauf gegen die Workflows unter .claude/workflows/: je Mutant genau eine Ersetzung, dann faelle_workflows.mjs.

Jede Ersetzung muss genau einmal vorkommen, sonst bricht der Lauf mit Exit 2 ab: Ein Muster, das
nicht mehr passt, ist ein veralteter Mutant und kein ueberlebter. Ein ueberlebter Mutant ist ein
Befund, ausser er steht in WIRKUNGSLOS mit Begruendung.
Aufruf: python mutanten_workflows.py [<ordner mit den workflows>]
Exit 0, wenn jeder wirksame Mutant gefangen wird, 1 bei einem Ueberlebenden, 2 bei einem veralteten Muster.
"""
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

HIER = Path(__file__).resolve().parent
FAELLE = HIER / "faelle_workflows.mjs"

# Mutanten, die bei den heutigen Stufen kein Verhalten aendern, mit Grund.
WIRKUNGSLOS = {
    "harte Obergrenze fehlt": "keine Stufe gibt mehr als vier Plaetze; wer die Stufen anhebt, braucht einen Fall",
}

MUTANTEN = [
    ("verbrauch", "Stufe 50 -> 60", "[[50, 4], [70, 2], [85, 1]]", "[[60, 4], [70, 2], [85, 1]]"),
    ("verbrauch", "Wochenstufe 85 -> 80", "[[70, 4], [85, 2], [95, 1]]", "[[70, 4], [80, 2], [95, 1]]"),
    ("verbrauch", "ueber 100 wieder Fehler", "|| p < 0) return", "|| p < 0 || p > 100) return"),
    ("verbrauch", "doppeltes Fenster: erstes gewinnt", "if (!e[art] || e[art].prozent < p) e[art] = neu", "if (!e[art]) e[art] = neu"),
    ("verbrauch", "Opus-Woche ignoriert", "Math.max(e.woche.prozent, e.opus ? e.opus.prozent : 0)", "e.woche.prozent"),
    ("verbrauch", "Opus-Woche ersetzt die Woche", "Math.max(e.woche.prozent, e.opus ? e.opus.prozent : 0)", "(e.opus ? e.opus.prozent : e.woche.prozent)"),
    ("verbrauch", "harte Obergrenze fehlt", "Math.min(wunsch, grenzeFuenf, grenzeWoche, HARTE_OBERGRENZE)", "Math.min(wunsch, grenzeFuenf, grenzeWoche)"),
    ("verbrauch", "gebremst auch bei wunsch 0", "const gebremst = wunsch > 0 && plaetze === 0", "const gebremst = plaetze === 0"),
    ("verbrauch", "Status ok nicht verlangt", " || ablesung.status !== 'ok'", ""),
    ("verbrauch", "Rueckfall 3 statt 2", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"),
    ("verbrauch", "wunsch-Pruefung fehlt", "if (!Number.isInteger(a.wunsch) || a.wunsch < 0) throw", "if (false) throw"),
    ("verbrauch", "fremdes Fenster mit Zahl geprueft", "if (!art) { e.andere.push(f.label); continue }\n    const p = f.percentUsed", "const p = f.percentUsed\n    if (!art) { if (typeof p !== 'number') return { fehler: 'x' }; e.andere.push(f.label); continue }"),
    ("plankritik", "Bremse vor Linsen fehlt", "if (vorLinsen.gebremst) {", "if (false) {"),
    ("plankritik", "Bremse vor Widerlegung fehlt", "if (vorWiderlegung.gebremst) {", "if (false) {"),
    ("plankritik", "Widerlegung bis vier", "const WIDERLEGUNG_HOECHSTENS = 2", "const WIDERLEGUNG_HOECHSTENS = 4"),
    ("plankritik", "Widerlegung ignoriert die Ablesung", "} else plaetzeWiderlegung = vorWiderlegung.plaetze", "} else plaetzeWiderlegung = WIDERLEGUNG_HOECHSTENS"),
    ("plankritik", "Linsen fest zwei", "await hoechstens(vorLinsen.plaetze, gewaehlt,", "await hoechstens(2, gewaehlt,"),
    ("plankritik", "Rueckfall ohne Ablesung 3", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"),
    ("plankritik", "Testort fehlt in der Widerlegung", "Du änderst keine Datei im Projekt und keine im Plan. ${testort(ctx)}\n- ", "Du änderst keine Datei im Projekt und keine im Plan.\n- "),
    ("plankritik", "Testort fehlt bei den Linsen", "Executer-Bericht, dessen Testlauf du wiederholen könntest. ${testort(ctx)}", "Executer-Bericht, dessen Testlauf du wiederholen könntest. ${linse === 'arithmetik' ? testort(ctx) : ''}"),
    ("plankritik", "keine Ablesung am Ende", "await ablesen('Ende', 0)\n", "\n"),
    ("plankritik", "gebremste Befunde ohne Hinweis", "'nicht geprüft (gebremst)'", "'nicht geprüft'"),
    ("plankritik", "gebremst auch unter der Schwelle", "gebremstVor && zuPruefen.includes(g) ?", "gebremstVor ?"),
    ("plankritik", "naechster_schritt uebergeht die Bremse", "naechster_schritt: gebremstVor ? ", "naechster_schritt: false ? "),
    ("plankritik", "zaehlung null bei Bremse vor Linsen", "zaehlung: { roh: 0, zusammengefasst: 0, bestaetigt: 0, offen: 0, widerlegt: 0, nicht_geprueft: 0 },", "zaehlung: null,"),
    ("pruefung", "verbrauch false wird uebergangen", "if (args.verbrauch === false) return\n", "\n"),
    ("pruefung", "Ablesung nach der Pruefung fehlt", "await ablesen('nach Prüfung')\n", "\n"),
    ("welle-mit-pruefung", "Kind liest selbst", "{ ...pruefungArgs, verbrauch: false }", "{ ...pruefungArgs }"),
    ("welle-mit-pruefung", "bis vergleich wirkt nicht", "if (args.bis === 'vergleich') {", "if (false) {"),
    ("welle-mit-pruefung", "Ablesung vor der Pruefung fehlt", "  await ablesen('vor Prüfung')\n", "\n"),
    ("planung", "Voraussetzung laufend genuegt", "['fertig', 'erledigt'].includes(status.get(n)))) starte(t)", "['fertig', 'erledigt', 'laeuft'].includes(status.get(n)))) starte(t)"),
    ("planung", "keine Blockade", "status.set(t.nr, 'blockiert')\n        geaendert = true", "geaendert = false"),
    ("planung", "kein Abbruch nach drei", "gescheitert.length >= ABBRUCH_NACH && !abgebrochen", "false"),
    ("planung", "Bremse beim Nachlesen fehlt", "if (v.gebremst) {\n      gebremst = true\n      log('Planung gebremst", "if (false) {\n      gebremst = true\n      log('Planung gebremst"),
    ("planung", "Nachlesen uebernimmt die Plaetze nicht", "log('Planung gebremst: kein weiterer Start, die laufenden Planer enden')\n    } else plaetze = v.plaetze", "log('Planung gebremst: kein weiterer Start, die laufenden Planer enden')\n    }"),
    ("planung", "Bremse am Anfang nicht gemeldet", "  if (v.gebremst) gebremst = true\n  else plaetze = v.plaetze", "  plaetze = v.plaetze"),
    ("planung", "Plaetze nicht begrenzt", "if (laufend.size >= plaetze) break", "if (laufend.size >= 99) break"),
    ("planung", "Reihenfolge unsortiert", "const reihenfolge = [...args.tasks].sort((x, y) => ((x.welle || 0) - (y.welle || 0)) || (x.nr - y.nr))", "const reihenfolge = [...args.tasks]"),
    ("planung", "Nachlesen ohne offene Tasks", "fertigSeitAblesung >= nachlesenAlle && nochOffen()", "fertigSeitAblesung >= nachlesenAlle"),
    ("planung", "fremde Datei zaehlt als fertig", "const r = passt ? roh : null", "const r = roh"),
    ("planung", "Vorgabe vier statt zwei", "const VORGABE_PLAETZE = 2", "const VORGABE_PLAETZE = 4"),
    ("planung", "Zyklenpruefung fehlt", "if (geschafft < a.tasks.length) throw", "if (false) throw"),
    ("planung", "erledigt ignoriert", "(args.erledigt || []).includes(t.nr) ? 'erledigt' : 'offen'", "'offen'"),
    ("planung", "Rueckfall ohne Ablesung 3", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"),
]


def main() -> int:
    ordner = Path(sys.argv[1]) if len(sys.argv) > 1 else HIER.parent.parent / ".claude" / "workflows"
    ueberlebt, veraltet = [], []
    for datei, name, alt, neu in MUTANTEN:
        with tempfile.TemporaryDirectory() as tmp:
            ziel = Path(tmp) / "wf"
            shutil.copytree(ordner, ziel)
            pfad = ziel / (datei + ".js")
            text = pfad.read_text(encoding="utf-8")
            if text.count(alt) != 1:
                veraltet.append(name)
                print(f"VERALTET  {datei}: {name} (Muster {text.count(alt)}-mal)")
                continue
            pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
            lauf = subprocess.run(["node", str(FAELLE), str(ziel)], capture_output=True, text=True, encoding="utf-8", errors="replace")
            rot = [z for z in lauf.stdout.splitlines() if z.startswith("FEHLER")]
            if lauf.returncode == 0:
                grund = WIRKUNGSLOS.get(name)
                if grund:
                    print(f"WIRKUNGSLOS {datei}: {name} ({grund})")
                else:
                    ueberlebt.append(name)
                    print(f"UEBERLEBT {datei}: {name}")
            else:
                print(f"GEFANGEN  {datei}: {name} ({len(rot)} rot, z. B. {rot[0][7:90] if rot else 'Absturz'})")
    print(f"{len(MUTANTEN)} Mutanten, {len(ueberlebt)} ueberlebt, {len(veraltet)} veraltet")
    return 2 if veraltet else 1 if ueberlebt else 0


if __name__ == "__main__":
    sys.stdout.reconfigure(errors="replace")
    sys.exit(main())

Verifikation

Im Vault-Wurzelordner:

node .vault-tools/workflow-pruefung/faelle_workflows.mjs

Erwartet: Alle übrigen Zeilen beginnen mit OK, die letzte enthält 0 gescheitert, Exit 0.

python .vault-tools/workflow-pruefung/mutanten_workflows.py

Erwartet: Die letzte Zeile enthält 0 ueberlebt, 0 veraltet, Exit 0. Der Lauf dauert einige Minuten.

Danach git status --porcelain -- .vault-tools/workflow-pruefung/attrappe.mjs .vault-tools/workflow-pruefung/faelle_workflows.mjs .vault-tools/workflow-pruefung/mutanten_workflows.py zeigen.