Files
claude-werkzeuge/.claude/workflows/welle-mit-pruefung.js
T
Pascal Michel ae3ec3ca89 Abgleich mit dem Vault-Stand 22761eb
11 Datei(en) geändert.
Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.
2026-11-03 03:00:04 +01:00

242 lines
14 KiB
JavaScript

export const meta = {
name: 'welle-mit-pruefung',
description: 'Schritt 3 und 4 in einem Aufruf: Kopie mit angewendetem Plan vor der Welle, executer-welle, Byte-Vergleich und Suite, danach pruefung mit den Rohbelegen; mit bis "vergleich" endet er vor der Prüfung',
whenToUse: 'Nach der Plankritik, sobald eine Welle mit disjunkten Zieldateien steht (AI/Regelwerk/codeworkflow.md, Schritte 3 und 4). Args: {projekt, planordner, tasks: [{datei, modell, effort?}], kopie? oder ohne: [relpfade, nicht leer], prosa?: [relpfade], suite?, kritisch?, abschluss?, effort?, version?, schwerpunkt?, rot_nachweis_erbracht?, zusatz?, werkzeuge?, bis?: "vergleich"}. Ohne kopie wird sie vor der Welle gebaut; ohne nennt dann .git und Geheimnisdateien. Mit bis "vergleich" kommt pruefung_args zurück: pruefung damit selbst starten und währenddessen die eigene Gegenprüfung fahren. Ersetzt die eigene Gegenprüfung des Koordinators nicht.',
phases: [
{ title: 'Kopie', detail: 'Hilfsagent: Stand vor der Welle kopieren und den Plan der Welle anwenden' },
{ title: 'Welle', detail: 'executer-welle' },
{ title: 'Vergleich', detail: 'Hilfsagent: welle_vergleich.py und Suite im echten Baum' },
{ title: 'Prüfung', detail: 'pruefung mit den Rohbelegen' },
],
}
// Ruft die gespeicherten Workflows executer-welle und pruefung verschachtelt auf, damit ihre
// Aufträge und Rollenparameter an genau einer Stelle stehen. Der Hilfsagent hat keinen
// agentType und bekommt seinen Effort deshalb mit (low). Opus läuft nur in den Kindern,
// und die laufen nacheinander; die Obergrenze von zwei hält executer-welle selbst.
// Modell: werkzeug:kopie läuft auf haiku, weil nur das Skript die Ausgabe liest. werkzeug:vergleich
// bleibt auf sonnet: seine Rohausgabe geht an den pruefer.
// Verbrauch: Vor und nach der Prüfung liest dieser Workflow den Planverbrauch über verbrauch.js
// ab (nur messen); das Kind pruefung bekommt verbrauch: false, weil ein Kind keinen weiteren
// Workflow aufrufen kann.
// Planbefund (seit 2026-11-02): Meldet plan_materialisiere.py in der Phase Kopie einen Befund, also
// eine Zeile "BEFUND <taskdatei>:<zeile>:" (Exit 2, nach einem Absturz auch 1), startet keine Welle.
// Der Lauf endet vor executer-welle mit status plan_befund und den Zeilen in plan_befunde, ohne
// Vergleich, Prüfung und Ablesung. Vorher lief die Welle trotzdem, nur ohne Byte-Vergleich. Exit 2
// ohne eine solche Zeile ist ein gescheitertes Werkzeug (python endet auch bei "can't open file"
// mit 2) und nimmt den alten Weg, ebenso die BEFUND-Zeilen von kopie_bauen.py: Sie tragen keine
// Zeilennummer.
const MODELLE = ['haiku', 'sonnet', 'opus']
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
// Befundzeile der Planwerkzeuge: "BEFUND <datei>:<zeile>:", bei plan_zeichen "<datei>:<zeile>:<spalte>:".
const BEFUNDZEILE = /^BEFUND [^\s:]+:\d+:/
const befundZeilen = text => String(text || '').split('\n').map(z => z.replace(/\r$/, '')).filter(z => BEFUNDZEILE.test(z))
// Ein Hilfsagent darf seine Ausgabe auf mehrere laeufe verteilen (Echtlauf wf_9ab7fd8e-ad1 am
// 2026-11-02: je Werkzeug ein Lauf). Jede Ausgabe eines Hilfsagenten wird deshalb über alle laeufe
// gelesen, in ihrer Reihenfolge und mit Zeilenumbruch verbunden: Befundzeilen, KOPIE-Zeile und
// angewendete Blöcke der Kopie, die Rohausgabe des Vergleichs. Sonst stünde ein Befund in laeufe[1],
// und die Welle liefe trotzdem.
const ausgabeAller = r => (r && Array.isArray(r.laeufe) ? r.laeufe : []).map(l => String((l && l.ausgabe) || '')).join('\n')
// Exit über alle laeufe: Der Befehl der Kopie ist eine Kette mit &&, sie endet nur mit 0, wenn jedes
// Glied mit 0 endet. Verwendbar ist die Kopie deshalb nur, wenn jeder Lauf Exit 0 hat; ein Lauf ohne
// Exit zählt nicht als 0 und steht im Log als undefined. Mit laeufe[0] allein galt ein erster Lauf
// mit 0 vor einem Absturz im zweiten als fertige Kopie.
const exitsAller = r => (r && Array.isArray(r.laeufe) ? r.laeufe : []).map(l => l && l.exit)
const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)
const tempHinweis = t => t ? `${t} bleibt liegen (Projektkopie ohne die Ausschlüsse); löschen, wenn der Diff nicht mehr gebraucht wird` : null
const LAUF = {
type: 'object',
properties: {
laeufe: { type: 'array', items: { type: 'object', properties: { befehl: { type: 'string' }, exit: { type: 'integer' }, ausgabe: { type: 'string' } }, required: ['befehl', 'exit', 'ausgabe'] } },
},
required: ['laeufe'],
}
// Dieselben Regeln wie in executer-welle, aber VOR dem ersten Agenten: Ein ungültiger Task
// soll keine Projektkopie im Temp hinterlassen.
function pruefeArgs(a) {
if (!a || typeof a !== 'object') throw new Error('welle-mit-pruefung: args fehlen')
const fehlt = ['projekt', 'planordner', 'tasks'].filter(k => !a[k])
if (fehlt.length) throw new Error('welle-mit-pruefung: args fehlen: ' + fehlt.join(', '))
if (!Array.isArray(a.tasks) || a.tasks.length === 0) throw new Error('welle-mit-pruefung: tasks muss eine nicht leere Liste von {datei, modell, effort?} sein')
a.tasks.forEach((t, i) => {
if (!t || typeof t !== 'object' || !t.datei || typeof t.datei !== 'string') throw new Error(`welle-mit-pruefung: tasks[${i}] ohne datei`)
if (!MODELLE.includes(t.modell)) throw new Error(`welle-mit-pruefung: ${t.datei}: modell "${t.modell}" nicht in ${MODELLE.join('/')}`)
if (t.effort !== undefined && !EFFORTS.includes(t.effort)) throw new Error(`welle-mit-pruefung: ${t.datei}: effort "${t.effort}" nicht in ${EFFORTS.join('/')}`)
})
if (a.effort !== undefined && !EFFORTS.includes(a.effort)) throw new Error('welle-mit-pruefung: effort "' + a.effort + '" ungültig')
if (a.prosa !== undefined && !Array.isArray(a.prosa)) throw new Error('welle-mit-pruefung: prosa muss eine Liste sein')
if (a.bis !== undefined && a.bis !== 'vergleich') throw new Error('welle-mit-pruefung: bis kennt nur "vergleich"')
if (!a.kopie) {
const ok = Array.isArray(a.ohne) && a.ohne.length > 0 && a.ohne.every(x => typeof x === 'string' && x)
if (!ok) throw new Error('welle-mit-pruefung: kopie oder ohne angeben. ohne ist eine nicht leere Liste von Ausschlüssen für kopie_bauen.py, als Pfade vom Projektwurzelordner, mindestens .git (falls vorhanden) und Geheimnisdateien')
}
}
function werkzeugAgent(label, phase, befehl, zweck, modell = 'sonnet') {
return agent(`Du führst einen Werkzeugbefehl aus und gibst seine Ausgabe roh zurück. Zweck: ${zweck}
Führe genau diesen Befehl einmal im Bash-Werkzeug (Git-Bash) aus, unverändert:
${befehl}
Ändere keine Datei außer denen, die der Befehl selbst anlegt. Deute nichts und fasse nichts zusammen. Gib Befehl, Exit-Code und die vollständige Ausgabe zurück; bei mehr als 200 Zeilen die ersten 60 und die letzten 120, mit einer Zeile "[... N Zeilen gekürzt ...]" dazwischen.`,
{ label, phase, schema: LAUF, model: modell, effort: 'low' })
}
// Anfang und Ende behalten: Das Ergebnis der Suite steht am Schluss.
function kopfUndEnde(text, kopf, ende) {
if (!text || text.length <= kopf + ende) return text || ''
return text.slice(0, kopf) + '\n[... gekürzt ...]\n' + text.slice(-ende)
}
// Eine messende Hilfsfunktion ablesen() steht auch in pruefung.js; plankritik.js und planung.js
// haben eine mit Wunsch und Rückgabe. Die Regel (Fenster, Stufen, Rückfall) steht nur in
// verbrauch.js.
const ablesungen = []
async function ablesen(anlass) {
let r = null
try {
r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })
} catch (e) {
r = { status: 'nicht_gelesen', grund: 'verbrauch.js nicht ausführbar: ' + (e && e.message) }
}
ablesungen.push({ anlass, ...(r || { status: 'nicht_gelesen', grund: 'verbrauch.js ohne Ergebnis' }) })
}
pruefeArgs(args)
const W = args.werkzeuge || 'D:/Claude/.vault-tools/plan-kopie'
const taskdateien = args.tasks.map(t => t.datei)
let kopie = args.kopie || null
let temp = null
const belege = {}
const fehler = []
phase('Kopie')
if (!kopie) {
const befehl = `T=$(mktemp -d) && echo "KOPIE=$(cygpath -m "$T/mat")" && ${PY} ${q(W + '/kopie_bauen.py')} ${q(args.projekt)} "$T/mat" --ohne ${args.ohne.map(q).join(' ')} && ${PY} ${q(W + '/plan_materialisiere.py')} ${q(args.planordner)} "$T/mat" ${taskdateien.map(q).join(' ')}`
const r = await werkzeugAgent('werkzeug:kopie', 'Kopie', befehl, 'Stand vor der Welle kopieren und die Taskdateien der Welle darauf anwenden', 'haiku')
belege.kopie = r
const aus = ausgabeAller(r)
const m = /^KOPIE=(.+?)\s*$/m.exec(aus)
if (m) temp = m[1].replace(/\/mat$/, '')
const planBefunde = befundZeilen(aus)
if (planBefunde.length) {
const grund = `plan_materialisiere.py meldet ${planBefunde.length} Befund(e) (Exit ${exitsAller(r).map(String).join('/')}); die Welle startet nicht`
log('Kopie: ' + grund)
return {
status: 'plan_befund',
plan_befunde: planBefunde,
kopie: null,
temp_ordner: tempHinweis(temp),
welle: null,
vergleich: null,
kopie_lauf: r,
pruefung: { uebersprungen: grund },
pruefung_args: null,
pruefung_grund: grund,
fehler,
verbrauch: ablesungen,
naechster_schritt: 'Den Plan an den Zeilen in plan_befunde korrigieren und welle-mit-pruefung neu starten. Kein Executer ist gelaufen.' + (temp ? ' Die Kopie unter temp_ordner löschen.' : ''),
}
}
const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(aus)
if (alleExitNull(r) && m && angewendet) kopie = m[1]
else log('Kopie nicht verwendbar (' + (!exitsAller(r).length ? 'Hilfsagent ohne Ergebnis' : !alleExitNull(r) ? 'Exit ' + exitsAller(r).map(String).join('/') :!m ? 'kein KOPIE-Pfad' : 'kein Block angewendet') + '); der Byte-Vergleich entfällt, die Welle läuft trotzdem')
}
phase('Welle')
let welle = null
try {
welle = await workflow('executer-welle', {
projekt: args.projekt, planordner: args.planordner, tasks: args.tasks,
rot_nachweis_erbracht: args.rot_nachweis_erbracht, zusatz: args.zusatz,
})
} catch (e) {
fehler.push('executer-welle: ' + (e && e.message))
log('executer-welle gescheitert: ' + (e && e.message))
}
phase('Vergleich')
if (welle) {
const teile = []
if (kopie) teile.push(`${PY} ${q(W + '/welle_vergleich.py')} --projekt ${q(args.projekt)} --mat ${q(kopie)}${args.prosa && args.prosa.length ? ' --prosa ' + args.prosa.map(q).join(' ') : ''} -- ${taskdateien.map(d => q(args.planordner + '/' + d)).join(' ')}; echo "EXIT welle_vergleich: $?"`)
if (args.suite) teile.push(`if cd ${q(args.projekt)}; then export COLUMNS=300; ${args.suite}; echo "EXIT suite: $?"; else echo "EXIT suite: cd ins Projekt gescheitert"; fi`)
if (teile.length) {
belege.vergleich = await werkzeugAgent('werkzeug:vergleich', 'Vergleich', teile.join('; '), 'Nach der Welle: geänderte Dateien und Bytes gegen die Kopie, dann die Suite im echten Baum')
if (!belege.vergleich) log('Vergleich: Hilfsagent ohne Ergebnis')
} else log('Vergleich entfällt: weder Kopie noch suite')
} else log('Vergleich entfällt: keine Welle')
phase('Prüfung')
let pruefung = null
let pruefungGrund = null
if (!welle) pruefungGrund = 'keine Welle'
else if (welle.ohne_bericht && welle.ohne_bericht.length) pruefungGrund = 'Welle unvollständig, ohne Bericht: ' + welle.ohne_bericht.join(', ')
let pruefungArgs = null
if (!pruefungGrund) {
// Anfang und Ende des verbundenen Texts wie bei einem Lauf. Liefert der Hilfsagent die Suite vor
// einem langen Vergleich, fällt ihr Ergebnis heraus (benannte Lücke; der pruefer fährt die Suite selbst).
const vergleichstext = kopfUndEnde(ausgabeAller(belege.vergleich), 1500, 2500)
const schwerpunkt = [
args.schwerpunkt || '',
welle.mit_abweichung.length ? `Diese Tasks melden Abweichungen vom Plan: ${welle.mit_abweichung.join(', ')}.` : '',
vergleichstext ? `Rohausgabe von Byte-Vergleich und Suite nach der Welle (vom Hilfsagenten, nicht von dir gefahren):\n${vergleichstext}` : '',
].filter(Boolean).join('\n')
pruefungArgs = {
projekt: args.projekt, plan: args.planordner, tasks: taskdateien, suite: args.suite,
kritisch: args.kritisch, abschluss: args.abschluss, version: args.version, effort: args.effort, schwerpunkt,
}
}
const tempText = tempHinweis(temp)
if (args.bis === 'vergleich') {
if (pruefungGrund) log('Prüfung wäre übersprungen worden: ' + pruefungGrund)
return {
status: 'bis_vergleich',
kopie,
temp_ordner: tempText,
welle,
vergleich: belege.vergleich || null,
kopie_lauf: belege.kopie || null,
pruefung_args: pruefungArgs,
pruefung_grund: pruefungGrund,
fehler,
verbrauch: ablesungen,
naechster_schritt: pruefungArgs
? 'pruefung mit pruefung_args als eigenen Workflow im Hintergrund starten. Währenddessen die eigene Gegenprüfung nach Schritt 4, nur lesend: nichts in Projektbaum oder Planordner schreiben, Rot- und Mutantentests nur auf einer Kopie, den pruefer nicht abbrechen. Plankorrektur und Commit erst nach seinem Bericht.'
: 'Keine Prüfung möglich (' + pruefungGrund + '). Welle nachziehen, dann erneut.',
}
}
if (pruefungGrund) log('Prüfung übersprungen: ' + pruefungGrund)
else {
await ablesen('vor Prüfung')
try {
pruefung = await workflow('pruefung', { ...pruefungArgs, verbrauch: false })
} catch (e) {
fehler.push('pruefung: ' + (e && e.message))
pruefungGrund = 'pruefung gescheitert: ' + (e && e.message)
log(pruefungGrund)
}
await ablesen('nach Prüfung')
}
return {
status: pruefung ? 'fertig' : 'ohne_pruefung',
kopie,
temp_ordner: tempText,
welle,
vergleich: belege.vergleich || null,
kopie_lauf: belege.kopie || null,
pruefung: pruefung || { uebersprungen: pruefungGrund },
fehler,
verbrauch: ablesungen,
naechster_schritt: 'Eigene Gegenprüfung nach Schritt 4: Testlauf selbst wiederholen, zentrale Artefakte selbst lesen, eigener Lauf mit echten Daten; bei Abweichung im Byte-Vergleich diff auf die ganze Datei. Befunde am Plan korrigieren, nicht am Code.',
}