// 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 "▸ ", ab dem // zweiten Aufruf desselben Kindes "▸ #" (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. // 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 = {} 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) } 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 } } }