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]>
This commit is contained in:
Pascal MichelandClaude Opus 5.5 committed 2026-10-14 11:27:59 +02:00
commit fa1c26b407
137 files changed
+43926

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
export const meta = {
name: 'executer-welle',
description: 'Führt die Taskdateien einer Welle mit dem executer aus, Modell und Effort je Task aus dem Task-Frontmatter; gibt die Berichte für den Byte-Vergleich zurück',
whenToUse: 'Schritt 3 der Standardschleife (AI/Regelwerk/codeworkflow.md), sobald eine Welle mit disjunkten Zieldateien steht. Args: {projekt, planordner, tasks: [{datei, modell, effort?}], rot_nachweis_erbracht?, zusatz?}. modell und effort liest der Koordinator vorher aus dem Frontmatter der Taskdateien (grep "^modell:\\|^effort:"). Ein Opus-Task ohne effort läuft auf high.',
phases: [{ title: 'Welle', detail: 'ein executer je Taskdatei, Opus höchstens zwei gleichzeitig' }],
}
// Rollenparameter: model wird immer gesetzt (Stamm, "Agenten & Modelle"). Der Effort kommt
// aus dem Frontmatter von .claude/agents/executer.md (medium); ein effort im Task überschreibt
// ihn, etwa low für Abschrift mit typografischen Zeichen. Ein Opus-Task ist die Hochstufung
// nach zwei Fehlschlägen und läuft ohne Angabe auf high statt auf dem medium des Executers.
// Haiku kennt keinen Effort.
const MODELLE = ['haiku', 'sonnet', 'opus']
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
const OPUS_PARALLEL = 2
function pruefeArgs(a) {
if (!a || typeof a !== 'object') throw new Error('executer-welle: args fehlen')
const fehlt = ['projekt', 'planordner', 'tasks'].filter(k => !a[k])
if (fehlt.length) throw new Error('executer-welle: args fehlen: ' + fehlt.join(', '))
if (!Array.isArray(a.tasks) || a.tasks.length === 0) throw new Error('executer-welle: tasks muss eine nicht leere Liste von {datei, modell, effort?} sein')
a.tasks.forEach((t, i) => {
if (!t || typeof t !== 'object') throw new Error(`executer-welle: tasks[${i}] ist kein Objekt`)
if (!t.datei || typeof t.datei !== 'string') throw new Error(`executer-welle: tasks[${i}] ohne datei`)
if (!MODELLE.includes(t.modell)) throw new Error(`executer-welle: ${t.datei}: modell "${t.modell}" nicht in ${MODELLE.join('/')}`)
if (t.effort !== undefined && !EFFORTS.includes(t.effort)) throw new Error(`executer-welle: ${t.datei}: effort "${t.effort}" nicht in ${EFFORTS.join('/')}`)
})
}
// Höchstens n Aufgaben gleichzeitig, ohne Leerlauf zwischen Paaren.
async function hoechstens(n, items, fn) {
const ergebnisse = new Array(items.length).fill(null)
let naechster = 0
async function arbeiter() {
while (naechster < items.length) {
const i = naechster++
try { ergebnisse[i] = await fn(items[i], i) } catch (e) { log(`Aufgabe ${i + 1} abgebrochen: ${e && e.message}`) }
}
}
await Promise.all(Array.from({ length: Math.min(n, items.length) }, () => arbeiter()))
return ergebnisse
}
const BERICHT = {
type: 'object',
properties: {
erledigt: { type: 'array', items: { type: 'string' } },
geaenderte_dateien: { type: 'array', items: { type: 'string' } },
verifikation: { type: 'array', items: { type: 'object', properties: { befehl: { type: 'string' }, ausgabe: { type: 'string' } }, required: ['befehl', 'ausgabe'] } },
abweichungen: { type: 'array', items: { type: 'string' } },
annahmen: { type: 'array', items: { type: 'string' } },
beobachtet: { type: 'array', items: { type: 'string' } },
},
required: ['erledigt', 'geaenderte_dateien', 'verifikation', 'abweichungen', 'annahmen', 'beobachtet'],
}
function auftrag(a, t) {
const rot = a.rot_nachweis_erbracht
? 'Der Rot-Nachweis dieses Tasks ist mit rot_nachweis.py schon erbracht. Du musst keine Produktivdatei beiseitelegen.'
: 'Verlangt der Task erst den roten Test und dann die Umsetzung: Leg die fertige Produktivdatei kurz beiseite (den alten Stand etwa aus git show HEAD:<pfad>), beobachte und zitiere den roten Lauf und leg danach die Fassung aus dem Plan byte-gleich zurück. Schreib sie nie neu.'
return `Setze die Taskdatei ${a.planordner}/${t.datei} um. Arbeitsverzeichnis ist das Projekt ${a.projekt}.
Verweist die Taskdatei auf globale Vorgaben, stehen sie in ${a.planordner}/00-uebersicht.md.
Erlaubt sind genau die Dateien unter "## Zieldateien" der Taskdatei. Jede andere Datei ist verboten, ebenso git commit und git push.
Code, der im Plan wörtlich steht, übernimmst du byte-genau, auch Kommentare, Docstrings und Sonderzeichen.
Ein Widerspruch im Plan wird im Bericht unter abweichungen gemeldet und nicht still aufgelöst. Das gilt auch dann, wenn ein Prüfbefehl nur grün würde, weil du einen Kommentar umformulierst. Gibt es keine Abweichung, steht dort genau ein Eintrag "keine".
${rot}
Endet eine FAILED-Zeile von pytest auf "...", ist sie gekürzt: pytest kürzt ohne Terminal auf 80 Zeichen. Wiederhole den Befehl dann mit COLUMNS=300 davor, bevor du eine Fehlerart mit dem Plan vergleichst.
Ist das Projekt ein Git-Repo, zeig am Ende die Ausgabe von git status --porcelain -- <deine Zieldateien>.
${a.zusatz ? a.zusatz + '\n' : ''}Übertrage deinen Schlussbericht in die Felder des StructuredOutput-Werkzeugs.`
}
function effortFuer(t) {
if (t.modell === 'haiku') {
if (t.effort) log(`${t.datei}: effort ${t.effort} bei haiku wirkungslos, nicht gesetzt`)
return undefined
}
if (t.effort) return t.effort
return t.modell === 'opus' ? 'high' : undefined
}
function starte(a, t) {
const opts = { label: 'executer:' + t.datei.replace(/\.md$/, ''), phase: 'Welle', schema: BERICHT, agentType: 'executer', model: t.modell }
const effort = effortFuer(t)
if (effort) opts.effort = effort
return agent(auftrag(a, t), opts).then(r => {
if (!r) log(`${t.datei}: kein Bericht (Agent abgebrochen oder übersprungen)`)
return { datei: t.datei, modell: t.modell, effort: effort || '(Frontmatter)', bericht: r }
})
}
// "keine", "Keine.", "keine — alles wie beauftragt" zählen als keine Abweichung;
// "Keine Abweichung im Code, aber ..." und alles andere zählt als Abweichung.
function istKeine(x) {
return /^\s*keine(\s+abweichung(en)?)?\s*($|[.:;!—–-])/i.test(x) && !/\baber\b/i.test(x)
}
pruefeArgs(args)
phase('Welle')
const opus = args.tasks.filter(t => t.modell === 'opus')
const rest = args.tasks.filter(t => t.modell !== 'opus')
const [restErg, opusErg] = await Promise.all([
parallel(rest.map(t => () => starte(args, t))),
hoechstens(OPUS_PARALLEL, opus, t => starte(args, t)),
])
const berichte = [...restErg, ...opusErg].filter(Boolean)
const ohneBericht = args.tasks.map(t => t.datei).filter(d => !berichte.some(b => b.datei === d && b.bericht))
const mitAbweichung = berichte.filter(b => b.bericht && b.bericht.abweichungen.some(x => !istKeine(x))).map(b => b.datei)
if (ohneBericht.length) log('Ohne Bericht: ' + ohneBericht.join(', '))
return {
berichte,
ohne_bericht: ohneBericht,
mit_abweichung: mitAbweichung,
naechster_schritt: 'Ein Bericht ist eine Behauptung. Jetzt Byte-Vergleich gegen die Kopie mit angewendetem Plan (.vault-tools/plan-kopie/welle_vergleich.py) und Gegenprüfung nach Schritt 4.',
}
+393
View File
@@ -0,0 +1,393 @@
export const meta = {
name: 'plankritik',
description: 'Plankritik: pruefer-Linsen nach Verbrauch, doppelte Befunde zusammengefasst, wichtige und blockierende von einem zweiten pruefer widerlegt',
whenToUse: 'Schritt 2 der Standardschleife vor dem ersten Executer, Pflicht ab etwa drei Tasks (AI/Regelwerk/codeworkflow.md). Args: {projekt, planordner, spec?, kopie?, materialisieren?: {ohne: [relpfade], tasks?: [taskdateien]}, suite?, linsen?, mutanten?: "voll"|"muster"|"aus" (Vorgabe voll), plaetze?: 1..4, verifizieren?: true, verifizieren_ab?: "wichtig"|"blockierend", effort?, werkzeuge?: Ordner der Planwerkzeuge, zusatz?}. Einzelheiten im Kommentar unter meta.',
phases: [
{ title: 'Vorbereitung', detail: 'Hilfsagent: Kopie bauen, Plan anwenden, Planwerkzeuge fahren' },
{ title: 'Plankritik', detail: 'je Linse ein pruefer, gleichzeitig nach Verbrauch' },
{ title: 'Widerlegung', detail: 'je wichtiger Befund ein zweiter pruefer, höchstens zwei gleichzeitig' },
],
}
// Diese Datei traegt LF. Mit CRLF im Arbeitsbaum (seit einer Nachbesserung am 2026-09-24) lehnte
// die Rechtepruefung sie per Name wie per scriptPath mit "script contains control characters" ab:
// Das CR ist das Steuerzeichen. Belegt am 2026-09-26 (Erinnerung
// gespeicherter-workflow-per-name-scheitert). Die Einzelheiten zu den Args stehen hier, damit meta
// kurz bleibt:
// - werkzeuge ist ein Pfad, keine Ausgabe; Vorgabe D:/Claude/.vault-tools/plan-kopie.
// - Ohne kopie baut materialisieren sie selbst (kopie_bauen.py plus plan_materialisiere.py);
// ohne() muss Geheimnisdateien und große Datenordner nennen.
// - mutanten: "voll" (Vorgabe), "muster" oder "aus". Die Stufe greift, sobald ctx.kopie steht -
// also auch mit materialisieren, nicht nur mit args.kopie direkt gesetzt. Sie läuft auch ohne
// die Linse arithmetik in args.linsen; die Rohausgabe steht dann nur in der Rückgabe
// (mutanten_lauf, mutanten_status, mutanten_modus). Steuert den Hilfsagenten werkzeug:mutanten,
// der mutanten_lauf_kopie.sh fährt (siehe Kommentar dort unten).
// - plaetze begrenzt die gleichzeitigen Linsen; verbrauch.js senkt sie nach dem 5-Stunden-Fenster
// und der Woche.
// - Rückgabe mit status fertig oder gebremst; ein gebremster Lauf wird neu gestartet, nicht
// fortgesetzt.
//
// Rollenparameter: model wird immer gesetzt (Stamm). Der Effort der pruefer kommt aus dem
// Frontmatter von .claude/agents/pruefer.md; args.effort überschreibt ihn nur bewusst.
// Der Hilfsagent hat keinen agentType und bekommt deshalb seinen Effort mit (low: gemessen
// gleich treu wie high bei mechanischer Arbeit, codeworkflow.md "Effort je Rolle").
// Opus-Plätze: Vor den Linsen und vor der Widerlegung liest verbrauch.js den Planverbrauch ab.
// Die Linsen laufen mit so vielen Plätzen, wie verbrauch.js erlaubt (höchstens so viele wie
// Linsen, höchstens LINSEN_HOECHSTENS), die Widerlegung mit höchstens zwei. Gebremst heißt: Die
// Phase startet nicht, und der Lauf kommt mit status gebremst zurück.
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
const LINSEN_HOECHSTENS = 4
const WIDERLEGUNG_HOECHSTENS = 2
const OHNE_ABLESUNG = 2
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
const RANG = { blockierend: 0, wichtig: 1, klein: 2 }
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
// Text für die Linse arithmetik, wenn der Mutantenlauf (werkzeug:mutanten) etwas geliefert hat.
// Seit der Nachbesserung 2026-09-28 (N5, N7): modus: muster in der Rohausgabe heißt, dass kein
// Mutant gefahren ist (der Satz "prüfe bei OK an der Fehlermeldung" entfällt dann); Exit 2 oder 3
// heißt, der Lauf ist vor oder während der Mutanten gescheitert.
function mutantenText(m) {
if (!m) return ''
if (m.keineToml) return ' Der Plan hat keine mutanten.toml. Das ist ein Befund der Schwere wichtig, sobald der Plan Mutanten in "Fertig heißt" nennt.'
const zeilen = m.rohausgabe.split('\n')
const letzte = zeilen.slice(-200).join('\n')
const musterModus = /^modus: muster/m.test(m.rohausgabe)
const exitMatch = /EXIT mutanten_lauf: (\d+)/.exec(m.rohausgabe)
const exitCode = exitMatch ? Number(exitMatch[1]) : null
let hinweis = ''
if (/ZEITGRENZE/.test(m.rohausgabe)) hinweis = ' Der Lauf hat seine Zeitgrenze erreicht: Mutanten ohne eigene Zeile sind nicht gefahren; setze für sie selbst höchstens die aus, die dir am riskantesten erscheinen, in einer eigenen Kopie.'
else if (exitCode === 2 || exitCode === 3) hinweis = ` Der Mutantenlauf ist vor oder während der Mutanten gescheitert (Exit ${exitCode}); welche Mutanten ohne eigene Zeile sind, sind nicht gefahren; das ist ein Befund der Schwere wichtig.`
const bewertung = musterModus
? ' Kein Mutant ist gefahren; OK heißt hier nur, dass das Muster genau einmal in der Datei steht; ob ein Test fällt, ist unbelegt.'
: ' Bewerte jeden Mutanten mit LUECKE, UNKLAR, ZEIT oder MUSTER als Befund und prüfe bei OK an der Fehlermeldung, ob der Test an der gemeinten Stelle fällt; setze Mutanten nicht erneut von Hand.'
return `${hinweis}${bewertung}\n<mutantenlauf>\n${letzte}\n</mutantenlauf>`
}
const LINSEN = {
arithmetik: (a, ctx) => `Linse: Fixture-Arithmetik und Rot-Nachweise. Rechne jede Mindestzahl, jede Grenze und jeden Schwellenwert im Produktivcode des Plans gegen jede Fixture der Tests nach. Prüfe jede im Plan erwartete Fehlermeldung und jede vorhergesagte Testausgabe wörtlich: Würde der Test vor der Umsetzung genau so rot, und danach grün? pytest kürzt FAILED-Zeilen ohne Terminal auf 80 Zeichen; vergleiche Fehlerzeilen deshalb mit COLUMNS=300 vor dem Befehl.${ctx.kopie ? ` Der Plan ist auf die Kopie ${ctx.kopie} angewendet. Fahre dort die Suite (${a.suite || 'Testbefehl aus dem Plan'}) und vergleiche jede Vorhersage des Plans mit der echten Ausgabe. In der Kopie änderst du nichts außer dem, was die Testläufe selbst anlegen.` : ' Eine Kopie mit angewendetem Plan gibt es nicht; du rechnest am Text.'}${mutantenText(ctx.mutantenLauf)}`,
bezeichner: (a, ctx) => `Linse: Bezeichner, Signaturen und Byte-Treue. Prüfe jeden Namen, jede Signatur und jeden Import über Taskgrenzen hinweg. Jeder Aus-Block muss genau einmal im echten Baum stehen, jeder Codeblock muss sich parsen lassen, und Codeblöcke dürfen keine Zeilen- und Absatztrenner, Format- oder Steuerzeichen enthalten. Prüfbefehle im Plan dürfen nur Code treffen, nie ein Wort, das auch in einem Kommentar steht.${ctx.werkzeuge ? `\n\nDie Planwerkzeuge sind schon gelaufen, hier ihre Rohausgabe. Bewerte sie, statt sie erneut zu fahren; nur bei Zweifeln selbst nachfahren.\n<werkzeugausgabe>\n${ctx.werkzeuge}\n</werkzeugausgabe>` : ` Fahre dazu plan_bloecke.py vor und plan_zeichen.py aus ${a.werkzeuge || 'D:/Claude/.vault-tools/plan-kopie'}.`}`,
abdeckung: (a) => `Linse: Spec-Abdeckung in beide Richtungen. Jede Anforderung${a.spec ? ` aus ${a.spec}` : ' der Übersicht'} braucht einen Task und einen Test, und jeder Task braucht eine Anforderung. Die "Fertig heißt"-Liste der Übersicht muss je Check einen Beleg außerhalb der eigenen Meinung haben. Das Frontmatter jeder Taskdatei braucht modell und modell_begruendung; Abschrift-Tasks mit Anführungszeichen, Gedanken- und Streckenstrichen oder Auslassungspunkten im Code tragen modell sonnet und effort low statt haiku.`,
}
const BEFUNDE = {
type: 'object',
properties: {
befunde: {
type: 'array',
items: {
type: 'object',
properties: {
schweregrad: { type: 'string', enum: ['blockierend', 'wichtig', 'klein'] },
datei: { type: 'string' },
zeile: { type: 'integer' },
beschreibung: { type: 'string' },
reproduktion: { type: 'string' },
},
required: ['schweregrad', 'datei', 'zeile', 'beschreibung', 'reproduktion'],
},
},
selbst_ausgefuehrt: { type: 'array', items: { type: 'string' } },
nicht_pruefbar: { type: 'array', items: { type: 'string' } },
},
required: ['befunde', 'selbst_ausgefuehrt', 'nicht_pruefbar'],
}
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'],
}
const URTEIL = {
type: 'object',
properties: {
urteil: { type: 'string', enum: ['bestaetigt', 'widerlegt', 'offen'] },
schweregrad: { type: 'string', enum: ['blockierend', 'wichtig', 'klein'], description: 'dein eigenes Urteil über die Schwere' },
bedingung: { type: 'string', description: 'unter welcher Bedingung der Befund gilt; leer, wenn immer' },
begruendung: { type: 'string' },
beleg: { type: 'string', description: 'eigener Befehl mit Ausgabe oder wörtliches Zitat mit Fundstelle' },
},
required: ['urteil', 'schweregrad', 'bedingung', 'begruendung', 'beleg'],
}
function pruefeArgs(a) {
if (!a || typeof a !== 'object') throw new Error('plankritik: args fehlen')
const fehlt = ['projekt', 'planordner'].filter(k => !a[k])
if (fehlt.length) throw new Error('plankritik: args fehlen: ' + fehlt.join(', '))
if (a.linsen !== undefined && !Array.isArray(a.linsen)) throw new Error('plankritik: linsen muss eine Liste sein, bekannt: ' + Object.keys(LINSEN).join(', '))
if (a.materialisieren && a.materialisieren.tasks !== undefined && !Array.isArray(a.materialisieren.tasks)) throw new Error('plankritik: materialisieren.tasks muss eine Liste von Taskdateien sein')
const unbekannt = (a.linsen || []).filter(l => !LINSEN[l])
if (unbekannt.length) throw new Error('plankritik: unbekannte Linse: ' + unbekannt.join(', ') + '; bekannt: ' + Object.keys(LINSEN).join(', '))
if (a.linsen && new Set(a.linsen).size !== a.linsen.length) throw new Error('plankritik: eine Linse steht doppelt in linsen: ' + a.linsen.join(', '))
if (a.effort !== undefined && !EFFORTS.includes(a.effort)) throw new Error('plankritik: effort "' + a.effort + '" ungültig')
if (a.verifizieren_ab !== undefined && !['wichtig', 'blockierend'].includes(a.verifizieren_ab)) throw new Error('plankritik: verifizieren_ab muss wichtig oder blockierend sein')
if (a.plaetze !== undefined && !(Number.isInteger(a.plaetze) && a.plaetze >= 1 && a.plaetze <= LINSEN_HOECHSTENS)) throw new Error('plankritik: plaetze muss eine ganze Zahl von 1 bis ' + LINSEN_HOECHSTENS + ' sein')
if (a.materialisieren && !(Array.isArray(a.materialisieren.ohne) && a.materialisieren.ohne.length > 0 && a.materialisieren.ohne.every(x => typeof x === 'string' && x))) throw new Error('plankritik: materialisieren.ohne muss eine nicht leere Liste sein, Ausschlüsse als Pfade vom Projektwurzelordner, mindestens .git (falls vorhanden) und Geheimnisdateien')
if (a.mutanten !== undefined && !['voll', 'muster', 'aus'].includes(a.mutanten)) throw new Error('plankritik: mutanten muss voll, muster oder aus sein')
}
// Höchstens n Aufgaben gleichzeitig, ohne Leerlauf zwischen Paaren.
async function hoechstens(n, items, fn) {
const ergebnisse = new Array(items.length).fill(null)
let naechster = 0
async function arbeiter() {
while (naechster < items.length) {
const i = naechster++
try { ergebnisse[i] = await fn(items[i], i) } catch (e) { log(`Aufgabe ${i + 1} abgebrochen: ${e && e.message}`) }
}
}
await Promise.all(Array.from({ length: Math.min(n, items.length) }, () => arbeiter()))
return ergebnisse
}
// Eine Hilfsfunktion ablesen() steht auch in planung.js (gleiche Form) sowie in pruefung.js und
// welle-mit-pruefung.js (dort nur messend, ohne Rückgabe). Die Regel (Fenster, Stufen, Rückfall)
// steht nur in verbrauch.js; hier steht nur der Rückfall für den Fall, dass verbrauch.js selbst
// nicht läuft, etwa weil dieser Workflow als Kind läuft.
const ablesungen = []
async function ablesen(anlass, wunsch) {
let r = null
try {
r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'plankritik:' + anlass, wunsch })
} catch (e) {
r = { status: 'nicht_gelesen', grund: 'verbrauch.js nicht ausführbar: ' + (e && e.message) }
}
if (!r || typeof r.plaetze !== 'number') {
r = { status: 'nicht_gelesen', quelle: 'keine', fuenf_stunden: null, woche: null, plaetze: Math.min(wunsch, OHNE_ABLESUNG), gebremst: false, grund: (r && r.grund) || 'verbrauch.js ohne verwertbares Ergebnis' }
}
ablesungen.push({ anlass, ...r })
log(`Verbrauch ${anlass}: ${r.status}, ${r.plaetze} Plätze${r.gebremst ? ', gebremst' : ''} (${r.grund})`)
return r
}
function werkzeugAgent(label, phase, befehl, zweck, zusatz) {
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}
${zusatz ? '\n' + zusatz + '\n' : ''}
Ä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: 'sonnet', effort: 'low' })
}
function testort(ctx) {
return ctx.kopie
? `Tests fährst du nur in der Kopie ${ctx.kopie}, nie im echten Projektbaum: Dort kann Betrieb laufen, und andere Prüfer arbeiten gleichzeitig. Mutanten legst du nur in einer eigenen Kopie dieser Kopie an, in einem neuen Ordner unter dem System-Temp, nie in ${ctx.kopie} selbst.`
: 'Tests fährst du in dieser Plankritik nicht, auch nicht im echten Projektbaum: Dort kann Betrieb laufen, und andere Prüfer arbeiten gleichzeitig.'
}
function auftrag(a, linse, ctx) {
return `Lies den Plan im Ordner ${a.planordner} für das Projekt ${a.projekt}, bevor ein Executer startet. Du änderst keine Datei im Projekt und keine im Plan. Eine Umsetzung gibt es noch nicht, also auch keinen Executer-Bericht, dessen Testlauf du wiederholen könntest. ${testort(ctx)}
${LINSEN[linse](a, ctx)}
Melde jeden Befund mit Schweregrad, auch unsichere; ein zweiter Prüfer versucht danach jeden wichtigen Befund zu widerlegen. Nenn je Befund Datei, Zeile und wie er sich reproduzieren lässt.
${a.zusatz ? a.zusatz + '\n' : ''}Übertrage deinen Schlussbericht in die Felder des StructuredOutput-Werkzeugs.`
}
function pfadnorm(p) { return String(p).replace(/\\/g, '/').replace(/\/+/g, '/').toLowerCase() }
function dateiname(p) { return pfadnorm(p).split('/').pop() }
// Doppelte Befunde verschiedener Linsen zusammenfassen: gleicher Pfad und gleiche Zeile.
// Zeile 0 oder kleiner heißt "ganze Datei" und wird nie zusammengefasst, weil darunter
// verschiedene Befunde derselben Datei liegen.
function zusammenfassen(befunde) {
const gruppen = new Map()
befunde.forEach((b, i) => {
const k = b.zeile > 0 ? pfadnorm(b.datei) + ':' + b.zeile : 'einzeln:' + i
if (!gruppen.has(k)) gruppen.set(k, { ...b, linsen: [b.linse], quellen: [b] })
else {
const g = gruppen.get(k)
g.quellen.push(b)
if (!g.linsen.includes(b.linse)) g.linsen.push(b.linse)
if (RANG[b.schweregrad] < RANG[g.schweregrad]) g.schweregrad = b.schweregrad
}
})
return [...gruppen.values()]
}
function widerlegungsauftrag(a, g, ctx) {
const texte = g.quellen.map((b, i) => `Fassung ${i + 1} (Linse ${b.linse}, ${b.schweregrad}, ${b.datei}:${b.zeile}): ${b.beschreibung}\nReproduktion laut Linse: ${b.reproduktion}`).join('\n\n')
return `Eine Plankritik hat zum Plan im Ordner ${a.planordner} (Projekt ${a.projekt}) diesen Befund gemeldet, Fundstelle ${g.datei}:${g.zeile}:
<befund>
${texte}
</befund>
Versuche ihn zu widerlegen. Lies die Stelle selbst und reproduziere den Befund mit einem eigenen Befehl. Du änderst keine Datei im Projekt und keine im Plan. ${testort(ctx)}
- "bestaetigt" nur mit eigener Reproduktion oder einem wörtlichen Beleg aus der Quelle.
- "widerlegt" nur mit einem Gegenbeleg: ein Befehl mit Ausgabe oder ein Zitat, das die Aussage falsch macht.
- Sonst "offen".
Gilt der Befund nur unter einer Bedingung, nenn sie. Gib deine eigene Einschätzung der Schwere an.
Übertrage dein Urteil in die Felder des StructuredOutput-Werkzeugs.`
}
pruefeArgs(args)
const W = args.werkzeuge || 'D:/Claude/.vault-tools/plan-kopie'
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
const ctx = { kopie: args.kopie || null, werkzeuge: null, temp: null, mutantenLauf: null, mutantenStatus: null }
const MUTANTEN_MODUS = args.mutanten || 'voll'
const vorbereitung = []
// Eine Kopie gilt nur als verwendbar, wenn der Lauf sauber endet, den Pfad bis zum
// Zeilenende nennt (Pfade mit Leerzeichen) und das Planwerkzeug mindestens einen Block
// angewendet hat. Sonst rechnet Linse 1 am Text, und das steht im Log.
function kopieAusLauf(r, wozu) {
const lauf = r && r.laeufe && r.laeufe[0]
if (!lauf) { log(`${wozu}: Hilfsagent ohne Ergebnis, keine Kopie`); return null }
const m = /^KOPIE=(.+?)\s*$/m.exec(lauf.ausgabe)
const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(lauf.ausgabe)
if (lauf.exit !== 0 || !m) { log(`${wozu}: Kopie nicht verwendbar (Exit ${lauf.exit}${m ? '' : ', kein KOPIE-Pfad'})`); return null }
ctx.temp = m[1].replace(/\/mat$/, '')
if (!angewendet) { log(`${wozu}: das Planwerkzeug hat keinen Block angewendet; die Kopie ist der unveränderte Stand und wird nicht verwendet`); return null }
return m[1]
}
function tempText() {
return ctx.temp ? `${ctx.temp} bleibt liegen (Projektkopie ohne die Ausschlüsse); löschen, wenn nicht mehr gebraucht` : null
}
phase('Vorbereitung')
if (ctx.kopie && args.materialisieren) log('kopie ist gesetzt, materialisieren wird übergangen')
if (!ctx.kopie && args.materialisieren) {
const liste = (args.materialisieren.tasks || []).map(q).join(' ')
const auswahl = liste
? `TASKS=(${liste})`
: `shopt -s nullglob; pushd ${q(args.planordner)} >/dev/null && TASKS=(task-*.md) && popd >/dev/null; [ \${#TASKS[@]} -gt 0 ] || { echo "KEINE_TASKS: im Planordner liegt keine task-*.md; materialisieren.tasks angeben"; exit 3; }`
const befehl = `${auswahl}; T=$(mktemp -d) && echo "KOPIE=$(cygpath -m "$T/mat")" && ${PY} ${q(W + '/kopie_bauen.py')} ${q(args.projekt)} "$T/mat" --ohne ${args.materialisieren.ohne.map(q).join(' ')} && ${PY} ${q(W + '/plan_materialisiere.py')} ${q(args.planordner)} "$T/mat" "\${TASKS[@]}"`
const r = await werkzeugAgent('werkzeug:kopie', 'Vorbereitung', befehl, 'Kopie des Projekts bauen und den Plan darauf anwenden')
ctx.kopie = kopieAusLauf(r, 'Plankritik')
vorbereitung.push(r)
}
{
const befehl = `${PY} ${q(W + '/plan_zeichen.py')} ${q(args.planordner)}; echo "EXIT plan_zeichen: $?"; ls ${q(args.planordner)}/task-*.md >/dev/null 2>&1 && { ${PY} ${q(W + '/plan_bloecke.py')} vor ${q(args.planordner)}/task-*.md --projekt ${q(args.projekt)}; echo "EXIT plan_bloecke: $?"; } || echo "plan_bloecke: keine task-*.md im Planordner, nicht gefahren"`
const r = await werkzeugAgent('werkzeug:planwerkzeuge', 'Vorbereitung', befehl, 'Codeblöcke auf unsichtbare Zeichen und Aus-Blöcke gegen den Baum prüfen')
// Nur wenn beide Werkzeuge mit 0 endeten, gilt die Ausgabe als gelaufen (2026-09-24: ein Protokollsatz statt
// Pfad in args.werkzeuge endete mit Errno 2 und ging trotzdem als Rohausgabe an die Linse). Der Exit des
// ganzen Befehls hilft dabei nicht, er ist der des letzten echo; gelesen werden die EXIT-Zeilen.
const aus = r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''
const gelaufen = /EXIT plan_zeichen: 0(?!\d)/.test(aus) && (/EXIT plan_bloecke: 0(?!\d)/.test(aus) || /plan_bloecke: keine task-/.test(aus))
if (gelaufen) ctx.werkzeuge = aus
else log('Planwerkzeuge: Hilfsagent ohne Ergebnis; Linse bezeichner fährt sie selbst')
vorbereitung.push(r)
}
// Mutantenlauf (seit 2026-09-27). mutanten_lauf.py ändert Dateien in der Wurzel, die es bekommt,
// und setzt sie nach jedem Mutanten zurück. Die Linsen fahren gleichzeitig Tests in ctx.kopie;
// deshalb arbeitet mutanten_lauf_kopie.sh auf einer Kopie der Kopie unter /tmp in WSL. Dieser
// await steht in der Vorbereitung, also vor phase('Plankritik'): Der Mutantenlauf ist abgewartet,
// bevor die Linsen starten, und die Kopie der Kopie schützt dabei ctx.kopie selbst - die Linsen
// arbeiten gleichzeitig darin, mutanten_lauf.py aber nur in seiner eigenen Kopie unter /tmp.
// MSYS_NO_PATHCONV=1 ist Pflicht: Ohne sie übersetzt die Git-Bash /mnt/d/... in einen
// Windows-Pfad (Erinnerung git-bash-uebersetzt-pfade-fuer-wsl-exe). cygpath -l ist Pflicht, weil
// die Kopie oft unter dem Kurznamen PASCAL~1 liegt. cygpath -m statt -w, weil wsl -d Debian -- ...
// die Befehlszeile an die Linux-Shell weiterreicht: Die Backslash-Begründung dafür
// ("D:\Claude\foo" kam als "D:Claudefoo" an, gemessen 2026-09-27) konnte der Prüfer am
// 2026-09-28 in dieser Aufrufform nicht nachstellen; -m bleibt trotzdem, weil es nicht schadet,
// und wslpath -u im Skript wandelt den Pfad ohnehin um. Das Bash-Werkzeug des Hilfsagenten endet
// nach höchstens zehn Minuten; das Skript begrenzt den Lauf deshalb selbst auf 540 s und meldet
// eine überschrittene Grenze mit ZEITGRENZE.
// mutanten_status (N8, seit 2026-09-28): aus, ohne_kopie, keine_toml, ohne_ergebnis, sonst exit_<n>.
if (MUTANTEN_MODUS === 'aus') ctx.mutantenStatus = 'aus'
else if (!ctx.kopie) ctx.mutantenStatus = 'ohne_kopie'
if (ctx.kopie && MUTANTEN_MODUS !== 'aus') {
const befehl = `MSYS_NO_PATHCONV=1 wsl -d Debian -- bash /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$(cygpath -m -l ${q(ctx.kopie)})" "$(cygpath -m -l ${q(args.planordner)})" ${MUTANTEN_MODUS === 'muster' ? 'muster' : 'voll'}`
const r = await werkzeugAgent('werkzeug:mutanten', 'Vorbereitung', befehl, 'Mutanten aus mutanten.toml auf einer Kopie der Kopie fahren', 'Setze im Bash-Werkzeug den Parameter timeout auf 600000; der Befehl darf bis zu zehn Minuten laufen.')
const aus = r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''
const exitMatch = /EXIT mutanten_lauf: (\d+)/.exec(aus)
if (/KEINE_MUTANTEN_TOML/.test(aus)) { ctx.mutantenLauf = { keineToml: true, rohausgabe: aus }; ctx.mutantenStatus = 'keine_toml' }
else if (exitMatch) { ctx.mutantenLauf = { keineToml: false, rohausgabe: aus }; ctx.mutantenStatus = 'exit_' + exitMatch[1] }
else { log('Mutantenlauf: Hilfsagent ohne verwertbares Ergebnis (kein KEINE_MUTANTEN_TOML, kein EXIT mutanten_lauf); Linsen laufen ohne Mutantenlauf weiter'); ctx.mutantenStatus = 'ohne_ergebnis' }
vorbereitung.push(r)
}
phase('Plankritik')
const gewaehlt = args.linsen && args.linsen.length ? args.linsen : Object.keys(LINSEN)
const vorLinsen = await ablesen('vor Linsen', Math.min(args.plaetze || gewaehlt.length, gewaehlt.length, LINSEN_HOECHSTENS))
if (vorLinsen.gebremst) {
log('Plankritik gebremst vor den Linsen: ' + vorLinsen.grund)
return {
status: 'gebremst',
gebremst_vor: 'Linsen',
zaehlung: { roh: 0, zusammengefasst: 0, bestaetigt: 0, offen: 0, widerlegt: 0, nicht_geprueft: 0 },
kopie: ctx.kopie,
temp_ordner: tempText(),
mutanten_lauf: ctx.mutantenLauf ? ctx.mutantenLauf.rohausgabe : null,
mutanten_status: ctx.mutantenStatus,
mutanten_modus: MUTANTEN_MODUS,
befunde: [],
selbst_ausgefuehrt: [],
nicht_pruefbar: [],
vorbereitung,
ohne_ergebnis: gewaehlt,
verbrauch: ablesungen,
plaetze: { linsen: 0, widerlegung: 0 },
naechster_schritt: 'Nach der Zurücksetzung des Fensters neu starten, nicht mit resumeFromRunId fortsetzen: Die Ablesung käme sonst aus dem Zwischenspeicher.',
}
}
const kritiken = await hoechstens(vorLinsen.plaetze, gewaehlt, linse => {
const opts = { label: 'plankritik:' + linse, phase: 'Plankritik', schema: BEFUNDE, agentType: 'pruefer', model: 'opus' }
if (args.effort) opts.effort = args.effort
return agent(auftrag(args, linse, ctx), opts).then(r => ({ linse, ergebnis: r }))
})
const ergebnisse = kritiken.filter(k => k && k.ergebnis)
const ohneErgebnis = gewaehlt.filter(l => !ergebnisse.some(e => e.linse === l))
if (ohneErgebnis.length) log('Linsen ohne Ergebnis: ' + ohneErgebnis.join(', '))
const roh = ergebnisse.flatMap(e => e.ergebnis.befunde.map(b => ({ linse: e.linse, ...b })))
const gruppen = zusammenfassen(roh)
const ab = RANG[args.verifizieren_ab || 'wichtig']
const zuPruefen = args.verifizieren === false ? [] : gruppen.filter(g => RANG[g.schweregrad] <= ab)
log(`${roh.length} Befunde, zusammengefasst ${gruppen.length}; ${zuPruefen.length} gehen in die Widerlegung, ${gruppen.length - zuPruefen.length} nicht (${args.verifizieren === false ? 'abgeschaltet' : 'unter der Schwelle ' + (args.verifizieren_ab || 'wichtig')})`)
phase('Widerlegung')
let gebremstVor = null
let plaetzeWiderlegung = 0
if (zuPruefen.length) {
const vorWiderlegung = await ablesen('vor Widerlegung', Math.min(WIDERLEGUNG_HOECHSTENS, zuPruefen.length))
if (vorWiderlegung.gebremst) {
gebremstVor = 'Widerlegung'
log('Widerlegung gebremst, die Befunde bleiben ungeprüft: ' + vorWiderlegung.grund)
} else plaetzeWiderlegung = vorWiderlegung.plaetze
}
if (!gebremstVor) {
const urteile = await hoechstens(plaetzeWiderlegung, zuPruefen, (g, i) => {
const opts = { label: `widerlegung:${i + 1}:${dateiname(g.datei)}:${g.zeile}`, phase: 'Widerlegung', schema: URTEIL, agentType: 'pruefer', model: 'opus' }
if (args.effort) opts.effort = args.effort
return agent(widerlegungsauftrag(args, g, ctx), opts)
})
zuPruefen.forEach((g, i) => {
if (!urteile[i]) log(`Widerlegung ${i + 1} (${g.datei}:${g.zeile}) ohne Ergebnis; Befund bleibt offen`)
g.widerlegung = urteile[i] || { urteil: 'offen', schweregrad: g.schweregrad, bedingung: '', begruendung: 'Widerlegungsagent ohne Ergebnis', beleg: '' }
})
}
await ablesen('Ende', 0)
const URTEILRANG = { bestaetigt: 0, offen: 1, undefined: 2, widerlegt: 3 }
const befunde = gruppen
.map(g => ({ urteil: g.widerlegung ? g.widerlegung.urteil : gebremstVor && zuPruefen.includes(g) ? 'nicht geprüft (gebremst)' : 'nicht geprüft', ...g }))
.sort((x, y) => (URTEILRANG[x.widerlegung && x.widerlegung.urteil] - URTEILRANG[y.widerlegung && y.widerlegung.urteil]) || (RANG[x.schweregrad] - RANG[y.schweregrad]))
const zaehlung = { roh: roh.length, zusammengefasst: gruppen.length, bestaetigt: 0, offen: 0, widerlegt: 0, nicht_geprueft: gruppen.filter(g => !g.widerlegung).length }
befunde.forEach(b => { if (b.widerlegung) zaehlung[b.widerlegung.urteil]++ })
return {
status: gebremstVor ? 'gebremst' : 'fertig',
gebremst_vor: gebremstVor,
zaehlung,
kopie: ctx.kopie,
temp_ordner: tempText(),
mutanten_lauf: ctx.mutantenLauf ? ctx.mutantenLauf.rohausgabe : null,
mutanten_status: ctx.mutantenStatus,
mutanten_modus: MUTANTEN_MODUS,
befunde,
selbst_ausgefuehrt: ergebnisse.map(e => ({ linse: e.linse, befehle: e.ergebnis.selbst_ausgefuehrt })),
nicht_pruefbar: ergebnisse.flatMap(e => e.ergebnis.nicht_pruefbar.map(x => e.linse + ': ' + x)),
vorbereitung,
ohne_ergebnis: ohneErgebnis,
verbrauch: ablesungen,
plaetze: { linsen: vorLinsen.plaetze, widerlegung: plaetzeWiderlegung },
naechster_schritt: gebremstVor ? 'Die Widerlegung nach der Zurücksetzung des Fensters nachholen: plankritik neu starten, nicht mit resumeFromRunId fortsetzen.' : 'Bestätigte und offene Befunde am Plan korrigieren, danach die Kopie neu bauen.',
}
+211
View File
@@ -0,0 +1,211 @@
export const meta = {
name: 'planung',
description: 'Erstplanung: je Task ein planer, gestartet, sobald die Taskdateien seiner Voraussetzungen geschrieben sind; Opus-Plätze nach Verbrauch, vorgegeben zwei',
whenToUse: 'Schritt 2 der Standardschleife, wenn 00-uebersicht.md mit der Tasktabelle steht und viele Taskdateien zu schreiben sind (AI/Regelwerk/codeworkflow.md, "Verbrauch und Opus-Plätze"). Nicht für Nachbesserungen. Args: {projekt, planordner, gemeinsam, taskauftrag, tasks: [{nr, thema, datei, modell, voraussetzungen: [nr], welle?}], erledigt?: [nr], plaetze?: 1..4, nachlesen_alle?: 1..20, zusatz?}. plaetze ist der Wunsch, verbrauch.js senkt ihn; ohne plaetze gilt zwei, drei oder vier erst nach der Eichung der Stufen. taskauftrag darf {NR}, {THEMA}, {DATEI}, {MODELL}, {WELLE} und {VORAUSSETZUNGEN} enthalten. Rückgabe mit status fertig, unvollstaendig, gebremst oder abgebrochen; offene Tasks mit erledigt in einem neuen Lauf nachholen, nicht mit resumeFromRunId. Die Plankritik läuft danach als eigener Workflow.',
phases: [{ title: 'Taskdateien', detail: 'planer je Task, Start nach den Voraussetzungen, Plätze nach Verbrauch' }],
}
// Rollenparameter: model wird immer gesetzt (Stamm), der Effort kommt aus dem Frontmatter von
// .claude/agents/planer.md. Ein Task startet erst, wenn die Planer aller seiner Voraussetzungen
// ein Ergebnis geliefert haben; sonst erfindet er Namen, die ein Vorgänger anders schreibt
// (codeworkflow.md, "Verbrauch und Opus-Plätze"). Die Zahl gleichzeitiger Planer liest
// verbrauch.js am Anfang und nach je nachlesen_alle fertigen Planern ab. Gebremst heißt: Kein
// Planer startet mehr, die laufenden enden, der Rest kommt als offen zurück. Nach drei Planern
// ohne Ergebnis startet keiner mehr, meist ist dann das Kontingent erschöpft. Ein Ergebnis zählt
// nur, wenn es die erwartete Taskdatei nennt; sonst gilt der Task als gescheitert.
// VORGABE_PLAETZE ist Stufe 1 der Freigabe vom 2026-09-24 (Pool mit zwei Plätzen). Stufe 3 mit
// drei bis vier Plätzen für die Erstplanung kommt erst nach der Eichung der Stufen in verbrauch.js.
const MODELLE = ['haiku', 'sonnet', 'opus']
const HOECHSTENS = 4
const VORGABE_PLAETZE = 2
const OHNE_ABLESUNG = 2
const ABBRUCH_NACH = 3
const NACHLESEN_ALLE = 4
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
const TASK_SCHEMA = {
type: 'object',
properties: {
datei: { type: 'string' },
zieldateien: { type: 'array', items: { type: 'string' } },
testdateien: { type: 'array', items: { type: 'string' } },
zeilen_code: { type: 'integer' },
abweichungen_von_uebersicht: { type: 'array', items: { type: 'string' } },
offene_punkte: { type: 'array', items: { type: 'string' } },
},
required: ['datei', 'zieldateien', 'testdateien', 'abweichungen_von_uebersicht', 'offene_punkte'],
}
function pruefeArgs(a) {
if (!a || typeof a !== 'object') throw new Error('planung: args fehlen')
const fehlt = ['projekt', 'planordner', 'gemeinsam', 'taskauftrag', 'tasks'].filter(k => !a[k])
if (fehlt.length) throw new Error('planung: args fehlen: ' + fehlt.join(', '))
if (!Array.isArray(a.tasks) || a.tasks.length === 0) throw new Error('planung: tasks muss eine nicht leere Liste sein')
const nummern = new Set()
const dateien = new Set()
a.tasks.forEach((t, i) => {
if (!t || typeof t !== 'object') throw new Error(`planung: tasks[${i}] ist kein Objekt`)
if (!Number.isInteger(t.nr)) throw new Error(`planung: tasks[${i}]: nr muss eine ganze Zahl sein`)
if (nummern.has(t.nr)) throw new Error(`planung: Task ${t.nr} steht doppelt in tasks`)
nummern.add(t.nr)
for (const k of ['thema', 'datei', 'modell']) if (typeof t[k] !== 'string' || !t[k]) throw new Error(`planung: Task ${t.nr}: ${k} fehlt`)
if (!MODELLE.includes(t.modell)) throw new Error(`planung: Task ${t.nr}: modell "${t.modell}" nicht in ${MODELLE.join('/')}`)
if (dateien.has(t.datei)) throw new Error(`planung: Datei ${t.datei} steht bei zwei Tasks`)
dateien.add(t.datei)
if (!Array.isArray(t.voraussetzungen) || !t.voraussetzungen.every(n => Number.isInteger(n))) throw new Error(`planung: Task ${t.nr}: voraussetzungen muss eine Liste ganzer Zahlen sein`)
if (new Set(t.voraussetzungen).size !== t.voraussetzungen.length) throw new Error(`planung: Task ${t.nr}: eine Voraussetzung steht doppelt`)
})
a.tasks.forEach(t => t.voraussetzungen.forEach(n => {
if (!nummern.has(n)) throw new Error(`planung: Task ${t.nr}: Voraussetzung ${n} gibt es nicht`)
}))
if (a.erledigt !== undefined && !(Array.isArray(a.erledigt) && a.erledigt.every(n => nummern.has(n)))) throw new Error('planung: erledigt muss eine Liste bekannter Tasknummern sein')
if (a.plaetze !== undefined && !(Number.isInteger(a.plaetze) && a.plaetze >= 1 && a.plaetze <= HOECHSTENS)) throw new Error('planung: plaetze muss eine ganze Zahl von 1 bis ' + HOECHSTENS + ' sein')
if (a.nachlesen_alle !== undefined && !(Number.isInteger(a.nachlesen_alle) && a.nachlesen_alle >= 1 && a.nachlesen_alle <= 20)) throw new Error('planung: nachlesen_alle muss eine ganze Zahl von 1 bis 20 sein')
// Zyklen: Kahn-Verfahren über die Voraussetzungen; was übrig bleibt, hängt im Kreis.
const offen = new Map(a.tasks.map(t => [t.nr, t.voraussetzungen.length]))
const bereit = a.tasks.filter(t => t.voraussetzungen.length === 0).map(t => t.nr)
let geschafft = 0
while (bereit.length) {
const nr = bereit.pop()
geschafft++
for (const t of a.tasks) if (t.voraussetzungen.includes(nr)) {
offen.set(t.nr, offen.get(t.nr) - 1)
if (offen.get(t.nr) === 0) bereit.push(t.nr)
}
}
if (geschafft < a.tasks.length) throw new Error('planung: Zyklus in den Voraussetzungen bei Task ' + [...offen].filter(([, n]) => n > 0).map(([nr]) => nr).join(', '))
}
// Eine Hilfsfunktion ablesen() steht auch in plankritik.js (gleiche Form) sowie in pruefung.js
// und welle-mit-pruefung.js (dort nur messend, ohne Rückgabe). Die Regel (Fenster, Stufen,
// Rückfall) steht nur in verbrauch.js.
const ablesungen = []
async function ablesen(anlass, wunsch) {
let r = null
try {
r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'planung:' + anlass, wunsch })
} catch (e) {
r = { status: 'nicht_gelesen', grund: 'verbrauch.js nicht ausführbar: ' + (e && e.message) }
}
if (!r || typeof r.plaetze !== 'number') {
r = { status: 'nicht_gelesen', quelle: 'keine', fuenf_stunden: null, woche: null, plaetze: Math.min(wunsch, OHNE_ABLESUNG), gebremst: false, grund: (r && r.grund) || 'verbrauch.js ohne verwertbares Ergebnis' }
}
ablesungen.push({ anlass, ...r })
log(`Verbrauch ${anlass}: ${r.status}, ${r.plaetze} Plätze${r.gebremst ? ', gebremst' : ''} (${r.grund})`)
return r
}
function auftrag(a, t, jeNr) {
const vor = t.voraussetzungen.map(n => jeNr.get(n).datei)
const text = a.taskauftrag
.split('{NR}').join(String(t.nr).padStart(2, '0'))
.split('{THEMA}').join(t.thema)
.split('{DATEI}').join(t.datei)
.split('{WELLE}').join(t.welle === undefined ? '' : String(t.welle))
.split('{MODELL}').join(t.modell)
.split('{VORAUSSETZUNGEN}').join(vor.length ? vor.join(', ') : '(keine)')
const lies = vor.length ? ` Die Taskdateien deiner Voraussetzungen liegen fertig im Planordner: ${vor.join(', ')}. Lies sie, bevor du einen Namen, eine Signatur oder einen Import daraus übernimmst.` : ''
return `${a.gemeinsam}
${text}
Schreib genau eine Datei: ${a.planordner}/${t.datei}.${lies}
${a.zusatz ? a.zusatz + '\n' : ''}Übertrage deinen Schlussbericht in die Felder des StructuredOutput-Werkzeugs.`
}
pruefeArgs(args)
phase('Taskdateien')
const jeNr = new Map(args.tasks.map(t => [t.nr, t]))
const status = new Map(args.tasks.map(t => [t.nr, (args.erledigt || []).includes(t.nr) ? 'erledigt' : 'offen']))
const reihenfolge = [...args.tasks].sort((x, y) => ((x.welle || 0) - (y.welle || 0)) || (x.nr - y.nr))
const wunsch = args.plaetze || VORGABE_PLAETZE
const nachlesenAlle = args.nachlesen_alle || NACHLESEN_ALLE
const geschrieben = []
const gescheitert = []
const plaetzeVerlauf = []
const laufend = new Map()
let gebremst = false
let abgebrochen = false
let plaetze = 0
let fertigSeitAblesung = 0
const nochOffen = () => [...status.values()].some(s => s === 'offen')
function starte(t) {
status.set(t.nr, 'laeuft')
const opts = { label: `planer:task-${String(t.nr).padStart(2, '0')}`, phase: 'Taskdateien', schema: TASK_SCHEMA, agentType: 'planer', model: 'opus' }
laufend.set(t.nr, agent(auftrag(args, t, jeNr), opts).then(r => ({ nr: t.nr, r }), e => ({ nr: t.nr, r: null, fehler: e && e.message })))
}
if (nochOffen()) {
const v = await ablesen('Anfang', wunsch)
plaetzeVerlauf.push(v.plaetze)
if (v.gebremst) gebremst = true
else plaetze = v.plaetze
}
while (true) {
// Tasks hinter einer gescheiterten oder blockierten Voraussetzung starten nie.
let geaendert = true
while (geaendert) {
geaendert = false
for (const t of args.tasks) {
if (status.get(t.nr) === 'offen' && t.voraussetzungen.some(n => ['gescheitert', 'blockiert'].includes(status.get(n)))) {
status.set(t.nr, 'blockiert')
geaendert = true
}
}
}
if (!gebremst && !abgebrochen) {
for (const t of reihenfolge) {
if (laufend.size >= plaetze) break
if (status.get(t.nr) === 'offen' && t.voraussetzungen.every(n => ['fertig', 'erledigt'].includes(status.get(n)))) starte(t)
}
}
if (laufend.size === 0) break
const { nr, r: roh, fehler: grund } = await Promise.race(laufend.values())
laufend.delete(nr)
const passt = roh && typeof roh.datei === 'string' && ('/' + roh.datei.replace(/\\/g, '/')).endsWith('/' + jeNr.get(nr).datei)
const r = passt ? roh : null
const fehler = grund || (roh && !passt ? `Ergebnis nennt ${JSON.stringify(roh.datei)} statt ${jeNr.get(nr).datei}` : null)
if (r) {
status.set(nr, 'fertig')
geschrieben.push({ nr, ...r })
} else {
status.set(nr, 'gescheitert')
gescheitert.push(nr)
log(`Task ${nr}: planer ohne Ergebnis${fehler ? ' (' + fehler + ')' : ''}`)
if (gescheitert.length >= ABBRUCH_NACH && !abgebrochen) {
abgebrochen = true
log(`${gescheitert.length} Planer ohne Ergebnis, vermutlich Kontingent: kein weiterer Start`)
}
}
fertigSeitAblesung++
if (!gebremst && !abgebrochen && fertigSeitAblesung >= nachlesenAlle && nochOffen()) {
fertigSeitAblesung = 0
const v = await ablesen(`nach ${geschrieben.length + gescheitert.length} Tasks`, wunsch)
plaetzeVerlauf.push(v.plaetze)
if (v.gebremst) {
gebremst = true
log('Planung gebremst: kein weiterer Start, die laufenden Planer enden')
} else plaetze = v.plaetze
}
}
if (geschrieben.length + gescheitert.length > 0) await ablesen('Ende', 0)
const liste = s => args.tasks.filter(t => status.get(t.nr) === s).map(t => t.nr)
const blockiert = liste('blockiert')
const offen = liste('offen')
const zustand = gebremst ? 'gebremst' : abgebrochen ? 'abgebrochen' : gescheitert.length || blockiert.length || offen.length ? 'unvollstaendig' : 'fertig'
return {
status: zustand,
geschrieben,
gescheitert,
blockiert,
offen,
verbrauch: ablesungen,
plaetze_verlauf: plaetzeVerlauf,
naechster_schritt: zustand === 'fertig'
? 'plankritik über den ganzen Planordner.'
: 'Gescheiterte, blockierte und offene Tasks in einem neuen Lauf nachholen, mit erledigt: [' + args.tasks.filter(t => ['fertig', 'erledigt'].includes(status.get(t.nr))).map(t => t.nr).join(', ') + ']; nicht mit resumeFromRunId fortsetzen.',
}
+88
View File
@@ -0,0 +1,88 @@
export const meta = {
name: 'pruefung',
description: 'Lässt eine fertige Welle vom pruefer mit dem vollen Pflichtprogramm gegen Plan und Zielumgebung prüfen; kritische Änderungen mit xhigh; schreibt den Planverbrauch vor und nach der Prüfung mit',
whenToUse: 'Schritt 4 der Standardschleife nach einer Welle, zusätzlich zur eigenen Gegenprüfung des Koordinators (AI/Regelwerk/codeworkflow.md). Args: {projekt, plan, tasks?, suite?, version?, kritisch?, abschluss?, schwerpunkt?, effort?, verbrauch?}. kritisch für sicherheits- oder datenrelevante Änderungen; abschluss fragt, ob eine weitere Runde noch Substanz hat; verbrauch: false schaltet die Ablesung ab, welle-mit-pruefung liest dann selbst.',
phases: [{ title: 'Prüfung', detail: 'ein pruefer' }],
}
// Rollenparameter: model wird immer gesetzt (Stamm). Der Effort kommt aus dem Frontmatter
// von .claude/agents/pruefer.md (high). kritisch hebt auf xhigh: In der Messung vom
// 2026-09-23 fuhr nur xhigh einen großen eigenen Datenlauf (codeworkflow.md, "Effort je Rolle").
// Verbrauch: Vor und nach dem pruefer liest verbrauch.js den Planverbrauch ab, nur zum Messen
// (wunsch 0); gebremst wird hier nicht. Als Kind von welle-mit-pruefung kann dieser Workflow
// keinen weiteren aufrufen; dort liest der Elternworkflow und gibt verbrauch: false mit.
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
const BERICHT = {
type: 'object',
properties: {
selbst_ausgefuehrt: { type: 'array', items: { type: 'string' } },
eigener_lauf: { type: 'array', items: { type: 'string' } },
befunde: {
type: 'array',
items: {
type: 'object',
properties: {
schweregrad: { type: 'string', enum: ['blockierend', 'wichtig', 'klein'] },
datei: { type: 'string' },
zeile: { type: 'integer' },
beschreibung: { type: 'string' },
reproduktion: { type: 'string' },
},
required: ['schweregrad', 'datei', 'zeile', 'beschreibung', 'reproduktion'],
},
},
plan_vs_umsetzung: { type: 'array', items: { type: 'string' } },
geprueft_und_in_ordnung: { type: 'array', items: { type: 'string' } },
nicht_pruefbar: { type: 'array', items: { type: 'string' } },
weitere_runde: { type: 'string', description: 'nur bei Abschlussprüfung: hat eine weitere Runde noch Substanz, und warum' },
},
required: ['selbst_ausgefuehrt', 'eigener_lauf', 'befunde', 'plan_vs_umsetzung', 'geprueft_und_in_ordnung', 'nicht_pruefbar'],
}
function pruefeArgs(a) {
const fehlt = ['projekt', 'plan'].filter(k => !a || !a[k])
if (fehlt.length) throw new Error('pruefung: args fehlen: ' + fehlt.join(', '))
if (a.effort !== undefined && !EFFORTS.includes(a.effort)) throw new Error('pruefung: effort "' + a.effort + '" ungültig')
if (a.tasks !== undefined && !Array.isArray(a.tasks)) throw new Error('pruefung: tasks muss eine Liste sein')
if (a.verbrauch !== undefined && typeof a.verbrauch !== 'boolean') throw new Error('pruefung: verbrauch muss true oder false sein')
}
function auftrag(a) {
const umfang = a.tasks && a.tasks.length ? `Geprüft wird die Umsetzung dieser Tasks: ${a.tasks.join(', ')}.` : 'Geprüft wird die gesamte Umsetzung des Plans.'
return `Prüfe die Umsetzung im Projekt ${a.projekt} gegen den Plan ${a.plan}. ${umfang}
Der Executer-Bericht ist eine Behauptung. Fahre das Pflichtprogramm deiner Agentendefinition: Tests selbst ausführen${a.suite ? ` (${a.suite})` : ''}, die zentralen Artefakte selbst lesen, einen eigenen Lauf mit realistischen Daten und eigenem Prüfcode. Eigenen Prüfcode führst du als python -c oder in einem Wegwerfordner unter dem System-Temp aus; im Projekt änderst du nichts.
${a.version ? `Aussagen über fremde Software belegst du am tatsächlich eingesetzten Versionsstand: ${a.version}.\n` : ''}${a.schwerpunkt ? `Schwerpunkt: ${a.schwerpunkt}\n` : ''}Melde jeden Befund mit Schweregrad, auch unsichere; der Koordinator filtert.
${a.abschluss ? 'Das ist die Abschlussprüfung dieser Runde. Sag im Feld weitere_runde, ob eine weitere Runde noch Substanz hat oder nur noch Text umschichtet, und warum.\n' : ''}Übertrage deinen Schlussbericht in die Felder des StructuredOutput-Werkzeugs.`
}
// Eine messende Hilfsfunktion ablesen() steht auch in welle-mit-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) {
if (args.verbrauch === false) return
let r = null
try {
r = await workflow({ scriptPath: VERBRAUCH }, { anlass: '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)
phase('Prüfung')
const opts = { label: 'pruefung' + (args.kritisch ? ':kritisch' : ''), phase: 'Prüfung', schema: BERICHT, agentType: 'pruefer', model: 'opus' }
if (args.effort) opts.effort = args.effort
else if (args.kritisch) opts.effort = 'xhigh'
await ablesen('vor Prüfung')
const bericht = await agent(auftrag(args), opts)
await ablesen('nach Prüfung')
if (!bericht) throw new Error('pruefung: der pruefer hat keinen Bericht geliefert')
const RANG = { blockierend: 0, wichtig: 1, klein: 2 }
const zaehlung = { blockierend: 0, wichtig: 0, klein: 0 }
bericht.befunde.forEach(b => { zaehlung[b.schweregrad]++ })
return { effort: opts.effort || '(Frontmatter)', zaehlung, ...bericht, befunde: [...bericht.befunde].sort((x, y) => RANG[x.schweregrad] - RANG[y.schweregrad]), verbrauch: ablesungen }
+658
View File
@@ -0,0 +1,658 @@
export const meta = {
name: 'quorum',
description: 'Quorum am Meilenstein: Opus, Sonnet und Codex (Ersatz Haiku) prüfen unabhängig denselben Unterschied seit dem letzten Quorum; was zwei melden, gilt, Einzelbefunde prüft Sonnet nach',
whenToUse: 'Vor dem Einspielen, vor Merge und Push einer neuen Minor- oder Major-Version und vor der Übergabe eines Kundendokuments (AI/Regelwerk/codeworkflow.md, "Quorum am Meilenstein"). Args: {anlass: "einspielen"|"version"|"kundendokument", projekt?, basis?, ordner?, dateien?, uebersicht?, kontext?: [pfade], suite?, suite_datei?, codex?: false, codex_runden?: 1..6}. Einspielen und version brauchen projekt (Wurzel eines Git-Repos, sauberer Arbeitsbaum), das erste Quorum dort zusätzlich basis. suite nur für eine Suite, die in neun Minuten durchläuft und keine unversionierten Dateien schreibt; sonst die Suite selbst fahren und suite_datei nennen. Kundendokument braucht ordner (im Vault unter D:/Claude/), dateien (Pfade relativ zu ordner) und uebersicht. Rückgabe mit status fertig, leer, gebremst oder unvollstaendig, urteil frei, nicht_frei oder kein_urteil, dazu bericht_pfad, bericht_md, paket und ablegen_befehl: bericht_md als bericht.md im Ordner paket speichern und ablegen_befehl fahren, der den Stand aus paket.json einsetzt. Einzelheiten im Kommentar unter meta.',
phases: [
{ title: 'Paket', detail: 'Hilfsagent baut das Prüfpaket mit quorum_werkzeug.py' },
{ title: 'Stimmen', detail: 'Opus und Sonnet als quorum-stimme, Codex über Hilfsagenten, Ersatz Haiku' },
{ title: 'Zusammenführen', detail: 'ein Sonnet-Agent gruppiert, das Skript zählt die Stimmen' },
{ title: 'Nachprüfen', detail: 'ein Sonnet-Agent prüft Einzelbefunde der Schwere blockierend und wichtig' },
],
}
// Diese Datei trägt LF (Erinnerung gespeicherter-workflow-per-name-scheitert). Regel und
// Auslöser stehen in AI/Regelwerk/codeworkflow.md, "Quorum am Meilenstein"; hier steht nur, wie
// der Ablauf gebaut ist. Plan: .vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/.
//
// Paket: Ein Workflow-Skript hat weder Dateisystem noch Shell. Das Prüfpaket baut deshalb ein
// Hilfsagent (sonnet, low), der genau einen Befehl fährt: quorum_werkzeug.py paket. Das Skript
// entscheidet, ob der Unterschied leer ist; der Hilfsagent überträgt nur die Ausgabe.
// Die Zeile QUORUM_PAKET trägt nur Felder fester Größe. Dateiliste und Stand-Liste bleiben in
// paket.json; den Stand setzt quorum_werkzeug.py ablegen beim Ablegen des Berichts aus paket.json
// ein, der Bericht trägt bis dahin die Zeile "stand: PAKET". So schreibt kein Agent den Stand ab.
// Stimmen: Opus und Sonnet laufen als agentType quorum-stimme, Effort aus deren Frontmatter.
// Vor der Opus-Stimme liest verbrauch.js mit wunsch 1 ab; gebremst heißt, keine Stimme startet.
// Codex über das Plugin codex 1.0.6, nur lesend; cc heißt scripts/codex-companion.mjs:
// - task ohne --write fährt read-only (cc 491), liest den Auftrag aus --prompt-file (cc 643-645)
// und braucht kein Git (cc 461-495). review und adversarial-review brauchen Git (cc 360),
// adversarial-review hat ein festes Ausgabeschema ohne Feld für den Lesebeleg (cc 415).
// - Im Hintergrund gestartet (cc 788-805) wartet je ein Hilfsagent mit status <job> --wait
// höchstens WARTEN_MS (cc 318-334, 891-897), weil das Bash-Werkzeug nach zehn Minuten endet.
// result <job> --json liefert Job und Ergebnis (cc 910-926); quorum_werkzeug.py codex zählt
// "usage limit" im Log des Jobs und zieht die Antwort heraus.
// - Gültig ist die Codex-Stimme nur mit gelesenem Status vor dem Lauf, Job completed, gelesenem
// Log ohne "usage limit" und einem Lesebeleg mit der Lesemarke aus paket.md. Sonst Haiku.
// - Die Codex-Hilfsagenten laufen auf sonnet mit low: feste Befehle, Ausgaben übertragen. Haiku
// nicht, weil Befundtexte typografische Zeichen tragen können, die Haiku still ersetzt.
// Zusammenführen: Ein Sonnet-Agent ohne agentType gruppiert nur belegte Befunde; das Skript teilt
// jede Gruppe nach Datei und Zeilen in zusammenhängende Teilgruppen (Funktion teilgruppen) und
// zählt die Stimmen. Mehrheit heißt zwei verschiedene Stimmen, die Schwere der Gruppe ist die
// höchste ihrer Mitglieder. quorum-stimme passt hier nicht, weil dessen Auftragsarten den Auftrag
// verfälschen; dass der Agent keine Datei liest, sagt nur sein Auftrag, ein Fall sichert den Satz.
// Nachprüfen: quorum-stimme auf sonnet für Gruppen ohne Mehrheit mit Schwere blockierend oder
// wichtig. Ein blockierender Einzelbefund ohne Urteil sperrt wie ein bestätigter.
// Urteil: frei heißt kein bestätigter und kein ungeprüfter blockierender Befund mit Beleg; ein
// blockierender Befund ohne Beleg sperrt nicht (Plan, Offener Punkt 8), der Grund nennt ihn. Mit weniger als
// zwei abgegebenen Stimmen gibt es kein Urteil und keinen Bericht, die Basis bleibt die alte.
// Laufzeit: Date.now() und new Date() gibt es im Laufzeitsystem nicht. Die Codex-Laufzeit kommt
// aus dem Job, die der Claude-Stimmen steht im Bericht als nicht gemessen.
// Bericht: Freitext aus Stimmen, Zusammenführen und Nachprüfung geht nur durch klartext() in
// bericht_md. Ein zitierter Wikilink, eine Einbettung oder ein Tag würde im Vault sonst lebendig;
// Hub-Zeile und Frontmatter baut der Workflow selbst, sie bleiben echte Links bzw. Felder.
const ANLAESSE = ['einspielen', 'version', 'kundendokument']
const RANG = { blockierend: 0, wichtig: 1, hinweis: 2 }
const KUERZEL = { opus: 'O', sonnet: 'S', codex: 'C', haiku: 'H' }
const NAMEN = { opus: 'Opus', sonnet: 'Sonnet', codex: 'Codex', haiku: 'Haiku' }
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
const WERKZEUG = 'D:/Claude/.vault-tools/quorum/quorum_werkzeug.py'
const VAULT = 'D:/Claude/'
const COMPANION = '"$HOME/.claude/plugins/cache/openai-codex/codex/1.0.6/scripts/codex-companion.mjs"'
const CODEX_RUNDEN = 3
const WARTEN_MS = 540000
const OHNE_ABLESUNG = 2
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
const LANGE = 'Setze im Bash-Werkzeug den Parameter timeout auf 600000; der Befehl darf bis zu zehn Minuten laufen.'
const PAKET_FELDER = ['ordner', 'anlass', 'art', 'wurzel', 'leer', 'basis', 'stand', 'anzahl_dateien', 'lesemarke', 'bericht_pfad', 'nummer', 'datum', 'vorher', 'suite', 'hinweise', 'paket_summe']
const STATUS_TEXT = { mehrheit: 'Mehrheit', bestaetigt: 'bestätigt nach Nachprüfung', widerlegt: 'widerlegt nach Nachprüfung', nicht_geprueft: 'nicht geprüft', hinweis_ungeprueft: 'Hinweis, ungeprüft' }
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
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'],
}
const BEFUND = {
type: 'object',
properties: {
datei: { type: 'string', description: 'Pfad relativ zur Wurzel aus dem Auftrag' },
stelle: { type: 'string', description: 'Zeile, Zeilenbereich oder Abschnitt' },
schwere: { type: 'string', enum: ['blockierend', 'wichtig', 'hinweis'] },
aussage: { type: 'string' },
beleg: { type: 'string', description: 'wörtlich zitierte Zeile oder Verweis auf das Paket; leer heißt unbelegt' },
},
required: ['datei', 'stelle', 'schwere', 'aussage', 'beleg'],
}
const STIMME = {
type: 'object',
properties: {
lesemarke: { type: 'string', description: 'der Wert hinter Lesemarke: in paket.md' },
befunde: { type: 'array', items: BEFUND },
nicht_pruefbar: { type: 'array', items: { type: 'string' } },
},
required: ['lesemarke', 'befunde', 'nicht_pruefbar'],
}
const CODEX = {
type: 'object',
properties: {
exit_codex: { type: 'integer' },
job_status: { type: 'string' },
log_gelesen: { type: 'boolean' },
usage_limit_treffer: { type: 'integer' },
antwort_gefunden: { type: 'boolean' },
dauer_s: { type: 'integer' },
lesebeleg: { type: 'string' },
befunde: { type: 'array', items: BEFUND },
nicht_pruefbar: { type: 'array', items: { type: 'string' } },
},
required: ['exit_codex', 'job_status', 'log_gelesen', 'usage_limit_treffer', 'antwort_gefunden', 'dauer_s', 'lesebeleg', 'befunde', 'nicht_pruefbar'],
}
const GRUPPEN = {
type: 'object',
properties: {
gruppen: { type: 'array', items: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string' } }, begruendung: { type: 'string' } }, required: ['ids', 'begruendung'] } },
},
required: ['gruppen'],
}
const URTEILE = {
type: 'object',
properties: {
urteile: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, urteil: { type: 'string', enum: ['bestaetigt', 'widerlegt'] }, fundstelle: { type: 'string' }, begruendung: { type: 'string' } }, required: ['id', 'urteil', 'fundstelle', 'begruendung'] } },
},
required: ['urteile'],
}
function pruefeArgs(a) {
if (!a || typeof a !== 'object') throw new Error('quorum: args fehlen')
if (!ANLAESSE.includes(a.anlass)) throw new Error('quorum: anlass muss einspielen, version oder kundendokument sein, ist ' + JSON.stringify(a.anlass))
for (const k of ['projekt', 'basis', 'ordner', 'uebersicht', 'suite', 'suite_datei']) {
if (a[k] !== undefined && (typeof a[k] !== 'string' || !a[k].trim())) throw new Error('quorum: ' + k + ' muss ein nicht leerer Text sein')
}
if (a.anlass === 'kundendokument') {
const fehlt = ['ordner', 'uebersicht'].filter(k => a[k] === undefined)
if (fehlt.length) throw new Error('quorum: args fehlen: ' + fehlt.join(', '))
if (!Array.isArray(a.dateien) || !a.dateien.length || !a.dateien.every(x => typeof x === 'string' && x.trim())) throw new Error('quorum: dateien muss eine nicht leere Liste von Pfaden relativ zu ordner sein')
if (new Set(a.dateien).size !== a.dateien.length) throw new Error('quorum: eine Datei steht doppelt in dateien')
const fremd = ['projekt', 'basis', 'suite', 'suite_datei'].filter(k => a[k] !== undefined)
if (fremd.length) throw new Error('quorum: ' + fremd.join(', ') + ' gilt nur für einspielen und version')
if (vaultPfad(a.ordner) === null) throw new Error('quorum: ordner muss im Vault unter ' + VAULT + ' liegen; der Bericht verlinkt seine Übersicht mit vollem Vault-Pfad')
} else {
if (a.projekt === undefined) throw new Error('quorum: args fehlen: projekt')
const fremd = ['ordner', 'dateien', 'uebersicht'].filter(k => a[k] !== undefined)
if (fremd.length) throw new Error('quorum: ' + fremd.join(', ') + ' gilt nur für kundendokument')
}
if (a.suite !== undefined && a.suite_datei !== undefined) throw new Error('quorum: suite und suite_datei schließen sich aus')
if (a.kontext !== undefined && !(Array.isArray(a.kontext) && a.kontext.every(x => typeof x === 'string' && x.trim()))) throw new Error('quorum: kontext muss eine Liste von Pfaden sein')
if (a.codex !== undefined && typeof a.codex !== 'boolean') throw new Error('quorum: codex muss true oder false sein')
if (a.codex_runden !== undefined && !(Number.isInteger(a.codex_runden) && a.codex_runden >= 1 && a.codex_runden <= 6)) throw new Error('quorum: codex_runden muss eine ganze Zahl von 1 bis 6 sein')
}
function werkzeugAgent(label, phase, befehl, zweck, zusatz) {
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}
${zusatz ? '\n' + zusatz + '\n' : ''}
Ä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: 'sonnet', effort: 'low' })
}
function ausgabe(r) {
return r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''
}
function wurzel(a) {
return a.anlass === 'kundendokument' ? a.ordner : a.projekt
}
// Pfad relativ zur Vault-Wurzel, ohne Schrägstrich am Ende; null außerhalb des Vaults.
function vaultPfad(ordner) {
const o = String(ordner).replace(/\\/g, '/').replace(/\/+$/, '')
return o.toLowerCase().startsWith(VAULT.toLowerCase()) ? o.slice(VAULT.length) : null
}
const laufHinweise = []
function hinweis(text) {
laufHinweise.push(text)
log(text)
}
function paketBefehl(a) {
const teile = [`${PY} ${q(WERKZEUG)} paket --anlass ${a.anlass} --ziel "$T"`]
if (a.anlass === 'kundendokument') teile.push(`--ordner ${q(a.ordner)} --uebersicht ${q(a.uebersicht)} --dateien ${a.dateien.map(q).join(' ')}`)
else teile.push(`--projekt ${q(a.projekt)}${a.basis ? ' --basis ' + q(a.basis) : ''}`)
if (a.kontext && a.kontext.length) teile.push(`--kontext ${a.kontext.map(q).join(' ')}`)
if (a.suite_datei) teile.push(`--suite-datei ${q(a.suite_datei)}`)
if (a.suite) teile.push('--suite-datei "$T/suite.txt"')
// Schreibt die Suite Dateien in den Arbeitsbaum, scheitert das Paket an "nicht sauber"; die
// BEFUND-Zeile davor nennt dann die Suite als Ursache. GIT_OPTIONAL_LOCKS=0, damit status
// .git/index nicht auffrischt und keinem git-Befehl des Koordinators das index.lock nimmt.
const baum = `GIT_OPTIONAL_LOCKS=0 git -C ${q(a.projekt)} status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum'`
const suite = a.suite ? `V=$(${baum}); (cd ${q(a.projekt)} && ${a.suite}) > "$T/suite.txt" 2>&1; echo "EXIT suite: $?" >> "$T/suite.txt"; [ "$V" = "$(${baum})" ] || echo "BEFUND: Die Suite hat den Arbeitsbaum verändert; suite_datei nutzen oder ihre Ausgaben in .gitignore aufnehmen"; ` : ''
return `T=$(cygpath -m -l "$(mktemp -d)") && ${suite}${teile.join(' ')}; echo "EXIT paket: $?"`
}
function lesePaket(r) {
const aus = ausgabe(r)
const m = /^QUORUM_PAKET (\{.*\})\s*$/m.exec(aus)
if (!/EXIT paket: 0(?!\d)/.test(aus) || !m) {
const zeilen = aus.split('\n').filter(z => /^(BEFUND|ABBRUCH|EXIT)/.test(z)).join('; ')
return { fehler: 'Paket nicht gebaut: ' + (zeilen || 'Hilfsagent ohne verwertbare Ausgabe') }
}
let p = null
try {
p = JSON.parse(m[1])
} catch (e) {
return { fehler: 'QUORUM_PAKET ist kein JSON: ' + (e && e.message) }
}
const fehlt = PAKET_FELDER.filter(k => !p || typeof p !== 'object' || !(k in p))
if (fehlt.length) return { fehler: 'QUORUM_PAKET ohne ' + fehlt.join(', ') }
if (!/^[0-9a-f]{16}$/.test(p.lesemarke)) return { fehler: 'QUORUM_PAKET mit unbrauchbarer Lesemarke ' + JSON.stringify(p.lesemarke) }
if (!/^[0-9a-f]{64}$/.test(p.paket_summe)) return { fehler: 'QUORUM_PAKET mit unbrauchbarer paket_summe ' + JSON.stringify(p.paket_summe) }
if (!Number.isInteger(p.anzahl_dateien) || p.anzahl_dateien < 0) return { fehler: 'QUORUM_PAKET: anzahl_dateien muss eine ganze Zahl ab 0 sein, ist ' + JSON.stringify(p.anzahl_dateien) }
if (!Array.isArray(p.hinweise)) return { fehler: 'QUORUM_PAKET: hinweise muss eine Liste sein' }
return { paket: p }
}
// Eine Hilfsfunktion ablesen() steht auch in plankritik.js und planung.js; die Regel (Fenster,
// Stufen, Rückfall) steht nur in verbrauch.js. Hier steht nur der Rückfall für den Fall, dass
// verbrauch.js selbst nicht läuft, etwa weil dieser Workflow als Kind läuft.
const ablesungen = []
async function ablesen(anlass, wunsch) {
let r = null
try {
r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'quorum:' + anlass, wunsch })
} catch (e) {
r = { status: 'nicht_gelesen', grund: 'verbrauch.js nicht ausführbar: ' + (e && e.message) }
}
if (!r || typeof r.plaetze !== 'number') {
r = { status: 'nicht_gelesen', quelle: 'keine', fuenf_stunden: null, woche: null, plaetze: Math.min(wunsch, OHNE_ABLESUNG), gebremst: false, grund: (r && r.grund) || 'verbrauch.js ohne verwertbares Ergebnis' }
}
ablesungen.push({ anlass, ...r })
log(`Verbrauch ${anlass}: ${r.status}, ${r.plaetze} Plätze${r.gebremst ? ', gebremst' : ''} (${r.grund})`)
return r
}
function stimmenAuftrag(a, p) {
return `Auftragsart: Stimme.
Prüfpaket: ${p.ordner}/paket.md
Wurzel: ${wurzel(a)}
Anlass: ${a.anlass}
Lies das Prüfpaket vollständig und prüfe, was seit der Basis dazukam, nach deiner Agentendefinition. Die anderen Stimmen kennst du nicht. Du änderst nichts und fährst keine Tests; das Suite-Ergebnis steht im Paket.
Übertrage die Lesemarke aus dem Paket, deine Befunde und was du nicht prüfen konntest in die Felder des StructuredOutput-Werkzeugs.`
}
function claudeStimme(e, p) {
if (!e) return { abgegeben: false, grund: 'ohne Ergebnis', dauer_s: -1, befunde: [], nicht_pruefbar: [], lesemarke_ok: false }
return { abgegeben: true, grund: '', dauer_s: -1, befunde: e.befunde, nicht_pruefbar: e.nicht_pruefbar, lesemarke_ok: String(e.lesemarke).includes(p.lesemarke) }
}
function codexEndeAuftrag(a, p, job) {
const datei = q(p.ordner + '/codex-ergebnis.json')
const befehl = `node ${COMPANION} result ${q(job)} --json --cwd ${q(wurzel(a))} > ${datei}; echo "EXIT result: $?"; ${PY} ${q(WERKZEUG)} codex --ergebnis ${datei}; echo "EXIT codex: $?"`
return `Du führst einen Werkzeugbefehl aus und überträgst seine Ausgabe in feste Felder. Zweck: das Ergebnis des Codex-Auftrags auswerten.
Führe genau diesen Befehl einmal im Bash-Werkzeug (Git-Bash) aus, unverändert:
${befehl}
Übertrage aus der Ausgabe:
- die Zahl hinter "EXIT codex:" nach exit_codex,
- die Werte der Zeilen job_status, usage_limit_treffer und dauer_s unverändert in die gleichnamigen Felder,
- log_gelesen und antwort_gefunden als true bei "ja", sonst als false,
- aus dem Block unter "ANTWORT-JSON:" die Felder lesebeleg, befunde (je Befund datei, stelle, schwere, aussage, beleg) und nicht_pruefbar, Zeichen für Zeichen.
Steht unter "ANTWORT-JSON:" nur "keine", setze lesebeleg auf "", befunde und nicht_pruefbar auf []. Fehlt eine Zeile, setze job_status auf "", die Zahlen auf -1 und die Wahrheitswerte auf false.
Ändere keine Datei außer denen, die der Befehl selbst anlegt. Deute nichts, fasse nichts zusammen und ergänze keinen Befund. Übertrage das Ergebnis in die Felder des StructuredOutput-Werkzeugs.`
}
function codexUngueltig(e, p) {
if (!e) return 'Auswertung ohne Ergebnis'
if (e.exit_codex !== 0) return `Auswertung endete mit Exit ${e.exit_codex}`
if (e.job_status !== 'completed') return `Codex-Auftrag endete mit ${e.job_status || 'unbekanntem Status'}`
if (e.log_gelesen !== true) return 'Log nicht gelesen'
if (e.usage_limit_treffer !== 0) return '"usage limit" im Log'
if (e.antwort_gefunden !== true) return 'keine Antwort im vereinbarten Format'
if (!String(e.lesebeleg).includes('Lesemarke: ' + p.lesemarke)) return 'Lesebeleg fehlt oder zitiert eine andere Lesemarke'
return null
}
async function codexStimme(a, p) {
const z = { abgegeben: false, grund: '', job: null, dauer_s: -1, befunde: [], nicht_pruefbar: [] }
if (a.codex === false) {
z.grund = 'im Auftrag abgeschaltet (codex: false)'
return z
}
const P = q(wurzel(a))
const start = ausgabe(await werkzeugAgent('codex:start', 'Stimmen',
`node ${COMPANION} status --json --cwd ${P}; S=$?; echo "EXIT status: $S"; if [ "$S" -eq 0 ]; then node ${COMPANION} task --background --json --cwd ${P} --prompt-file ${q(p.ordner + '/codex-auftrag.md')}; echo "EXIT task: $?"; fi`,
'den Codex-Status vor dem Lauf lesen und die Prüfung nur lesend im Hintergrund starten'))
if (!/EXIT status: 0(?!\d)/.test(start)) {
z.grund = 'Status vor dem Lauf nicht gelesen'
return z
}
// Nur hinter "EXIT status:" suchen: status --json listet frühere Jobs derselben Sitzung samt
// request.jobId (job-control.mjs 161-181); die erste Fundstelle wäre dann ein alter Job.
const job = /"jobId": "([^"]+)"/.exec(start.slice(start.lastIndexOf('EXIT status:')))
if (!/EXIT task: 0(?!\d)/.test(start) || !job) {
z.grund = 'Codex-Auftrag nicht gestartet'
return z
}
z.job = job[1]
const runden = a.codex_runden || CODEX_RUNDEN
let fertig = false
let gescheitert = false
for (let runde = 1; runde <= runden && !fertig && !gescheitert; runde++) {
const w = ausgabe(await werkzeugAgent('codex:warten:' + runde, 'Stimmen',
`node ${COMPANION} status ${q(z.job)} --wait --timeout-ms ${WARTEN_MS} --json --cwd ${P}; echo "EXIT warten: $?"`,
'auf das Ende des Codex-Auftrags warten', LANGE))
if (!/EXIT warten: 0(?!\d)/.test(w)) gescheitert = true
else fertig = /"waitTimedOut": false/.test(w)
}
if (!fertig) {
await werkzeugAgent('codex:abbruch', 'Stimmen', `node ${COMPANION} cancel ${q(z.job)} --json --cwd ${P}; echo "EXIT cancel: $?"`, 'den unfertigen Codex-Auftrag abbrechen, damit er kein Kontingent mehr verbraucht')
z.grund = gescheitert ? 'Warten auf Codex gescheitert, Auftrag abgebrochen' : `nach ${runden} Warterunden nicht fertig, Auftrag abgebrochen`
return z
}
const e = await agent(codexEndeAuftrag(a, p, z.job), { label: 'codex:ende', phase: 'Stimmen', schema: CODEX, model: 'sonnet', effort: 'low' })
if (e && Number.isInteger(e.dauer_s)) z.dauer_s = e.dauer_s
const ungueltig = codexUngueltig(e, p)
if (ungueltig) {
z.grund = ungueltig
return z
}
z.abgegeben = true
z.befunde = e.befunde
z.nicht_pruefbar = e.nicht_pruefbar
return z
}
function zeile(b) {
return `[${b.id}] Stimme ${NAMEN[b.stimme]} | ${b.datei} | ${b.stelle} | ${b.schwere}\nAussage: ${b.aussage}\nBeleg: ${b.beleg}`
}
function zusammenfuehrenAuftrag(befunde) {
return `Drei Prüfstimmen haben unabhängig voneinander denselben Stand geprüft. Unten stehen ihre belegten Befunde, je Befund eine ID. Lies keine Dateien und fahre keine Befehle; du arbeitest nur mit dieser Liste.
Fasse Befunde zu Gruppen zusammen, die denselben Fehler meinen. Gleich sind zwei Befunde nur, wenn Datei und Stelle übereinstimmen und die Aussage denselben Fehler beschreibt; eine Stelle stimmt überein, wenn sie dieselbe Zeile, dieselben Zeilen oder denselben Abschnitt meint. Im Zweifel bleiben Befunde getrennt, denn eine falsche Gruppe erzeugt eine falsche Mehrheit. Jede ID steht in genau einer Gruppe, ein Befund ohne Partner ist eine Gruppe für sich. Zähle nichts und bewerte nichts.
<befunde>
${befunde.map(zeile).join('\n\n')}
</befunde>
Übertrage je Gruppe die IDs und eine kurze Begründung in die Felder des StructuredOutput-Werkzeugs.`
}
function pfadnorm(p) {
return String(p).replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '').toLowerCase()
}
// Stelle als Zeilenbereich [von, bis], oder null ohne Zeilennummer; dann gilt das Urteil des
// Zusammenführ-Agenten. Gelesen werden "12", "12-14", "Zeile 12", "Zeilen 10-14", "Z. 10 bis 14",
// "L12", "line 12" und "a.py:42", auch mitten im Text wie "Zeile 42 (Funktion f)"; ein Bereich
// darf Bindestrich, Gedankenstrich, "bis" oder "to" tragen. Zuerst zählt eine Angabe mit Wort,
// dann eine hinter Dateiendung und Doppelpunkt, zuletzt die reine Zahl.
function zeilen(stelle) {
const s = String(stelle)
const m = /(?:^|[^\p{L}\d])(?:zeilen?|z\.|lines?|l)\s*(\d+)(?:\s*(?:-|–|bis|to)\s*l?\s*(\d+))?/iu.exec(s)
|| /\.[a-z0-9]+:(\d+)(?:\s*(?:-|–)\s*(\d+))?/i.exec(s)
|| /^\s*(\d+)(?:\s*(?:-|–|bis|to)\s*(\d+))?\s*$/i.exec(s)
if (!m) return null
const von = Number(m[1])
const bis = m[2] === undefined ? von : Number(m[2])
return von <= bis ? [von, bis] : [bis, von]
}
// Zwei Zeilenbereiche gehören zusammen, wenn sie sich mit einer Toleranz von TOLERANZ Zeilen
// überschneiden: "Zeile 42" und "Zeile 45" ja, "Zeile 42" und "Zeile 46" nicht.
const TOLERANZ = 3
function nahe(x, y) {
return x[0] <= y[1] + TOLERANZ && y[0] <= x[1] + TOLERANZ
}
// Teilt die Mitglieder einer Gruppe des Agenten in zusammenhängende Teilgruppen (transitive Hülle):
// Zusammen gehören nur Stellen derselben Datei, zwei Stellen mit Zeilennummern nur, wenn sie nahe
// liegen. Eine Stelle ohne Zeilennummer folgt dem Urteil des Agenten: Sie bleibt bei den Stellen
// ihrer Datei, solange deren Zeilen eine einzige Teilgruppe bilden oder die Datei gar keine Zeilen
// nennt. Zerfallen die Zeilen, steht sie für sich, denn eine falsche Gruppe erzeugt eine falsche
// Mehrheit. Mitglieder und Teilgruppen bleiben in der Reihenfolge des Agenten.
function teilgruppen(mitglieder) {
const bereich = mitglieder.map(b => zeilen(b.stelle))
const datei = mitglieder.map(b => pfadnorm(b.datei))
const eltern = mitglieder.map((b, i) => i)
const finde = i => (eltern[i] === i ? i : finde(eltern[i]))
const verbinde = (i, j) => { eltern[finde(j)] = finde(i) }
for (let i = 0; i < mitglieder.length; i++) {
for (let j = i + 1; j < mitglieder.length; j++) {
if (datei[i] === datei[j] && bereich[i] && bereich[j] && nahe(bereich[i], bereich[j])) verbinde(i, j)
}
}
for (let i = 0; i < mitglieder.length; i++) {
if (bereich[i]) continue
const gleich = mitglieder.map((b, j) => j).filter(j => j !== i && datei[j] === datei[i])
const stuecke = new Set(gleich.filter(j => bereich[j]).map(finde))
if (stuecke.size === 1) verbinde([...stuecke][0], i)
else if (stuecke.size === 0 && gleich.length) verbinde(gleich[0], i)
}
const teile = new Map()
mitglieder.forEach((b, i) => {
if (!teile.has(finde(i))) teile.set(finde(i), [])
teile.get(finde(i)).push(b)
})
return [...teile.values()]
}
// Gruppen des Agenten prüfen: unbekannte IDs fallen weg, eine ID zählt nur in ihrer ersten
// Gruppe, und jede Gruppe zerfällt in ihre Teilgruppen (Funktion teilgruppen); jede Aufteilung
// steht als Hinweis im Bericht. Was keiner Gruppe angehört, bleibt eine Gruppe für sich.
function gruppenBilden(roh, befunde) {
const jeId = new Map(befunde.map(b => [b.id, b]))
const vergeben = new Set()
const gruppen = []
const meldungen = []
const liste = roh && Array.isArray(roh.gruppen) ? roh.gruppen : []
for (const g of liste) {
const mitglieder = []
for (const id of Array.isArray(g.ids) ? g.ids : []) {
if (!jeId.has(id)) meldungen.push(`Zusammenführen: unbekannte ID ${id} verworfen`)
else if (vergeben.has(id)) meldungen.push(`Zusammenführen: ID ${id} steht in zwei Gruppen; nur die erste zählt`)
else {
vergeben.add(id)
mitglieder.push(jeId.get(id))
}
}
if (!mitglieder.length) continue
const teile = teilgruppen(mitglieder)
if (teile.length > 1) meldungen.push(`Zusammenführen: Gruppe ${mitglieder.map(b => b.id).join(', ')} aufgeteilt in ${teile.map(t => t.map(b => b.id).join(', ')).join(' | ')} (${mitglieder.map(b => `${b.id} ${b.datei} ${b.stelle}`).join('; ')})`)
teile.forEach(t => gruppen.push(t))
}
for (const b of befunde) if (!vergeben.has(b.id)) gruppen.push([b])
return { gruppen, meldungen }
}
function nachpruefAuftrag(a, p, gruppen) {
const liste = gruppen.map(g => `Befund ${g.mitglieder[0].id}:\n${g.mitglieder.map(zeile).join('\n')}`).join('\n\n')
return `Auftragsart: Nachprüfung.
Prüfpaket: ${p.ordner}/paket.md
Wurzel: ${wurzel(a)}
Jeder Befund unten stammt von nur einer Stimme. Prüfe jeden gegen den Code bzw. das Dokument im Stand des Pakets, nach deiner Agentendefinition. Stehen unter einer Zeile "Befund <ID>:" mehrere Fassungen, meinen sie denselben Befund; urteile einmal, unter dieser ID.
<befunde>
${liste}
</befunde>
Übertrage je Befund id, urteil, fundstelle und begruendung in die Felder des StructuredOutput-Werkzeugs.`
}
function laufzeit(s) {
return Number.isInteger(s) && s >= 0 ? `${Math.floor(s / 60)} min ${s % 60} s` : 'nicht gemessen'
}
function verbrauchText(anlass) {
const r = ablesungen.find(x => x.anlass === anlass)
return r && r.status === 'ok' && r.fuenf_stunden && r.woche ? `5 Stunden ${r.fuenf_stunden.prozent} %, Woche ${r.woche.prozent} %` : 'nicht gelesen'
}
function urteilText(urteil) {
return urteil === 'frei' ? 'frei' : urteil === 'nicht_frei' ? 'nicht frei' : 'kein Urteil'
}
// Die Zeile stand: PAKET ersetzt quorum_werkzeug.py ablegen durch den Stand aus paket.json.
function kopf(a, p, urteil, summary, codexFeld) {
return ['---', 'type: note', `title: "Quorum ${p.nummer}: ${a.anlass}"`, 'status: done', `summary: "${summary}"`, 'tags: [quorum]', `created: ${p.datum}`, `updated: ${p.datum}`, `nummer: ${p.nummer}`, `anlass: ${a.anlass}`, `urteil: ${urteil}`, `basis: "${p.basis}"`, 'stand: PAKET', `codex: ${codexFeld}`, '---'].join('\n')
}
// Hub-Zeile nach der Hub-Regel (AI/Regelwerk/navigation.md, "Links schreiben und Dateien
// verschieben"): im Code-Projekt ein relativer Markdown-Link aufs README, sonst ein Wikilink mit
// vollem Vault-Pfad auf die Übersicht des Kundendokuments. Der Bericht liegt in <ordner>/quorum/.
function hubZeile(a) {
if (a.anlass !== 'kundendokument') return 'Projekt: [README](../../README.md)'
const rel = vaultPfad(a.ordner)
const teile = rel.split('/')
if (teile[0].toLowerCase() === 'code projekte' && teile.length >= 2) return `Projekt: [README](${'../'.repeat(teile.length - 1)}README.md)`
const u = a.uebersicht.replace(/\\/g, '/')
return `Übersicht: [[${rel}/${u.replace(/\.md$/i, '')}|${u.split('/').pop()}]]`
}
function berichtLeer(a, p, urteil, grund) {
const vorher = p.vorher ? String(p.vorher.pfad).split('/').pop() : 'dem letzten Bericht'
return [kopf(a, p, urteil, `Urteil ${urteil}: ${grund}.`, 'nicht gelaufen'), '', `# Quorum ${p.nummer}: ${a.anlass}`, '', hubZeile(a), '', '## Urteil', '', `**${urteilText(urteil)}**: Seit dem letzten Quorum hat sich nichts geändert. Das Urteil stammt aus ${vorher}; keine Stimme ist gelaufen.`, ''].join('\n')
}
// Freitext als Klartext für Obsidian: Jede öffnende eckige Klammer in einer Folge von zwei oder
// mehr bekommt einen Backslash, damit kein Wikilink und keine Einbettung entsteht. Eine Raute
// bekommt einen Backslash, wenn ihr kein Buchstabe, keine Ziffer, kein Unterstrich und kein
// Backslash vorausgeht und weder Leerraum noch eine zweite Raute folgt; so entsteht kein Tag.
// Die Leseansicht zeigt den Text wie zitiert. Zweimal angewendet ändert sich nichts mehr.
function klartext(s) {
return String(s)
.replace(/\[{2,}/g, folge => folge.replace(/\[/g, '\\['))
.replace(/(^|[^\p{L}\p{N}_\\])#(?=[^\s#])/gu, '$1\\#')
}
function berichtVoll(a, p, x) {
const namen = Object.keys(x.stimmen).filter(n => x.stimmen[n].abgegeben).map(n => NAMEN[n]).join(', ')
const codexFeld = x.stimmen.codex.abgegeben ? 'abgegeben' : 'ersetzt'
const z = [kopf(a, p, x.urteil, `Urteil ${x.urteil}: ${x.grund}; Stimmen ${namen}.`, codexFeld), '', `# Quorum ${p.nummer}: ${a.anlass}`, '', hubZeile(a), '']
z.push('## Urteil', '', `**${urteilText(x.urteil)}**: ${x.grund}.`, '', `Basis ${p.basis}, Stand ${p.stand}; ${p.anzahl_dateien} Datei(en) im Unterschied.`, '')
z.push('## Stimmen', '', '| Stimme | abgegeben | Befunde | belegt | bestätigt | Alleinfunde | Laufzeit |', '|---|---|---|---|---|---|---|')
for (const n of Object.keys(x.stimmen)) {
const s = x.stimmen[n]
const c = x.zaehlung[n]
const name = n === 'haiku' ? 'Haiku (Ersatz für Codex)' : NAMEN[n]
z.push(`| ${name} | ${s.abgegeben ? 'ja' : 'nein: ' + klartext(s.grund)} | ${c.gesamt} | ${c.belegt} | ${c.bestaetigt} | ${c.alleinfunde} | ${laufzeit(s.dauer_s)} |`)
}
z.push('')
if (x.stimmen.haiku) z.push('Haiku stand statt Codex im Quorum. Dieses Quorum war nur mit Claude besetzt.', '')
z.push(`Verbrauch vor den Stimmen: ${verbrauchText('vor Stimmen')}; am Ende: ${verbrauchText('Ende')}.`, '')
z.push('## Befunde', '')
if (!x.gruppen.length) z.push('keine belegten Befunde', '')
for (const g of x.gruppen) {
const erste = g.mitglieder[0]
z.push(`### G${g.nr}: ${g.schwere}, ${STATUS_TEXT[g.status]} (${g.stimmen.map(n => NAMEN[n]).join(', ')})`, '', `\`${erste.datei}\`, Stelle ${klartext(erste.stelle)}`, '')
for (const b of g.mitglieder) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}, Stelle ${klartext(b.stelle)}): ${klartext(b.aussage)}`, ` Beleg: ${klartext(b.beleg)}`)
if (g.nachpruefung) z.push('', `Nachprüfung: ${g.nachpruefung.urteil}, ${klartext(g.nachpruefung.fundstelle)}. ${klartext(g.nachpruefung.begruendung)}`)
z.push('')
}
z.push('## Unbelegt', '')
if (!x.unbelegt.length) z.push('keine')
for (const b of x.unbelegt) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}) \`${b.datei}\`, Stelle ${klartext(b.stelle)}: ${klartext(b.aussage)}`)
z.push('', '## Nicht prüfbar', '')
const offen = Object.keys(x.stimmen).flatMap(n => (x.stimmen[n].nicht_pruefbar || []).map(t => `- ${NAMEN[n]}: ${klartext(t)}`))
z.push(...(offen.length ? offen : ['keine']))
z.push('', '## Hinweise des Laufs', '')
const alleHinweise = [...p.hinweise, ...laufHinweise]
z.push(...(alleHinweise.length ? alleHinweise.map(h => '- ' + klartext(h)) : ['keine']), '')
return z.join('\n')
}
// Der fertige Befehl für den Koordinator, nachdem er bericht_md als bericht.md im Paketordner
// gespeichert hat. --summe ist paket_summe aus QUORUM_PAKET: ablegen setzt den Stand nur aus dem
// paket.json ein, dessen SHA-256 der Paketbau gemeldet hat, und endet sonst mit BEFUND.
function ablegenBefehl(p) {
return `${PY} ${q(WERKZEUG)} ablegen --paket ${q(p.ordner)} --bericht ${q(p.ordner + '/bericht.md')} --ziel ${q(p.bericht_pfad)} --summe ${q(p.paket_summe)}`
}
function ergebnis(status, urteil, grund, md, stimmen, gruppen, unbelegt, zaehlung, naechster) {
return { status, urteil, grund, bericht_pfad: md ? paket.bericht_pfad : null, bericht_md: md, paket: paket.ordner, ablegen_befehl: md ? ablegenBefehl(paket) : null, stimmen, gruppen, unbelegt, zaehlung, verbrauch: ablesungen, hinweise: [...paket.hinweise, ...laufHinweise], naechster_schritt: naechster }
}
pruefeArgs(args)
phase('Paket')
const gebaut = lesePaket(await werkzeugAgent('werkzeug:paket', 'Paket', paketBefehl(args), 'Prüfpaket für das Quorum bauen', args.suite ? LANGE : ''))
if (gebaut.fehler) throw new Error('quorum: ' + gebaut.fehler)
const paket = gebaut.paket
paket.hinweise.forEach(h => log('Paket: ' + h))
if (paket.leer) {
const urteil = paket.vorher && paket.vorher.urteil === 'frei' ? 'frei' : 'nicht_frei'
const grund = `Unterschied leer; Urteil aus Quorum ${paket.vorher ? paket.vorher.nummer : '?'} übernommen, keine Stimme gestartet`
log(grund)
return ergebnis('leer', urteil, grund, berichtLeer(args, paket, urteil, grund), {}, [], [], null, 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren und committen; das übernommene Urteil gilt.')
}
phase('Stimmen')
const vorStimmen = await ablesen('vor Stimmen', 1)
if (vorStimmen.gebremst) {
const grund = 'gebremst vor den Stimmen: ' + vorStimmen.grund
log('Quorum ' + grund)
return ergebnis('gebremst', 'kein_urteil', grund, null, {}, [], [], null, 'Nach der Zurücksetzung des Fensters neu starten, nicht mit resumeFromRunId fortsetzen; die Basis bleibt die alte.')
}
const claude = modell => agent(stimmenAuftrag(args, paket), { label: 'stimme:' + modell, phase: 'Stimmen', schema: STIMME, agentType: 'quorum-stimme', model: modell })
.catch(e => {
log(`stimme:${modell} abgebrochen: ${e && e.message}`)
return null
})
const [rohOpus, rohSonnet, codex] = await Promise.all([
claude('opus'),
claude('sonnet'),
codexStimme(args, paket).catch(e => ({ abgegeben: false, grund: 'Absturz: ' + (e && e.message), job: null, dauer_s: -1, befunde: [], nicht_pruefbar: [] })),
])
const stimmen = { opus: claudeStimme(rohOpus, paket), sonnet: claudeStimme(rohSonnet, paket), codex }
if (!codex.abgegeben) {
hinweis('Codex ohne gültige Stimme (' + codex.grund + '); Haiku ist eingesprungen')
stimmen.haiku = claudeStimme(await claude('haiku'), paket)
}
for (const n of ['opus', 'sonnet', 'haiku']) {
const s = stimmen[n]
if (s && s.abgegeben && !s.lesemarke_ok) hinweis(`${NAMEN[n]}: Lesemarke fehlt oder weicht ab; ob die Stimme das Paket gelesen hat, ist unbelegt`)
}
const abgegeben = Object.keys(stimmen).filter(n => stimmen[n].abgegeben)
const alle = []
for (const n of abgegeben) {
stimmen[n].befunde.forEach((b, i) => alle.push({ id: KUERZEL[n] + (i + 1), stimme: n, datei: String(b.datei), stelle: String(b.stelle), schwere: b.schwere, aussage: String(b.aussage), beleg: String(b.beleg), belegt: String(b.beleg).trim() !== '' }))
}
const belegt = alle.filter(b => b.belegt)
const unbelegt = alle.filter(b => !b.belegt)
if (abgegeben.length < 2) {
await ablesen('Ende', 0)
const grund = `nur ${abgegeben.length} Stimme(n) abgegeben; ein Quorum braucht zwei`
log(grund)
return ergebnis('unvollstaendig', 'kein_urteil', grund, null, stimmen, [], unbelegt, null, 'Ursache der fehlenden Stimmen klären und neu starten; ohne Bericht bleibt die Basis die alte.')
}
phase('Zusammenführen')
let rohGruppen = null
if (new Set(belegt.map(b => b.stimme)).size >= 2) {
rohGruppen = await agent(zusammenfuehrenAuftrag(belegt), { label: 'zusammenfuehren', phase: 'Zusammenführen', schema: GRUPPEN, model: 'sonnet', effort: 'medium' }).catch(() => null)
if (!rohGruppen) hinweis('Zusammenführen ohne Ergebnis; jeder Befund blieb eine eigene Gruppe')
}
const gebildet = gruppenBilden(rohGruppen, belegt)
gebildet.meldungen.forEach(hinweis)
const gruppen = gebildet.gruppen.map((mitglieder, i) => {
const beteiligt = [...new Set(mitglieder.map(b => b.stimme))]
const schwere = mitglieder.map(b => b.schwere).sort((x, y) => RANG[x] - RANG[y])[0]
return { nr: i + 1, schwere, stimmen: beteiligt, mehrheit: beteiligt.length >= 2, status: '', nachpruefung: null, mitglieder }
})
phase('Nachprüfen')
const nachzupruefen = gruppen.filter(g => !g.mehrheit && g.schwere !== 'hinweis')
let rohUrteile = null
if (nachzupruefen.length) {
rohUrteile = await agent(nachpruefAuftrag(args, paket, nachzupruefen), { label: 'nachpruefung', phase: 'Nachprüfen', schema: URTEILE, agentType: 'quorum-stimme', model: 'sonnet' }).catch(() => null)
if (!rohUrteile) hinweis('Nachprüfung ohne Ergebnis; die Einzelbefunde blieben ungeprüft')
}
const jeUrteil = new Map()
for (const u of rohUrteile && Array.isArray(rohUrteile.urteile) ? rohUrteile.urteile : []) if (!jeUrteil.has(u.id)) jeUrteil.set(u.id, u)
for (const g of gruppen) {
if (g.mehrheit) g.status = 'mehrheit'
else if (g.schwere === 'hinweis') g.status = 'hinweis_ungeprueft'
else {
g.nachpruefung = jeUrteil.get(g.mitglieder[0].id) || null
g.status = g.nachpruefung ? g.nachpruefung.urteil : 'nicht_geprueft'
}
}
const gilt = g => g.status === 'mehrheit' || g.status === 'bestaetigt'
const sperrend = gruppen.filter(g => g.schwere === 'blockierend' && (gilt(g) || g.status === 'nicht_geprueft'))
const urteil = sperrend.length ? 'nicht_frei' : 'frei'
const grund = sperrend.length
? `${sperrend.filter(gilt).length} bestätigte und ${sperrend.filter(g => !gilt(g)).length} ungeprüfte blockierende Befunde`
: 'kein bestätigter und kein ungeprüfter blockierender Befund mit Beleg' + (unbelegt.some(b => b.schwere === 'blockierend') ? `; ${unbelegt.filter(b => b.schwere === 'blockierend').length} blockierende ohne Beleg stehen unter Unbelegt und sperren nicht` : '')
await ablesen('Ende', 0)
const zaehlung = {}
for (const n of Object.keys(stimmen)) {
const anteil = liste => liste.reduce((summe, g) => summe + g.mitglieder.filter(b => b.stimme === n).length, 0)
zaehlung[n] = {
abgegeben: stimmen[n].abgegeben,
gesamt: alle.filter(b => b.stimme === n).length,
belegt: belegt.filter(b => b.stimme === n).length,
bestaetigt: anteil(gruppen.filter(gilt)),
alleinfunde: anteil(gruppen.filter(g => g.status === 'bestaetigt')),
}
}
const ORDNUNG = { mehrheit: 0, bestaetigt: 0, nicht_geprueft: 1, widerlegt: 2, hinweis_ungeprueft: 3 }
gruppen.sort((x, y) => (ORDNUNG[x.status] - ORDNUNG[y.status]) || (RANG[x.schwere] - RANG[y.schwere]) || (x.nr - y.nr))
gruppen.forEach((g, i) => { g.nr = i + 1 })
const md = berichtVoll(args, paket, { urteil, grund, stimmen, zaehlung, gruppen, unbelegt })
return ergebnis('fertig', urteil, grund, md, stimmen, gruppen, unbelegt, zaehlung, urteil === 'frei'
? 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren, den Bericht im README bzw. in der Übersicht verlinken und committen; der Meilenstein ist frei.'
: 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren und committen. Die Korrekturen entscheidet der Koordinator, umgesetzt über die Standardschleife; danach bestätigt eine quorum-stimme auf Sonnet (Auftragsart Nachkontrolle) je Befund, dass er behoben ist.')
+118
View File
@@ -0,0 +1,118 @@
export const meta = {
name: 'verbrauch',
description: 'Liest den Planverbrauch (5-Stunden-Fenster, Woche) über get_usage und rechnet daraus, wie viele Opus-Agenten gleichzeitig laufen dürfen',
whenToUse: 'Vor jeder Opus-Staffel eines Workflows, als Kind aufgerufen: workflow({ scriptPath: "D:/Claude/.claude/workflows/verbrauch.js" }, { anlass, wunsch }). Args: {anlass, wunsch: ganze Zahl ab 0, probe?}. wunsch 0 heißt nur messen. Rückgabe {status: ok|nicht_gelesen|unbekannte_fenster, quelle, fuenf_stunden, woche, woche_opus, plaetze, gebremst, grund, fenster}. Ohne lesbare Ablesung gilt die alte Obergrenze zwei. Regel: AI/Regelwerk/codeworkflow.md, "Verbrauch und Opus-Plätze".',
phases: [{ title: 'Verbrauch', detail: 'ein Haiku-Agent liest get_usage ab, das Skript entscheidet' }],
}
// Geltungsbereich (AI/Regelwerk/pruefwerkzeuge.md): Das Skript liest den Planverbrauch und
// rechnet daraus, wie viele Opus-Agenten gleichzeitig laufen dürfen. Es ist eine Bremse, kein
// Nachweis. Woher die Antworten kommen:
// - Welche Fenster zählen und wie sie heißen: Quelle. get_usage in der Desktop-App lieferte am
// 2026-09-24 "5-hour limit", "Weekly · all models" und "Weekly · Fable", je mit percentUsed
// und resetsAt. Von 19 typisierten Limit-Abbrüchen im Session-Speicher (22.08. bis 20.09.)
// waren 16 five_hour und 3 seven_day, keiner seven_day_opus. Ein Fenster "Weekly · Opus" zählt
// trotzdem wie die Woche, falls es auftaucht, weil die Agenten, um die es geht, auf Opus laufen.
// - Die Stufen: Projektvorgabe, vorläufig (Pascal, 2026-09-24). Sie stehen nur hier und werden
// nach der Messreihe mit .vault-tools/workflow-pruefung/verbrauch_reihe.py nachgestellt.
// - Ohne lesbare Ablesung: Projektvorgabe, die bisherige Stammregel von zwei Opus-Agenten.
// Was das Skript nicht sieht: Agenten, die nach der Ablesung in anderen Sitzungen starten;
// laufende Agenten, die nach der Ablesung weiter verbrauchen; Sitzungen ohne get_usage
// (außerhalb der Desktop-App). Das Urteil rechnet das Skript, der Haiku-Agent liest nur ab.
const HARTE_OBERGRENZE = 4
const OHNE_ABLESUNG = 2
// Je Stufe [Schwelle in Prozent, Plätze unterhalb der Schwelle]; ab der letzten Schwelle 0.
const STUFEN_FUENF_STUNDEN = [[50, 4], [70, 2], [85, 1]]
const STUFEN_WOCHE = [[70, 4], [85, 2], [95, 1]]
const FUENF_STUNDEN = /^\s*5[- ]?hour/i
const WOCHE_ALLE = /^\s*weekly\b.*\ball models\b/i
const WOCHE_OPUS = /^\s*weekly\b.*\bopus\b/i
const ABLESUNG = {
type: 'object',
properties: {
verfuegbar: { type: 'boolean' },
status: { type: 'string' },
plan: { type: 'string' },
fenster: {
type: 'array',
items: {
type: 'object',
properties: { label: { type: 'string' }, percentUsed: { type: 'number' }, resetsAt: { type: 'string' } },
required: ['label', 'percentUsed', 'resetsAt'],
},
},
fehler: { type: 'string' },
},
required: ['verfuegbar', 'status', 'plan', 'fenster', 'fehler'],
}
const AUFTRAG = `Lade mit dem Werkzeug ToolSearch das Werkzeug mcp__ccd_session_mgmt__get_usage (Abfrage "select:mcp__ccd_session_mgmt__get_usage") und rufe es einmal ohne Parameter auf.
Übertrage aus dem Ergebnis plan.status nach status und plan.plan nach plan. Übertrage jeden Eintrag aus plan.windows mit label, percentUsed und resetsAt unverändert nach fenster, auch Einträge, die dir unwichtig erscheinen. Setze verfuegbar auf true und fehler auf "".
Ist das Werkzeug nicht verfügbar oder scheitert der Aufruf, setze verfuegbar auf false, status auf "fehler", plan auf "", fenster auf [] und schreibe die Fehlermeldung wörtlich nach fehler.
Tu sonst nichts. Übertrage das Ergebnis in die Felder des StructuredOutput-Werkzeugs.`
function pruefeArgs(a) {
if (!a || typeof a !== 'object') throw new Error('verbrauch: args fehlen')
if (typeof a.anlass !== 'string' || !a.anlass.trim()) throw new Error('verbrauch: anlass fehlt')
if (!Number.isInteger(a.wunsch) || a.wunsch < 0) throw new Error('verbrauch: wunsch muss eine ganze Zahl ab 0 sein, ist ' + JSON.stringify(a.wunsch))
if (a.probe !== undefined && (a.probe === null || typeof a.probe !== 'object' || Array.isArray(a.probe))) throw new Error('verbrauch: probe muss ein Objekt sein')
}
function stufe(prozent, stufen) {
for (const [schwelle, plaetze] of stufen) if (prozent < schwelle) return plaetze
return 0
}
// Ordnet die Fenster ein. Ein Fenster, das als 5 Stunden oder Woche erkannt wird, aber keine
// Prozentzahl ab 0 trägt, ist ein Befund und wird nicht still übergangen. Über 100 % heißt
// überschritten und bremst wie jeder Wert über der letzten Stufe.
function einordnen(fenster) {
if (!Array.isArray(fenster)) return { fehler: 'fenster ist keine Liste' }
const e = { fuenf: null, woche: null, opus: null, andere: [] }
for (const f of fenster) {
if (!f || typeof f !== 'object' || typeof f.label !== 'string') return { fehler: 'Fenster ohne label: ' + JSON.stringify(f) }
const art = FUENF_STUNDEN.test(f.label) ? 'fuenf' : WOCHE_ALLE.test(f.label) ? 'woche' : WOCHE_OPUS.test(f.label) ? 'opus' : null
if (!art) { e.andere.push(f.label); continue }
const p = f.percentUsed
if (typeof p !== 'number' || !Number.isFinite(p) || p < 0) return { fehler: `${f.label}: percentUsed ${JSON.stringify(p)} ist keine Zahl ab 0` }
const neu = { prozent: p, zuruecksetzung: typeof f.resetsAt === 'string' ? f.resetsAt : null }
if (!e[art] || e[art].prozent < p) e[art] = neu
}
const fehlt = [!e.fuenf && '5-hour limit', !e.woche && 'Weekly all models'].filter(Boolean)
if (fehlt.length) return { fehler: 'Fenster fehlt: ' + fehlt.join(', ') + (e.andere.length ? '; gelesen: ' + e.andere.join(', ') : '') }
return e
}
pruefeArgs(args)
phase('Verbrauch')
const wunsch = args.wunsch
const deckel = wunsch > HARTE_OBERGRENZE ? `; wunsch ${wunsch} auf die harte Obergrenze ${HARTE_OBERGRENZE} gesenkt` : ''
let quelle = 'probe'
let ablesung = args.probe
if (ablesung === undefined) {
quelle = 'agent'
ablesung = await agent(AUFTRAG, { label: 'verbrauch:' + args.anlass, phase: 'Verbrauch', schema: ABLESUNG, model: 'haiku' })
}
function ohneAblesung(status, grund) {
const plaetze = Math.min(wunsch, OHNE_ABLESUNG)
log(`Verbrauch ${args.anlass}: ${status} (${grund}); ${plaetze} Plätze nach der alten Obergrenze`)
return { status, quelle, fuenf_stunden: null, woche: null, woche_opus: null, plaetze, gebremst: false, grund: grund + deckel, fenster: ablesung && Array.isArray(ablesung.fenster) ? ablesung.fenster : null }
}
if (!ablesung || typeof ablesung !== 'object') return ohneAblesung('nicht_gelesen', 'kein Ergebnis vom Ableseagenten')
if (ablesung.verfuegbar !== true || ablesung.status !== 'ok') return ohneAblesung('nicht_gelesen', `get_usage: verfuegbar ${ablesung.verfuegbar}, status ${ablesung.status}${ablesung.fehler ? ', ' + ablesung.fehler : ''}`)
const e = einordnen(ablesung.fenster)
if (e.fehler) return ohneAblesung('unbekannte_fenster', e.fehler)
const wocheProzent = Math.max(e.woche.prozent, e.opus ? e.opus.prozent : 0)
const grenzeFuenf = stufe(e.fuenf.prozent, STUFEN_FUENF_STUNDEN)
const grenzeWoche = stufe(wocheProzent, STUFEN_WOCHE)
const plaetze = Math.min(wunsch, grenzeFuenf, grenzeWoche, HARTE_OBERGRENZE)
const gebremst = wunsch > 0 && plaetze === 0
const grund = `5 Stunden ${e.fuenf.prozent} % (bis ${grenzeFuenf} Plätze), Woche ${wocheProzent} % (bis ${grenzeWoche} Plätze)${e.opus ? ', Opus-Woche ' + e.opus.prozent + ' %' : ''}${deckel}`
log(`Verbrauch ${args.anlass}: ${grund}; ${plaetze} von ${wunsch} Plätzen${gebremst ? ', gebremst' : ''}`)
return { status: 'ok', quelle, fuenf_stunden: e.fuenf, woche: e.woche, woche_opus: e.opus, plaetze, gebremst, grund, fenster: ablesung.fenster }
+195
View File
@@ -0,0 +1,195 @@
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.
// 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.
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'
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) {
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: 'sonnet', 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')
belege.kopie = r
const lauf = r && r.laeufe && r.laeufe[0]
const m = lauf && /^KOPIE=(.+?)\s*$/m.exec(lauf.ausgabe)
if (m) temp = m[1].replace(/\/mat$/, '')
const angewendet = lauf && /'(neu|ersetzt|angehaengt)': [1-9]/.test(lauf.ausgabe)
if (lauf && lauf.exit === 0 && m && angewendet) kopie = m[1]
else log('Kopie nicht verwendbar (' + (!lauf ? 'Hilfsagent ohne Ergebnis' : lauf.exit !== 0 ? 'Exit ' + lauf.exit : !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) {
const vl = belege.vergleich && belege.vergleich.laeufe && belege.vergleich.laeufe[0]
const vergleichstext = vl ? kopfUndEnde(vl.ausgabe, 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 = temp ? `${temp} bleibt liegen (Projektkopie ohne die Ausschlüsse); löschen, wenn der Diff nicht mehr gebraucht wird` : null
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.',
}