Files
claude-regelwerk/.vault-tools/workflow-pruefung/attrappe.mjs
T
Pascal MichelandClaude Opus 5 a2015144bb Automatischer Push: 11 Datei(en) geändert
- .claude/agents/verbrauch-ableser.md
- .claude/workflows/verbrauch.js
- .vault-tools/workflow-pruefung/LIESMICH.md
- .vault-tools/workflow-pruefung/attrappe.mjs
- .vault-tools/workflow-pruefung/docs/plans/2026-10-16-ableser-abweichung/00-uebersicht.md
- .vault-tools/workflow-pruefung/docs/plans/2026-10-16-ableser-abweichung/task-01-erkennung.md
- .vault-tools/workflow-pruefung/docs/plans/2026-10-16-ableser-abweichung/task-02-agententyp.md
- .vault-tools/workflow-pruefung/faelle_workflows.mjs
- .vault-tools/workflow-pruefung/mutanten_workflows.py
- AI/Regelwerk/haiku-5-5.md
- CLAUDE.md

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

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-18 04:00:01 +02:00

162 lines
8.5 KiB
JavaScript

// 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>", ab dem
// zweiten Aufruf desselben Kindes "▸ <meta.name> #<n>" (Bundle 2.1.280 und meta.json eines
// echten Laufs mit drei Ablesungen: "▸ verbrauch", "▸ verbrauch #2", "▸ verbrauch #3").
// - 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.
// - agent() mit einem agentType, den die Sitzung nicht kennt, wirft vor dem Start (CLI-Abbild
// 2.1.299). Bekannt sind die Agentendateien unter .claude/agents/ des Vaults oder optionen.agenten.
// 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, readdirSync } 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/'
// Agententypen wie in der Sitzung: die Agentendateien des Vaults neben den Workflows. Fehlt der
// Ordner, ist jeder agentType unbekannt und jeder Fall damit rot: ein Befund, kein Durchlass.
const AGENTEN_ORDNER = path.join(ECHTER_ORDNER, '..', 'agents')
function agentenDesVaults() {
if (!existsSync(AGENTEN_ORDNER)) return []
return readdirSync(AGENTEN_ORDNER).filter(n => n.endsWith('.md')).map(n => n.slice(0, -3))
}
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 agenten = optionen.agenten || agentenDesVaults()
const protokoll = { agents: [], logs: [], phasen: [], verstoesse: [], spitze: {}, spitzeJePhase: {}, workflows: [] }
const laufend = {}
const laufendJePhase = {}
const kindAufrufe = {}
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)
let kindPhase = null
if (tiefe >= 1) {
kindAufrufe[name] = (kindAufrufe[name] || 0) + 1
kindPhase = '▸ ' + name + (kindAufrufe[name] > 1 ? ' #' + kindAufrufe[name] : '')
}
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)
}
if (opts.agentType != null && !agenten.includes(String(opts.agentType))) throw new Error(`agent({agentType}): agent type '${opts.agentType}' not found. Available agents: ${agenten.join(', ')}`)
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 }
}
}