Automatischer Push: 46 Datei(en) geändert
- .claude/agents/planer.md - .claude/skills/plan-executer-pruefung/SKILL.md - .claude/workflows/plankritik.js - .claude/workflows/planung.js - .claude/workflows/pruefung.js - .claude/workflows/verbrauch.js - .claude/workflows/welle-mit-pruefung.js - .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/plan_zeichen.py - .vault-tools/plan-kopie/rot_nachweis.py - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/abendkontrolle.py - … und 34 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
c5da36cb41
commit
f86a54a6e7
29 files changed
+6067
-79
No files matched your search
@@ -18,6 +18,7 @@ Du denkst, du tippst nicht. Dein Ergebnis ist ein Plan, den ein Executer ohne R
|
|||||||
- **Erst lesen, dann planen.** Bestehende Struktur, Namensgebung, Testkonventionen und Abhängigkeiten des Zielprojekts lesen. Der Plan passt sich der vorhandenen Umgebung an, nicht umgekehrt.
|
- **Erst lesen, dann planen.** Bestehende Struktur, Namensgebung, Testkonventionen und Abhängigkeiten des Zielprojekts lesen. Der Plan passt sich der vorhandenen Umgebung an, nicht umgekehrt.
|
||||||
- **Erwartete pytest-Fehlerzeilen nur mit breiter Ausgabe.** Ohne Terminal kürzt pytest (gemessen an 9.1.1) jede `FAILED`-Zeile der Kurzzusammenfassung auf 80 Zeichen, die Fehlerart ist dann abgeschnitten („Failed: D..."). Nennt der Plan eine erwartete Fehlerzeile wörtlich, steht im Befehl `COLUMNS=300` davor (Git-Bash) bzw. `$env:COLUMNS=300;` (PowerShell).
|
- **Erwartete pytest-Fehlerzeilen nur mit breiter Ausgabe.** Ohne Terminal kürzt pytest (gemessen an 9.1.1) jede `FAILED`-Zeile der Kurzzusammenfassung auf 80 Zeichen, die Fehlerart ist dann abgeschnitten („Failed: D..."). Nennt der Plan eine erwartete Fehlerzeile wörtlich, steht im Befehl `COLUMNS=300` davor (Git-Bash) bzw. `$env:COLUMNS=300;` (PowerShell).
|
||||||
- **Keine Platzhalter.** Kein „analog zu Task 2", kein „hier noch ergänzen", kein Pseudocode. Was im Plan steht, ist tippfertig.
|
- **Keine Platzhalter.** Kein „analog zu Task 2", kein „hier noch ergänzen", kein Pseudocode. Was im Plan steht, ist tippfertig.
|
||||||
|
- **Nachbesserung nach einer ausgeführten Welle schreibst du als neue Taskdateien, nicht als Neufassung.** Du hast kein Edit-Werkzeug und schreibst jede überarbeitete Datei komplett neu; am 2026-09-23 im Nexhorizons Hub waren dabei 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Deshalb: neue `task-NN-nachbesserung-<thema>.md` mit `Aus:`/`wird:`-Blöcken gegen den Baum, bestehende Taskdateien bleiben byte-gleich. Was ältere Tasks an den neuen Stand anpassen muss, schreibst du als Folgenliste `nachbesserung-<welle>-folgen.md` im selben `Aus:`/`wird:`-Format; eingesetzt wird sie vom Koordinator oder einem Executer. Für Korrekturen vor der ersten Ausführung gilt das nicht, außer der Auftrag verlangt es.
|
||||||
- **Offene Entscheidungen benennen, nicht überspielen.** Wo der Auftrag mehrdeutig ist: Annahme treffen, im Abschnitt „Getroffene Annahmen" explizit nennen. Nur abbrechen, wenn jede Annahme den Plan unbrauchbar machen würde.
|
- **Offene Entscheidungen benennen, nicht überspielen.** Wo der Auftrag mehrdeutig ist: Annahme treffen, im Abschnitt „Getroffene Annahmen" explizit nennen. Nur abbrechen, wenn jede Annahme den Plan unbrauchbar machen würde.
|
||||||
|
|
||||||
## Aufbau des Plans
|
## Aufbau des Plans
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ wird.
|
|||||||
Für die Plankritik in Schritt 2, die Welle in Schritt 3 und die Prüfung in Schritt 4 gibt es
|
Für die Plankritik in Schritt 2, die Welle in Schritt 3 und die Prüfung in Schritt 4 gibt es
|
||||||
gespeicherte Workflows mit eingebauten Rollenparametern: `plankritik`, `executer-welle` und
|
gespeicherte Workflows mit eingebauten Rollenparametern: `plankritik`, `executer-welle` und
|
||||||
`pruefung` unter `.claude/workflows/`, dazu `welle-mit-pruefung` für Schritt 3 und 4 in einem
|
`pruefung` unter `.claude/workflows/`, dazu `welle-mit-pruefung` für Schritt 3 und 4 in einem
|
||||||
Aufruf. Aufruf und Argumente stehen jeweils in `whenToUse` im
|
Aufruf und `planung` für die Erstplanung vieler Taskdateien. Aufruf und Argumente stehen jeweils
|
||||||
Kopf der Datei. Sie ersetzen deine eigene Gegenprüfung nicht.
|
in `whenToUse` im Kopf der Datei. Wie viele Opus-Agenten sie gleichzeitig starten, regelt
|
||||||
|
`codeworkflow.md`, „Verbrauch und Opus-Plätze". Sie ersetzen deine eigene Gegenprüfung nicht.
|
||||||
|
|
||||||
Danach je nach Aufgabe zusätzlich:
|
Danach je nach Aufgabe zusätzlich:
|
||||||
|
|
||||||
@@ -46,7 +47,8 @@ Danach je nach Aufgabe zusätzlich:
|
|||||||
1. **Die Gegenprüfung in Schritt 4.** Ein Agentenbericht ist eine Behauptung, kein Beleg. Du
|
1. **Die Gegenprüfung in Schritt 4.** Ein Agentenbericht ist eine Behauptung, kein Beleg. Du
|
||||||
wiederholst den Testlauf selbst, liest die zentralen Artefakte selbst und fährst einen
|
wiederholst den Testlauf selbst, liest die zentralen Artefakte selbst und fährst einen
|
||||||
eigenen Lauf mit echten Daten und eigenem Prüfcode. Der `pruefer`-Subagent (`opus`)
|
eigenen Lauf mit echten Daten und eigenem Prüfcode. Der `pruefer`-Subagent (`opus`)
|
||||||
verdoppelt das, er ersetzt es nicht.
|
verdoppelt das, er ersetzt es nicht. Du fährst sie, während er läuft, nur lesend
|
||||||
|
(`codeworkflow.md`, Schritt 4).
|
||||||
2. **Den Commit.** Subagenten committen nie — paralleles `git commit` kollidiert am Index-Lock.
|
2. **Den Commit.** Subagenten committen nie — paralleles `git commit` kollidiert am Index-Lock.
|
||||||
|
|
||||||
## Grenzen
|
## Grenzen
|
||||||
|
|||||||
+102
-23
@@ -1,10 +1,10 @@
|
|||||||
export const meta = {
|
export const meta = {
|
||||||
name: 'plankritik',
|
name: 'plankritik',
|
||||||
description: 'Liest einen Planordner mit getrennten pruefer-Linsen, fasst doppelte Befunde zusammen und lässt jeden wichtigen oder blockierenden Befund von einem zweiten pruefer zu widerlegen versuchen; höchstens zwei Opus gleichzeitig',
|
description: 'Liest einen Planordner mit getrennten pruefer-Linsen, gleichzeitig nach Verbrauch bis vier, fasst doppelte Befunde zusammen und lässt jeden wichtigen oder blockierenden Befund von einem zweiten pruefer zu widerlegen versuchen, höchstens zwei gleichzeitig',
|
||||||
whenToUse: 'Schritt 2 der Standardschleife vor dem ersten Executer, Pflicht ab etwa drei Tasks (AI/Regelwerk/codeworkflow.md, "Plankritik vor dem ersten Executer"). Args: {projekt, planordner, spec?, kopie?, materialisieren?: {ohne: [relpfade], tasks?: [taskdateien]}, suite?, linsen?, verifizieren?: true, verifizieren_ab?: "wichtig"|"blockierend", effort?, werkzeuge?, zusatz?}. Ohne kopie baut materialisieren sie selbst (kopie_bauen.py plus plan_materialisiere.py); ohne() muss Geheimnisdateien und große Datenordner nennen.',
|
whenToUse: 'Schritt 2 der Standardschleife vor dem ersten Executer, Pflicht ab etwa drei Tasks (AI/Regelwerk/codeworkflow.md, "Plankritik vor dem ersten Executer"). Args: {projekt, planordner, spec?, kopie?, materialisieren?: {ohne: [relpfade], tasks?: [taskdateien]}, suite?, linsen?, plaetze?: 1..4, verifizieren?: true, verifizieren_ab?: "wichtig"|"blockierend", effort?, werkzeuge?: Ordner der Planwerkzeuge (Pfad, keine Ausgabe; Vorgabe D:/Claude/.vault-tools/plan-kopie), zusatz?}. Ohne kopie baut materialisieren sie selbst (kopie_bauen.py plus plan_materialisiere.py); ohne() muss Geheimnisdateien und große Datenordner nennen. 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.',
|
||||||
phases: [
|
phases: [
|
||||||
{ title: 'Vorbereitung', detail: 'Hilfsagent: Kopie bauen, Plan anwenden, Planwerkzeuge fahren' },
|
{ title: 'Vorbereitung', detail: 'Hilfsagent: Kopie bauen, Plan anwenden, Planwerkzeuge fahren' },
|
||||||
{ title: 'Plankritik', detail: 'je Linse ein pruefer, paarweise' },
|
{ title: 'Plankritik', detail: 'je Linse ein pruefer, gleichzeitig nach Verbrauch' },
|
||||||
{ title: 'Widerlegung', detail: 'je wichtiger Befund ein zweiter pruefer, höchstens zwei gleichzeitig' },
|
{ title: 'Widerlegung', detail: 'je wichtiger Befund ein zweiter pruefer, höchstens zwei gleichzeitig' },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,15 @@ export const meta = {
|
|||||||
// Frontmatter von .claude/agents/pruefer.md; args.effort überschreibt ihn nur bewusst.
|
// 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
|
// 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").
|
// 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 EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
|
||||||
const OPUS_PARALLEL = 2
|
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 }
|
const RANG = { blockierend: 0, wichtig: 1, klein: 2 }
|
||||||
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
|
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
|
||||||
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
|
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||||
@@ -76,9 +83,11 @@ function pruefeArgs(a) {
|
|||||||
if (a.materialisieren && a.materialisieren.tasks !== undefined && !Array.isArray(a.materialisieren.tasks)) throw new Error('plankritik: materialisieren.tasks muss eine Liste von Taskdateien sein')
|
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])
|
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 (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.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.verifizieren_ab !== undefined && !['wichtig', 'blockierend'].includes(a.verifizieren_ab)) throw new Error('plankritik: verifizieren_ab muss wichtig oder blockierend 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.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')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Höchstens n Aufgaben gleichzeitig, ohne Leerlauf zwischen Paaren.
|
// Höchstens n Aufgaben gleichzeitig, ohne Leerlauf zwischen Paaren.
|
||||||
@@ -95,6 +104,26 @@ async function hoechstens(n, items, fn) {
|
|||||||
return ergebnisse
|
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) {
|
function werkzeugAgent(label, phase, befehl, zweck) {
|
||||||
return agent(`Du führst einen Werkzeugbefehl aus und gibst seine Ausgabe roh zurück. Zweck: ${zweck}
|
return agent(`Du führst einen Werkzeugbefehl aus und gibst seine Ausgabe roh zurück. Zweck: ${zweck}
|
||||||
|
|
||||||
@@ -105,8 +134,14 @@ ${befehl}
|
|||||||
{ label, phase, schema: LAUF, model: 'sonnet', effort: 'low' })
|
{ 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) {
|
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.
|
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)}
|
${LINSEN[linse](a, ctx)}
|
||||||
|
|
||||||
@@ -143,7 +178,7 @@ function widerlegungsauftrag(a, g, ctx) {
|
|||||||
${texte}
|
${texte}
|
||||||
</befund>
|
</befund>
|
||||||
|
|
||||||
Versuche ihn zu widerlegen. Lies die Stelle selbst und reproduziere den Befund mit einem eigenen Befehl${ctx.kopie ? `; der Plan ist auf die Kopie ${ctx.kopie} angewendet, dort darfst du Tests fahren` : ''}. Du änderst keine Datei im Projekt und keine im Plan.
|
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.
|
- "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.
|
- "widerlegt" nur mit einem Gegenbeleg: ein Befehl mit Ausgabe oder ein Zitat, das die Aussage falsch macht.
|
||||||
- Sonst "offen".
|
- Sonst "offen".
|
||||||
@@ -171,6 +206,10 @@ function kopieAusLauf(r, wozu) {
|
|||||||
return m[1]
|
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')
|
phase('Vorbereitung')
|
||||||
if (ctx.kopie && args.materialisieren) log('kopie ist gesetzt, materialisieren wird übergangen')
|
if (ctx.kopie && args.materialisieren) log('kopie ist gesetzt, materialisieren wird übergangen')
|
||||||
if (!ctx.kopie && args.materialisieren) {
|
if (!ctx.kopie && args.materialisieren) {
|
||||||
@@ -186,14 +225,38 @@ if (!ctx.kopie && args.materialisieren) {
|
|||||||
{
|
{
|
||||||
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 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')
|
const r = await werkzeugAgent('werkzeug:planwerkzeuge', 'Vorbereitung', befehl, 'Codeblöcke auf unsichtbare Zeichen und Aus-Blöcke gegen den Baum prüfen')
|
||||||
if (r && r.laeufe && r.laeufe[0]) ctx.werkzeuge = r.laeufe[0].ausgabe
|
// 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')
|
else log('Planwerkzeuge: Hilfsagent ohne Ergebnis; Linse bezeichner fährt sie selbst')
|
||||||
vorbereitung.push(r)
|
vorbereitung.push(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
phase('Plankritik')
|
phase('Plankritik')
|
||||||
const gewaehlt = args.linsen && args.linsen.length ? args.linsen : Object.keys(LINSEN)
|
const gewaehlt = args.linsen && args.linsen.length ? args.linsen : Object.keys(LINSEN)
|
||||||
const kritiken = await hoechstens(OPUS_PARALLEL, gewaehlt, linse => {
|
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(),
|
||||||
|
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' }
|
const opts = { label: 'plankritik:' + linse, phase: 'Plankritik', schema: BEFUNDE, agentType: 'pruefer', model: 'opus' }
|
||||||
if (args.effort) opts.effort = args.effort
|
if (args.effort) opts.effort = args.effort
|
||||||
return agent(auftrag(args, linse, ctx), opts).then(r => ({ linse, ergebnis: r }))
|
return agent(auftrag(args, linse, ctx), opts).then(r => ({ linse, ergebnis: r }))
|
||||||
@@ -206,34 +269,50 @@ const roh = ergebnisse.flatMap(e => e.ergebnis.befunde.map(b => ({ linse: e.lins
|
|||||||
const gruppen = zusammenfassen(roh)
|
const gruppen = zusammenfassen(roh)
|
||||||
const ab = RANG[args.verifizieren_ab || 'wichtig']
|
const ab = RANG[args.verifizieren_ab || 'wichtig']
|
||||||
const zuPruefen = args.verifizieren === false ? [] : gruppen.filter(g => RANG[g.schweregrad] <= ab)
|
const zuPruefen = args.verifizieren === false ? [] : gruppen.filter(g => RANG[g.schweregrad] <= ab)
|
||||||
const nichtGeprueft = gruppen.length - zuPruefen.length
|
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')})`)
|
||||||
log(`${roh.length} Befunde, zusammengefasst ${gruppen.length}; ${zuPruefen.length} gehen in die Widerlegung, ${nichtGeprueft} nicht (${args.verifizieren === false ? 'abgeschaltet' : 'unter der Schwelle ' + (args.verifizieren_ab || 'wichtig')})`)
|
|
||||||
|
|
||||||
phase('Widerlegung')
|
phase('Widerlegung')
|
||||||
const urteile = await hoechstens(OPUS_PARALLEL, zuPruefen, (g, i) => {
|
let gebremstVor = null
|
||||||
const opts = { label: `widerlegung:${i + 1}:${dateiname(g.datei)}:${g.zeile}`, phase: 'Widerlegung', schema: URTEIL, agentType: 'pruefer', model: 'opus' }
|
let plaetzeWiderlegung = 0
|
||||||
if (args.effort) opts.effort = args.effort
|
if (zuPruefen.length) {
|
||||||
return agent(widerlegungsauftrag(args, g, ctx), opts)
|
const vorWiderlegung = await ablesen('vor Widerlegung', Math.min(WIDERLEGUNG_HOECHSTENS, zuPruefen.length))
|
||||||
})
|
if (vorWiderlegung.gebremst) {
|
||||||
zuPruefen.forEach((g, i) => {
|
gebremstVor = 'Widerlegung'
|
||||||
if (!urteile[i]) log(`Widerlegung ${i + 1} (${g.datei}:${g.zeile}) ohne Ergebnis; Befund bleibt offen`)
|
log('Widerlegung gebremst, die Befunde bleiben ungeprüft: ' + vorWiderlegung.grund)
|
||||||
g.widerlegung = urteile[i] || { urteil: 'offen', schweregrad: g.schweregrad, bedingung: '', begruendung: 'Widerlegungsagent ohne Ergebnis', beleg: '' }
|
} 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 URTEILRANG = { bestaetigt: 0, offen: 1, undefined: 2, widerlegt: 3 }
|
||||||
const befunde = gruppen
|
const befunde = gruppen
|
||||||
.map(g => ({ urteil: g.widerlegung ? g.widerlegung.urteil : 'nicht geprüft', ...g }))
|
.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]))
|
.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: nichtGeprueft }
|
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]++ })
|
befunde.forEach(b => { if (b.widerlegung) zaehlung[b.widerlegung.urteil]++ })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
status: gebremstVor ? 'gebremst' : 'fertig',
|
||||||
|
gebremst_vor: gebremstVor,
|
||||||
zaehlung,
|
zaehlung,
|
||||||
kopie: ctx.kopie,
|
kopie: ctx.kopie,
|
||||||
temp_ordner: ctx.temp ? `${ctx.temp} bleibt liegen (Projektkopie ohne die Ausschlüsse); löschen, wenn nicht mehr gebraucht` : null,
|
temp_ordner: tempText(),
|
||||||
befunde,
|
befunde,
|
||||||
selbst_ausgefuehrt: ergebnisse.map(e => ({ linse: e.linse, befehle: e.ergebnis.selbst_ausgefuehrt })),
|
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)),
|
nicht_pruefbar: ergebnisse.flatMap(e => e.ergebnis.nicht_pruefbar.map(x => e.linse + ': ' + x)),
|
||||||
vorbereitung,
|
vorbereitung,
|
||||||
ohne_ergebnis: ohneErgebnis,
|
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.',
|
||||||
}
|
}
|
||||||
@@ -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.',
|
||||||
|
}
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
export const meta = {
|
export const meta = {
|
||||||
name: 'pruefung',
|
name: 'pruefung',
|
||||||
description: 'Lässt eine fertige Welle vom pruefer mit dem vollen Pflichtprogramm gegen Plan und Zielumgebung prüfen; kritische Änderungen mit xhigh',
|
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?}. kritisch für sicherheits- oder datenrelevante Änderungen; abschluss fragt, ob eine weitere Runde noch Substanz hat.',
|
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' }],
|
phases: [{ title: 'Prüfung', detail: 'ein pruefer' }],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rollenparameter: model wird immer gesetzt (Stamm). Der Effort kommt aus dem Frontmatter
|
// 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
|
// 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").
|
// 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 EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
|
||||||
|
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
|
||||||
|
|
||||||
const BERICHT = {
|
const BERICHT = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -42,6 +46,7 @@ function pruefeArgs(a) {
|
|||||||
if (fehlt.length) throw new Error('pruefung: args fehlen: ' + fehlt.join(', '))
|
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.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.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) {
|
function auftrag(a) {
|
||||||
@@ -52,15 +57,32 @@ ${a.version ? `Aussagen über fremde Software belegst du am tatsächlich eingese
|
|||||||
${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.`
|
${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)
|
pruefeArgs(args)
|
||||||
phase('Prüfung')
|
phase('Prüfung')
|
||||||
const opts = { label: 'pruefung' + (args.kritisch ? ':kritisch' : ''), phase: 'Prüfung', schema: BERICHT, agentType: 'pruefer', model: 'opus' }
|
const opts = { label: 'pruefung' + (args.kritisch ? ':kritisch' : ''), phase: 'Prüfung', schema: BERICHT, agentType: 'pruefer', model: 'opus' }
|
||||||
if (args.effort) opts.effort = args.effort
|
if (args.effort) opts.effort = args.effort
|
||||||
else if (args.kritisch) opts.effort = 'xhigh'
|
else if (args.kritisch) opts.effort = 'xhigh'
|
||||||
|
await ablesen('vor Prüfung')
|
||||||
const bericht = await agent(auftrag(args), opts)
|
const bericht = await agent(auftrag(args), opts)
|
||||||
|
await ablesen('nach Prüfung')
|
||||||
if (!bericht) throw new Error('pruefung: der pruefer hat keinen Bericht geliefert')
|
if (!bericht) throw new Error('pruefung: der pruefer hat keinen Bericht geliefert')
|
||||||
|
|
||||||
const RANG = { blockierend: 0, wichtig: 1, klein: 2 }
|
const RANG = { blockierend: 0, wichtig: 1, klein: 2 }
|
||||||
const zaehlung = { blockierend: 0, wichtig: 0, klein: 0 }
|
const zaehlung = { blockierend: 0, wichtig: 0, klein: 0 }
|
||||||
bericht.befunde.forEach(b => { zaehlung[b.schweregrad]++ })
|
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]) }
|
return { effort: opts.effort || '(Frontmatter)', zaehlung, ...bericht, befunde: [...bericht.befunde].sort((x, y) => RANG[x.schweregrad] - RANG[y.schweregrad]), verbrauch: ablesungen }
|
||||||
@@ -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 }
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export const meta = {
|
export const meta = {
|
||||||
name: 'welle-mit-pruefung',
|
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',
|
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?}. Ohne kopie wird sie vor der Welle gebaut; ohne nennt dann .git und Geheimnisdateien. Ersetzt die eigene Gegenprüfung des Koordinators nicht.',
|
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: [
|
phases: [
|
||||||
{ title: 'Kopie', detail: 'Hilfsagent: Stand vor der Welle kopieren und den Plan der Welle anwenden' },
|
{ title: 'Kopie', detail: 'Hilfsagent: Stand vor der Welle kopieren und den Plan der Welle anwenden' },
|
||||||
{ title: 'Welle', detail: 'executer-welle' },
|
{ title: 'Welle', detail: 'executer-welle' },
|
||||||
@@ -14,8 +14,12 @@ export const meta = {
|
|||||||
// Aufträge und Rollenparameter an genau einer Stelle stehen. Der Hilfsagent hat keinen
|
// 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,
|
// 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.
|
// 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 MODELLE = ['haiku', 'sonnet', 'opus']
|
||||||
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
|
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.
|
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
|
||||||
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
|
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||||
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
|
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
|
||||||
@@ -42,6 +46,7 @@ function pruefeArgs(a) {
|
|||||||
})
|
})
|
||||||
if (a.effort !== undefined && !EFFORTS.includes(a.effort)) throw new Error('welle-mit-pruefung: effort "' + a.effort + '" ungültig')
|
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.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) {
|
if (!a.kopie) {
|
||||||
const ok = Array.isArray(a.ohne) && a.ohne.length > 0 && a.ohne.every(x => typeof x === 'string' && x)
|
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')
|
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')
|
||||||
@@ -64,6 +69,20 @@ function kopfUndEnde(text, kopf, ende) {
|
|||||||
return text.slice(0, kopf) + '\n[... gekürzt ...]\n' + text.slice(-ende)
|
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)
|
pruefeArgs(args)
|
||||||
const W = args.werkzeuge || 'D:/Claude/.vault-tools/plan-kopie'
|
const W = args.werkzeuge || 'D:/Claude/.vault-tools/plan-kopie'
|
||||||
const taskdateien = args.tasks.map(t => t.datei)
|
const taskdateien = args.tasks.map(t => t.datei)
|
||||||
@@ -113,8 +132,8 @@ let pruefung = null
|
|||||||
let pruefungGrund = null
|
let pruefungGrund = null
|
||||||
if (!welle) pruefungGrund = 'keine Welle'
|
if (!welle) pruefungGrund = 'keine Welle'
|
||||||
else if (welle.ohne_bericht && welle.ohne_bericht.length) pruefungGrund = 'Welle unvollständig, ohne Bericht: ' + welle.ohne_bericht.join(', ')
|
else if (welle.ohne_bericht && welle.ohne_bericht.length) pruefungGrund = 'Welle unvollständig, ohne Bericht: ' + welle.ohne_bericht.join(', ')
|
||||||
if (pruefungGrund) log('Prüfung übersprungen: ' + pruefungGrund)
|
let pruefungArgs = null
|
||||||
else {
|
if (!pruefungGrund) {
|
||||||
const vl = belege.vergleich && belege.vergleich.laeufe && belege.vergleich.laeufe[0]
|
const vl = belege.vergleich && belege.vergleich.laeufe && belege.vergleich.laeufe[0]
|
||||||
const vergleichstext = vl ? kopfUndEnde(vl.ausgabe, 1500, 2500) : ''
|
const vergleichstext = vl ? kopfUndEnde(vl.ausgabe, 1500, 2500) : ''
|
||||||
const schwerpunkt = [
|
const schwerpunkt = [
|
||||||
@@ -122,25 +141,55 @@ else {
|
|||||||
welle.mit_abweichung.length ? `Diese Tasks melden Abweichungen vom Plan: ${welle.mit_abweichung.join(', ')}.` : '',
|
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}` : '',
|
vergleichstext ? `Rohausgabe von Byte-Vergleich und Suite nach der Welle (vom Hilfsagenten, nicht von dir gefahren):\n${vergleichstext}` : '',
|
||||||
].filter(Boolean).join('\n')
|
].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 {
|
try {
|
||||||
pruefung = await workflow('pruefung', {
|
pruefung = await workflow('pruefung', { ...pruefungArgs, verbrauch: false })
|
||||||
projekt: args.projekt, plan: args.planordner, tasks: taskdateien, suite: args.suite,
|
|
||||||
kritisch: args.kritisch, abschluss: args.abschluss, version: args.version, effort: args.effort, schwerpunkt,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
fehler.push('pruefung: ' + (e && e.message))
|
fehler.push('pruefung: ' + (e && e.message))
|
||||||
pruefungGrund = 'pruefung gescheitert: ' + (e && e.message)
|
pruefungGrund = 'pruefung gescheitert: ' + (e && e.message)
|
||||||
log(pruefungGrund)
|
log(pruefungGrund)
|
||||||
}
|
}
|
||||||
|
await ablesen('nach Prüfung')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
status: pruefung ? 'fertig' : 'ohne_pruefung',
|
||||||
kopie,
|
kopie,
|
||||||
temp_ordner: temp ? `${temp} bleibt liegen (Projektkopie ohne die Ausschlüsse); löschen, wenn der Diff nicht mehr gebraucht wird` : null,
|
temp_ordner: tempText,
|
||||||
welle,
|
welle,
|
||||||
vergleich: belege.vergleich || null,
|
vergleich: belege.vergleich || null,
|
||||||
kopie_lauf: belege.kopie || null,
|
kopie_lauf: belege.kopie || null,
|
||||||
pruefung: pruefung || { uebersprungen: pruefungGrund },
|
pruefung: pruefung || { uebersprungen: pruefungGrund },
|
||||||
fehler,
|
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.',
|
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.',
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergl
|
|||||||
summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)."
|
summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)."
|
||||||
tags: [vault-tools, codeworkflow]
|
tags: [vault-tools, codeworkflow]
|
||||||
created: 2026-09-17
|
created: 2026-09-17
|
||||||
updated: 2026-09-20
|
updated: 2026-09-24
|
||||||
---
|
---
|
||||||
|
|
||||||
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
||||||
@@ -21,12 +21,14 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
|
|||||||
5. **Nach jeder Welle:** `welle_vergleich.py --projekt <projekt> --mat <kopie> [--prosa <rel> …] -- <taskdateien der Welle>`. Prüft, ob genau die Zieldateien geändert sind und ob jede byte-gleich mit der Kopie ist. Es zeigt je Datei nur die erste Abweichung — danach `diff` auf die ganze Datei. Eigene Plankorrekturen im Baum meldet es zu Recht als „außerhalb der Zieldateien".
|
5. **Nach jeder Welle:** `welle_vergleich.py --projekt <projekt> --mat <kopie> [--prosa <rel> …] -- <taskdateien der Welle>`. Prüft, ob genau die Zieldateien geändert sind und ob jede byte-gleich mit der Kopie ist. Es zeigt je Datei nur die erste Abweichung — danach `diff` auf die ganze Datei. Eigene Plankorrekturen im Baum meldet es zu Recht als „außerhalb der Zieldateien".
|
||||||
6. **Escape-Folgen setzen:** `plan_escape.py <datei> <zeile> <codepunkt-hex> …` ersetzt echte Zeichen in einer Zeile durch ihre Escape-Folge, ohne dass eine Schreibschicht mitredet.
|
6. **Escape-Folgen setzen:** `plan_escape.py <datei> <zeile> <codepunkt-hex> …` ersetzt echte Zeichen in einer Zeile durch ihre Escape-Folge, ohne dass eine Schreibschicht mitredet.
|
||||||
|
|
||||||
7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test]`. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
|
7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]`. **Für ein anderes Projekt als die Trading Plattform braucht es die drei letzten Schalter (seit 2026-09-24):** Ihre Vorgaben sind die der Trading Plattform (`~/venvs/trading`, `dashboard`, `trading-plattform`). Jedes andere Projekt nennt seine Werte, für den Nexhorizons Hub `--venv '~/venvs/hub' --frontend frontend --projektname 'active - Nexhorizons Hub'`, das Venv in einfachen Anführungszeichen, sonst macht die Git-Bash aus `~` einen Windows-Pfad. Das Skript druckt die benutzten Werte in der ersten Zeile. Die Vorgabe `trading-plattform` passt nur auf Pläne mit Arbeitsbaum-Pfaden; ältere Trading-Pläne mit absoluten Pfaden unter `active - Trading Plattform` brauchen `--projektname 'active - Trading Plattform'`, sonst endet der Lauf mit einem `ValueError`. Weiter fest verdrahtet bleiben die Distribution `Debian`, die Ordner `tests/` und `tests/fixtures/`, die Endungen `.test.ts`/`.test.tsx` und `npx.cmd vitest`. **Vorbedingung:** Das Projekt ist im Venv nicht editierbar installiert (`pip install -e`); sonst löst der Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung läuft grün. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
|
||||||
|
|
||||||
## Was belegt ist
|
## Was belegt ist
|
||||||
|
|
||||||
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
|
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
|
||||||
|
|
||||||
|
Seit 2026-09-24 hat `rot_nachweis.py` die Schalter `--venv`, `--frontend` und `--projektname`; vorher waren `~/venvs/trading`, `dashboard` und `trading-plattform/` fest verdrahtet. Ein Hub-Lauf im Trading-Venv wäre für die Tasks mit `argon2-cffi` und `pyotp` rot aus einem fehlenden Paket gewesen, nicht aus der fehlenden Umsetzung. Fehlt das Venv in WSL, steht beim Task jetzt `VORAUSSETZUNG FEHLT` statt `ROT`, und die Schlusszeile zählt diese Läufe getrennt. Belegt am selben Tag: Der Vorgabe-Befehl ist byte-gleich mit der alten Fassung, falsche Schalterwerte enden mit Exit 3, und der erste echte Lauf über 38 Tasks des Hub meldete keinen grün ohne Umsetzung. Im selben Lauf zeigte sich eine zweite Lücke: Beim Zurücksetzen blieben Ordner stehen, die erst der Task angelegt hatte. Ein leeres `hub/auth/` liest Python als Namespace-Paket, und die Fehlerzeile nannte `hub.auth.passwort` statt `hub.auth`. Jetzt gehen solche Ordner mit, sobald sie leer sind; derselbe Lauf für die Tasks 7, 9 und 11 zeigt danach genau die Zeile aus dem Plan. Die unabhängige Prüfung desselben Tages fand eine dritte Lücke: Ein vergessenes `--venv` fiel still auf das Trading-Venv zurück, und Task 7 stand als `ROT … No module named 'argon2'` da. Seitdem wird eine erste Fehlerzeile, die ein Modul außerhalb des Projekts und außerhalb der Zieldateien vermisst, als `VORAUSSETZUNG FEHLT` gemeldet. Das trifft auch den Ende-zu-Ende-Task des Hub, dessen Tests `playwright` brauchen. Mit den Vorgaben lieferte das Skript für die sieben Tasks des Trading-Pakets K vor und nach allen Änderungen dieselben Zeilen. Zwei weitere Lücken desselben Tages, beide ein ROT, das nichts misst: **Ein Arbeitsordner mit Windows-Kurznamen** (`C:/Users/PASCAL~1/…`) ließ vitest dort starten, die Dateien aber über den langen Namen auflösen, und jede Oberflächen-Testdatei endete als `Cannot find module '/src/…'`. Das Skript löst den Arbeitsordner jetzt auf den langen Namen auf; danach zeigte Trading-Task 395 wieder `9 failed | 52 passed` statt `no tests`, und die Hub-Tasks 24, 25, 29 und 37 die Zeile aus dem Plan (`Error: Failed to resolve import "./kontrast" …`). Die erste `Error:`-Zeile steht seitdem mit in der Ergebniszeile, weil die `FAIL`-Zeile allein den Grund nicht zeigt. **Ein Test, der über Vite eine Datei aus `node_modules` lädt** (`?raw`), scheitert hinter der Junction mit `Denied ID`, weil Vite nichts außerhalb seines Wurzelordners ausliefert; das Skript meldet das als `VORAUSSETZUNG FEHLT`. Für solche Tasks läuft der Nachweis in einer Kopie mit echtem `node_modules` (242 MB, gut 20 Sekunden).
|
||||||
|
|
||||||
Seit 2026-09-22 behandelt `rot_nachweis.py` `tests/conftest.py` als Produktivseite. Vorher zählte alles unter `tests/` als Testdatei — lag die Korrektur eines Tasks selbst in `conftest.py` (Trading Plattform, Task 369: die Ersatzfunktionen der Netzsperre trugen eine andere Vorgabe als die Originale), wendete der Lauf „ohne Umsetzung" sie mit an und meldete `GRUEN pytest: 147 passed` — der eine Zustand, in dem der Nachweis nichts misst, sah aus wie ein sauberer Befund. Gemessen an derselben Basis-Kopie und demselben Task, altes gegen neues Skript: `GRUEN 147 passed` → `ROT 3 failed, 144 passed` mit der gemeinten Fehlerzeile (`assert ('', '') == ('probe-thema', '')`). Die Gegenprobe für den anderen Fall — eine neue Fixture des Tasks liegt in `conftest.py` (Task 368) — mit `--conftest-als-test` `ROT 1 failed, 11 passed` wie vor der Änderung, ohne Schalter `ROT 6 failed, 6 passed` mit `AttributeError: module 'conftest' has no attribute '_gesperrte_pfade'` als erster Fehlerzeile: rot aus dem falschen Grund, aber sichtbar, und das Skript sagt je Task, wie es `conftest.py` behandelt hat. Was es weiter nicht sieht: ob ein Task beides in `conftest.py` legt, Fixture und Korrektur — dann ist keine der beiden Einstellungen richtig, und der Nachweis läuft von Hand.
|
Seit 2026-09-22 behandelt `rot_nachweis.py` `tests/conftest.py` als Produktivseite. Vorher zählte alles unter `tests/` als Testdatei — lag die Korrektur eines Tasks selbst in `conftest.py` (Trading Plattform, Task 369: die Ersatzfunktionen der Netzsperre trugen eine andere Vorgabe als die Originale), wendete der Lauf „ohne Umsetzung" sie mit an und meldete `GRUEN pytest: 147 passed` — der eine Zustand, in dem der Nachweis nichts misst, sah aus wie ein sauberer Befund. Gemessen an derselben Basis-Kopie und demselben Task, altes gegen neues Skript: `GRUEN 147 passed` → `ROT 3 failed, 144 passed` mit der gemeinten Fehlerzeile (`assert ('', '') == ('probe-thema', '')`). Die Gegenprobe für den anderen Fall — eine neue Fixture des Tasks liegt in `conftest.py` (Task 368) — mit `--conftest-als-test` `ROT 1 failed, 11 passed` wie vor der Änderung, ohne Schalter `ROT 6 failed, 6 passed` mit `AttributeError: module 'conftest' has no attribute '_gesperrte_pfade'` als erster Fehlerzeile: rot aus dem falschen Grund, aber sichtbar, und das Skript sagt je Task, wie es `conftest.py` behandelt hat. Was es weiter nicht sieht: ob ein Task beides in `conftest.py` legt, Fixture und Korrektur — dann ist keine der beiden Einstellungen richtig, und der Nachweis läuft von Hand.
|
||||||
|
|
||||||
Seit 2026-09-20 quotet `rot_nachweis.py` den Arbeitsordner für den WSL-Aufruf (`shlex.quote`). Ein Temp-Pfad mit Leerzeichen — der Benutzername steckt im Scratchpad-Pfad — ließ vorher das `cd` scheitern: Alle sieben Tasks eines Plans der Trading Plattform standen als „ROT pytest: keine Summenzeile" da, ohne dass ein einziger Test lief. Nach der Korrektur derselbe Lauf mit echten Summenzeilen (`3 failed, 17 passed`, `1 error`, …). Wer „keine Summenzeile" bei **jedem** Task sieht, hat kein Rot gemessen, sondern ein kaputtes Werkzeug.
|
Seit 2026-09-20 quotet `rot_nachweis.py` den Arbeitsordner für den WSL-Aufruf (`shlex.quote`). Ein Temp-Pfad mit Leerzeichen — der Benutzername steckt im Scratchpad-Pfad — ließ vorher das `cd` scheitern: Alle sieben Tasks eines Plans der Trading Plattform standen als „ROT pytest: keine Summenzeile" da, ohne dass ein einziger Test lief. Nach der Korrektur derselbe Lauf mit echten Summenzeilen (`3 failed, 17 passed`, `1 error`, …). Wer „keine Summenzeile" bei **jedem** Task sieht, hat kein Rot gemessen, sondern ein kaputtes Werkzeug.
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
# Die Windows-Konsole kodiert cp1252; ein sichtbares Zeichen wie U+2192 in der Zusammenfassung
|
||||||
|
# liess das Skript sonst mit UnicodeEncodeError abbrechen (2026-09-24, Plan AF der Trading Plattform).
|
||||||
|
sys.stdout.reconfigure(errors="backslashreplace")
|
||||||
plan = Path(sys.argv[1])
|
plan = Path(sys.argv[1])
|
||||||
dateien = sorted(plan.glob("*.md"))
|
dateien = sorted(plan.glob("*.md"))
|
||||||
if not dateien:
|
if not dateien:
|
||||||
|
|||||||
@@ -20,16 +20,29 @@ Geltungsbereich und Luecken:
|
|||||||
Sammelfehler -- das ist ROT mit "fixture ... not found" in der ersten Fehlerzeile, also sichtbar.
|
Sammelfehler -- das ist ROT mit "fixture ... not found" in der ersten Fehlerzeile, also sichtbar.
|
||||||
Wer diesen Fall hat, ruft mit --conftest-als-test auf; dann gilt conftest.py wie frueher als Testdatei.
|
Wer diesen Fall hat, ruft mit --conftest-als-test auf; dann gilt conftest.py wie frueher als Testdatei.
|
||||||
Das Skript sagt je Task, wie es conftest.py behandelt hat.
|
Das Skript sagt je Task, wie es conftest.py behandelt hat.
|
||||||
- Python-Tests laufen in WSL (Debian, ~/venvs/trading), Oberflaechentests unter Windows mit npx vitest;
|
- Python-Tests laufen in WSL (Debian, Venv aus --venv), Oberflaechentests unter Windows mit npx vitest im
|
||||||
dafuer setzt das Skript eine Junction auf das node_modules des Projekts und loest sie vor dem Loeschen.
|
Ordner aus --frontend; dafuer setzt das Skript eine Junction auf das node_modules des Projekts und loest
|
||||||
|
sie vor dem Loeschen. Die Vorgaben der drei Schalter sind die der Trading Plattform (~/venvs/trading,
|
||||||
|
dashboard, trading-plattform/); jedes andere Projekt nennt seine Werte (seit 2026-09-24, Nexhorizons Hub).
|
||||||
|
Fehlt das Venv in WSL oder der Frontend-Ordner in der Kopie, steht beim Task VORAUSSETZUNG FEHLT statt
|
||||||
|
ROT -- sonst saehe ein Lauf im falschen Venv aus wie ein sauberer Rot-Nachweis. Dasselbe gilt, wenn die
|
||||||
|
erste pytest-Fehlerzeile ein Modul vermisst, das weder im Projekt liegt noch vom Task angelegt wird
|
||||||
|
(vergessenes --venv, eine Abhaengigkeit, die nur der Stack hat). Das greift nur an der ersten
|
||||||
|
Fehlerzeile; ein fremdes Modul, das erst hinter einem Projektmodul fehlt, sieht das Skript nicht.
|
||||||
|
- Vorbedingung: Das Projekt ist im Venv NICHT editierbar installiert (pip install -e). Sonst loest der
|
||||||
|
Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung
|
||||||
|
liefe gruen (Pruefung 2026-09-24, setuptools editable_wheel.py).
|
||||||
- Erzeugte Fixtures (der Plan laesst sie mit altem Code erzeugen) holt --fixturen aus einem Ordner nach,
|
- Erzeugte Fixtures (der Plan laesst sie mit altem Code erzeugen) holt --fixturen aus einem Ordner nach,
|
||||||
wenn sie nach dem Anwenden fehlen.
|
wenn sie nach dem Anwenden fehlen.
|
||||||
- Es sieht nicht, WELCHE Tests rot sein muessten; es zaehlt und zeigt die erste Fehlerzeile.
|
- Es sieht nicht, WELCHE Tests rot sein muessten; es zaehlt und zeigt die erste Fehlerzeile.
|
||||||
- Die Basis ist eine Kopie, die der Aufrufer VOR der ersten Welle gezogen hat; das Skript fasst das
|
- Die Basis ist eine Kopie, die der Aufrufer VOR der ersten Welle gezogen hat; das Skript fasst das
|
||||||
Projekt selbst nie an.
|
Projekt selbst nie an.
|
||||||
|
|
||||||
Aufruf: python rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test]
|
Aufruf: python rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]
|
||||||
--node-modules nennt das node_modules des Projekts, wenn die Basis-Kopie keines traegt.
|
--node-modules nennt das node_modules des Projekts, wenn die Basis-Kopie keines traegt.
|
||||||
|
--venv nennt das Venv in WSL (Vorgabe ~/venvs/trading), --frontend den Oberflaechenordner relativ zum
|
||||||
|
Projekt (Vorgabe dashboard), --projektname den Ordnernamen, hinter dem absolute Zielpfade abgeschnitten
|
||||||
|
werden (Vorgabe trading-plattform).
|
||||||
--conftest-als-test laesst tests/conftest.py als Testdatei stehen (nur, wenn die neue Fixture des Tasks dort liegt).
|
--conftest-als-test laesst tests/conftest.py als Testdatei stehen (nur, wenn die neue Fixture des Tasks dort liegt).
|
||||||
spec.json: [{"task": "task-141-....md", "vor": ["task-...md", ...]}, ...]
|
spec.json: [{"task": "task-141-....md", "vor": ["task-...md", ...]}, ...]
|
||||||
"""
|
"""
|
||||||
@@ -46,10 +59,16 @@ from pathlib import Path
|
|||||||
HIER = Path(__file__).resolve().parent
|
HIER = Path(__file__).resolve().parent
|
||||||
ZIEL = re.compile(r"^- `(.+?)`")
|
ZIEL = re.compile(r"^- `(.+?)`")
|
||||||
PROJEKTNAME = "trading-plattform/"
|
PROJEKTNAME = "trading-plattform/"
|
||||||
|
VENV = "~/venvs/trading"
|
||||||
|
FRONTEND = "dashboard"
|
||||||
CONFTEST = "tests/conftest.py"
|
CONFTEST = "tests/conftest.py"
|
||||||
|
# Das Venv steht ungequotet im bash-Aufruf, damit ~ expandiert; deshalb nur diese Zeichen.
|
||||||
|
VENV_MUSTER = re.compile(r"^~?[A-Za-z0-9._/-]+\Z")
|
||||||
|
FRONTEND_MUSTER = re.compile(r"^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*\Z")
|
||||||
|
FEHLENDES_MODUL = re.compile(r"No module named '([^']+)'")
|
||||||
|
|
||||||
|
|
||||||
def zieldateien(taskdatei: Path) -> list[str]:
|
def zieldateien(taskdatei: Path, projektname: str = PROJEKTNAME) -> list[str]:
|
||||||
"""Zieldateien relativ zum Projekt. Absolute Pfade werden hinter dem Projektordner abgeschnitten."""
|
"""Zieldateien relativ zum Projekt. Absolute Pfade werden hinter dem Projektordner abgeschnitten."""
|
||||||
drin = False
|
drin = False
|
||||||
ziele: list[str] = []
|
ziele: list[str] = []
|
||||||
@@ -61,10 +80,10 @@ def zieldateien(taskdatei: Path) -> list[str]:
|
|||||||
if treffer:
|
if treffer:
|
||||||
pfad = treffer.group(1).replace("\\", "/").removeprefix("./")
|
pfad = treffer.group(1).replace("\\", "/").removeprefix("./")
|
||||||
if re.match(r"^[A-Za-z]:/", pfad):
|
if re.match(r"^[A-Za-z]:/", pfad):
|
||||||
stelle = pfad.lower().find(PROJEKTNAME)
|
stelle = pfad.lower().find(projektname)
|
||||||
if stelle < 0:
|
if stelle < 0:
|
||||||
raise ValueError(f"{taskdatei.name}: absoluter Pfad ausserhalb des Projekts: {pfad}")
|
raise ValueError(f"{taskdatei.name}: absoluter Pfad ausserhalb des Projekts: {pfad}")
|
||||||
pfad = pfad[stelle + len(PROJEKTNAME) :]
|
pfad = pfad[stelle + len(projektname) :]
|
||||||
ziele.append(pfad)
|
ziele.append(pfad)
|
||||||
return ziele
|
return ziele
|
||||||
|
|
||||||
@@ -92,13 +111,57 @@ def wsl_pfad(pfad: Path) -> str:
|
|||||||
return f"/mnt/{text[0].lower()}{text[2:]}"
|
return f"/mnt/{text[0].lower()}{text[2:]}"
|
||||||
|
|
||||||
|
|
||||||
def loesche(ordner: Path) -> None:
|
def loesche(ordner: Path, frontend: str = FRONTEND) -> None:
|
||||||
junction = ordner / "dashboard" / "node_modules"
|
junction = ordner / frontend / "node_modules"
|
||||||
if junction.exists():
|
if junction.exists():
|
||||||
os.rmdir(junction) # loest nur die Verknuepfung, nie das Ziel
|
os.rmdir(junction) # loest nur die Verknuepfung, nie das Ziel
|
||||||
shutil.rmtree(ordner, ignore_errors=True)
|
shutil.rmtree(ordner, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_befehl(rot: Path, python_tests: list[str], venv: str = VENV) -> str:
|
||||||
|
# shlex.quote: Ein Arbeitsordner mit Leerzeichen (Benutzername im Temp-Pfad) liess das cd
|
||||||
|
# scheitern; jeder Task stand dann als "ROT pytest: keine Summenzeile" da, ohne dass ein Test
|
||||||
|
# lief (gefunden 2026-09-20).
|
||||||
|
return f"cd {shlex.quote(wsl_pfad(rot))} && {venv}/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60"
|
||||||
|
|
||||||
|
|
||||||
|
def venv_da(venv: str) -> bool:
|
||||||
|
lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", f"test -x {venv}/bin/python && echo VENV_DA"],
|
||||||
|
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||||
|
return "VENV_DA" in lauf.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def fremdes_modul(fehlerzeile: str, ziele: list[str], kopie: Path) -> str | None:
|
||||||
|
"""Das fehlende Modul, wenn es weder im Projekt liegt noch vom Task angelegt wird.
|
||||||
|
|
||||||
|
Ein solcher Fehler misst das Venv, nicht die fehlende Umsetzung: ein vergessenes --venv
|
||||||
|
(No module named 'argon2' im Trading-Venv) oder eine Abhaengigkeit, die nur der Stack hat
|
||||||
|
(playwright). Gefunden 2026-09-24 in der Pruefung des Werkzeugs.
|
||||||
|
"""
|
||||||
|
treffer = FEHLENDES_MODUL.search(fehlerzeile)
|
||||||
|
if not treffer:
|
||||||
|
return None
|
||||||
|
oben = treffer.group(1).split(".")[0]
|
||||||
|
eigene = set()
|
||||||
|
for ziel in ziele:
|
||||||
|
eigene.update(Path(ziel).parts[:-1])
|
||||||
|
eigene.add(Path(ziel).stem)
|
||||||
|
if oben in eigene or any(kopie.rglob(f"{oben}.py")) or any(p.is_dir() for p in kopie.rglob(oben)):
|
||||||
|
return None
|
||||||
|
return treffer.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def nimm_wert(argumente: list[str], schalter: str) -> str | None:
|
||||||
|
if schalter not in argumente:
|
||||||
|
return None
|
||||||
|
stelle = argumente.index(schalter)
|
||||||
|
if stelle + 1 >= len(argumente) or argumente[stelle + 1].startswith("--"):
|
||||||
|
raise ValueError(f"{schalter} ohne Wert")
|
||||||
|
wert = argumente[stelle + 1]
|
||||||
|
del argumente[stelle : stelle + 2]
|
||||||
|
return wert
|
||||||
|
|
||||||
|
|
||||||
def erste_fehlerzeile(ausgabe: str) -> str:
|
def erste_fehlerzeile(ausgabe: str) -> str:
|
||||||
for zeile in ausgabe.splitlines():
|
for zeile in ausgabe.splitlines():
|
||||||
kern = zeile.strip()
|
kern = zeile.strip()
|
||||||
@@ -109,16 +172,23 @@ def erste_fehlerzeile(ausgabe: str) -> str:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
argumente = sys.argv[1:]
|
argumente = sys.argv[1:]
|
||||||
fixturen = None
|
try:
|
||||||
if "--fixturen" in argumente:
|
fixturen_arg = nimm_wert(argumente, "--fixturen")
|
||||||
stelle = argumente.index("--fixturen")
|
node_modules_wert = nimm_wert(argumente, "--node-modules")
|
||||||
fixturen = Path(argumente[stelle + 1])
|
venv = nimm_wert(argumente, "--venv") or VENV
|
||||||
del argumente[stelle : stelle + 2]
|
frontend = (nimm_wert(argumente, "--frontend") or FRONTEND).replace("\\", "/").strip("/")
|
||||||
node_modules_arg = None
|
projektname = (nimm_wert(argumente, "--projektname") or PROJEKTNAME).replace("\\", "/").strip("/").lower() + "/"
|
||||||
if "--node-modules" in argumente:
|
except ValueError as fehler:
|
||||||
stelle = argumente.index("--node-modules")
|
print(f"AUFRUF FALSCH: {fehler}")
|
||||||
node_modules_arg = Path(argumente[stelle + 1])
|
return 3
|
||||||
del argumente[stelle : stelle + 2]
|
fixturen = Path(fixturen_arg) if fixturen_arg else None
|
||||||
|
node_modules_arg = Path(node_modules_wert) if node_modules_wert else None
|
||||||
|
if not VENV_MUSTER.match(venv):
|
||||||
|
print(f"AUFRUF FALSCH: --venv {venv!r} -- erlaubt sind nur ~, Buchstaben, Ziffern und ._/-")
|
||||||
|
return 3
|
||||||
|
if not FRONTEND_MUSTER.match(frontend) or ".." in frontend.split("/"):
|
||||||
|
print(f"AUFRUF FALSCH: --frontend {frontend!r} -- ein Ordner relativ zum Projekt, ohne ..")
|
||||||
|
return 3
|
||||||
conftest_als_test = False
|
conftest_als_test = False
|
||||||
if "--conftest-als-test" in argumente:
|
if "--conftest-als-test" in argumente:
|
||||||
conftest_als_test = True
|
conftest_als_test = True
|
||||||
@@ -126,23 +196,30 @@ def main() -> int:
|
|||||||
if len(argumente) != 4:
|
if len(argumente) != 4:
|
||||||
print(__doc__)
|
print(__doc__)
|
||||||
return 3
|
return 3
|
||||||
|
print(f"Einstellungen: venv={venv} frontend={frontend} projektname={projektname}")
|
||||||
basis, planordner, arbeit, spec_pfad = (Path(a) for a in argumente)
|
basis, planordner, arbeit, spec_pfad = (Path(a) for a in argumente)
|
||||||
if not basis.is_dir() or not planordner.is_dir() or not spec_pfad.is_file():
|
if not basis.is_dir() or not planordner.is_dir() or not spec_pfad.is_file():
|
||||||
print("VORAUSSETZUNG FEHLT: Basis-Kopie, Planordner oder Spec")
|
print("VORAUSSETZUNG FEHLT: Basis-Kopie, Planordner oder Spec")
|
||||||
return 3
|
return 3
|
||||||
arbeit.mkdir(parents=True, exist_ok=True)
|
arbeit.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Den langen Namen erzwingen: Kommt der Arbeitsordner als Windows-Kurzname (PASCAL~1), startet
|
||||||
|
# vitest dort, loest die Dateien aber ueber den langen Namen auf und meldet "Cannot find module"
|
||||||
|
# fuer die eigene Testdatei -- ein ROT, das nichts misst (2026-09-24, Hub-Tasks 24, 25, 29, 37
|
||||||
|
# und Trading-Task 395).
|
||||||
|
arbeit = arbeit.resolve()
|
||||||
node_modules = None
|
node_modules = None
|
||||||
for kandidat in (node_modules_arg, basis / "dashboard" / "node_modules"):
|
for kandidat in (node_modules_arg, basis / frontend / "node_modules"):
|
||||||
if kandidat is not None and kandidat.exists():
|
if kandidat is not None and kandidat.exists():
|
||||||
node_modules = kandidat
|
node_modules = kandidat
|
||||||
break
|
break
|
||||||
|
venv_geprueft: bool | None = None # erst pruefen, wenn ein Task Python-Tests hat
|
||||||
ergebnisse = []
|
ergebnisse = []
|
||||||
for eintrag in json.loads(spec_pfad.read_text(encoding="utf-8")):
|
for eintrag in json.loads(spec_pfad.read_text(encoding="utf-8")):
|
||||||
task, voraus = eintrag["task"], eintrag.get("vor", [])
|
task, voraus = eintrag["task"], eintrag.get("vor", [])
|
||||||
name = task.split("-")[1] if "-" in task else task
|
name = task.split("-")[1] if "-" in task else task
|
||||||
vor, rot = arbeit / f"{name}_vor", arbeit / f"{name}_rot"
|
vor, rot = arbeit / f"{name}_vor", arbeit / f"{name}_rot"
|
||||||
for ordner in (vor, rot):
|
for ordner in (vor, rot):
|
||||||
loesche(ordner)
|
loesche(ordner, frontend)
|
||||||
shutil.copytree(basis, vor, ignore=shutil.ignore_patterns("node_modules", "__pycache__"))
|
shutil.copytree(basis, vor, ignore=shutil.ignore_patterns("node_modules", "__pycache__"))
|
||||||
fehler = wende_an(planordner, vor, voraus)
|
fehler = wende_an(planordner, vor, voraus)
|
||||||
shutil.copytree(vor, rot)
|
shutil.copytree(vor, rot)
|
||||||
@@ -151,7 +228,7 @@ def main() -> int:
|
|||||||
ergebnisse.append((task, "ANWENDEN GESCHEITERT", fehler.replace("\n", " | ")))
|
ergebnisse.append((task, "ANWENDEN GESCHEITERT", fehler.replace("\n", " | ")))
|
||||||
print(f"{task}: ANWENDEN GESCHEITERT: {fehler[-300:]}")
|
print(f"{task}: ANWENDEN GESCHEITERT: {fehler[-300:]}")
|
||||||
continue
|
continue
|
||||||
ziele = zieldateien(planordner / task)
|
ziele = zieldateien(planordner / task, projektname)
|
||||||
tests = [z for z in ziele if ist_test(z, conftest_als_test) and not z.startswith("tests/fixtures/")]
|
tests = [z for z in ziele if ist_test(z, conftest_als_test) and not z.startswith("tests/fixtures/")]
|
||||||
if CONFTEST in ziele:
|
if CONFTEST in ziele:
|
||||||
# Sichtbar machen, was mit conftest.py geschah -- der Koordinator liest die erste Fehlerzeile
|
# Sichtbar machen, was mit conftest.py geschah -- der Koordinator liest die erste Fehlerzeile
|
||||||
@@ -168,14 +245,22 @@ def main() -> int:
|
|||||||
shutil.copyfile(vor / ziel, rot / ziel)
|
shutil.copyfile(vor / ziel, rot / ziel)
|
||||||
elif (rot / ziel).is_file():
|
elif (rot / ziel).is_file():
|
||||||
(rot / ziel).unlink()
|
(rot / ziel).unlink()
|
||||||
|
# Ordner, die erst der Task angelegt hat, gehen mit, sobald sie leer sind: Ein leeres
|
||||||
|
# hub/auth/ liest Python als Namespace-Paket, und die Fehlerzeile nannte dann das
|
||||||
|
# Untermodul statt des fehlenden Pakets (2026-09-24, Nexhorizons Hub, Tasks 7, 9, 11).
|
||||||
|
ordner = (rot / ziel).parent
|
||||||
|
while ordner != rot and not (vor / ordner.relative_to(rot)).exists() and not any(ordner.iterdir()):
|
||||||
|
ordner.rmdir()
|
||||||
|
ordner = ordner.parent
|
||||||
python_tests = [t for t in tests if t.endswith(".py")]
|
python_tests = [t for t in tests if t.endswith(".py")]
|
||||||
ober_tests = [t for t in tests if not t.endswith(".py")]
|
ober_tests = [t for t in tests if not t.endswith(".py")]
|
||||||
zeilen = []
|
zeilen = []
|
||||||
if python_tests:
|
if python_tests and venv_geprueft is None:
|
||||||
# shlex.quote: Ein Arbeitsordner mit Leerzeichen (Benutzername im Temp-Pfad) liess das cd
|
venv_geprueft = venv_da(venv)
|
||||||
# scheitern; jeder Task stand dann als "ROT pytest: keine Summenzeile" da, ohne dass ein Test
|
if python_tests and not venv_geprueft:
|
||||||
# lief (gefunden 2026-09-20).
|
zeilen.append(f"VORAUSSETZUNG FEHLT: {venv}/bin/python ist in WSL nicht ausfuehrbar")
|
||||||
befehl = f"cd {shlex.quote(wsl_pfad(rot))} && ~/venvs/trading/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60"
|
elif python_tests:
|
||||||
|
befehl = pytest_befehl(rot, python_tests, venv)
|
||||||
lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", befehl],
|
lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", befehl],
|
||||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||||
ausgabe = lauf.stdout
|
ausgabe = lauf.stdout
|
||||||
@@ -184,15 +269,22 @@ def main() -> int:
|
|||||||
# nicht gesammelt (etwa ein ImportError in tests/conftest.py) -- das ist rot, nicht gruen.
|
# nicht gesammelt (etwa ein ImportError in tests/conftest.py) -- das ist rot, nicht gruen.
|
||||||
# Gefunden am 2026-09-18: Task 172 stand als GRUEN da, obwohl kein einziger Test lief.
|
# Gefunden am 2026-09-18: Task 172 stand als GRUEN da, obwohl kein einziger Test lief.
|
||||||
rot_ja = bool(re.search(r"\d+ (failed|error)", summe)) or summe == "keine Summenzeile"
|
rot_ja = bool(re.search(r"\d+ (failed|error)", summe)) or summe == "keine Summenzeile"
|
||||||
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" pytest: {summe} | {erste_fehlerzeile(ausgabe)}")
|
erste = erste_fehlerzeile(ausgabe)
|
||||||
|
fremd = fremdes_modul(erste, ziele, rot) if rot_ja else None
|
||||||
|
if fremd:
|
||||||
|
zeilen.append(f"VORAUSSETZUNG FEHLT: Modul {fremd!r} liegt weder im Projekt noch im Venv {venv} | pytest: {summe} | {erste}")
|
||||||
|
else:
|
||||||
|
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" pytest: {summe} | {erste}")
|
||||||
if ober_tests:
|
if ober_tests:
|
||||||
if node_modules is None:
|
if node_modules is None:
|
||||||
zeilen.append("VORAUSSETZUNG FEHLT: node_modules in der Basis-Kopie")
|
zeilen.append("VORAUSSETZUNG FEHLT: node_modules in der Basis-Kopie")
|
||||||
|
elif not (rot / frontend).is_dir():
|
||||||
|
zeilen.append(f"VORAUSSETZUNG FEHLT: Frontend-Ordner {frontend} fehlt in der Kopie")
|
||||||
else:
|
else:
|
||||||
import _winapi
|
import _winapi
|
||||||
_winapi.CreateJunction(str(node_modules.resolve()), str(rot / "dashboard" / "node_modules"))
|
_winapi.CreateJunction(str(node_modules.resolve()), str(rot / frontend / "node_modules"))
|
||||||
relativ = [t.removeprefix("dashboard/") for t in ober_tests]
|
relativ = [t.removeprefix(f"{frontend}/") for t in ober_tests]
|
||||||
lauf = subprocess.run(["npx.cmd", "vitest", "run", *relativ], cwd=rot / "dashboard",
|
lauf = subprocess.run(["npx.cmd", "vitest", "run", *relativ], cwd=rot / frontend,
|
||||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||||
ausgabe = re.sub(r"\x1b\[[0-9;]*m", "", lauf.stdout + lauf.stderr)
|
ausgabe = re.sub(r"\x1b\[[0-9;]*m", "", lauf.stdout + lauf.stderr)
|
||||||
summe = next((z.strip() for z in ausgabe.splitlines() if z.strip().startswith("Tests ")), "")
|
summe = next((z.strip() for z in ausgabe.splitlines() if z.strip().startswith("Tests ")), "")
|
||||||
@@ -201,16 +293,30 @@ def main() -> int:
|
|||||||
# das Ende der Ausgabe zeigen, statt "rot" zu behaupten, ohne zu wissen woran.
|
# das Ende der Ausgabe zeigen, statt "rot" zu behaupten, ohne zu wissen woran.
|
||||||
summe = "keine Summenzeile; Ende der Ausgabe: " + " / ".join(z.strip() for z in ausgabe.splitlines()[-6:] if z.strip())[:300]
|
summe = "keine Summenzeile; Ende der Ausgabe: " + " / ".join(z.strip() for z in ausgabe.splitlines()[-6:] if z.strip())[:300]
|
||||||
rot_ja = "failed" in summe or lauf.returncode != 0
|
rot_ja = "failed" in summe or lauf.returncode != 0
|
||||||
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" vitest: {summe} | {erste_fehlerzeile(ausgabe)}")
|
if "Denied ID" in ausgabe:
|
||||||
|
# Vite liefert keine Datei ausserhalb seines Wurzelordners, und durch die Junction
|
||||||
|
# liegt node_modules ausserhalb der Kopie: Ein Test mit "?raw" aus node_modules
|
||||||
|
# scheitert dann als ganze Datei (2026-09-24, Nexhorizons Hub, Task 42).
|
||||||
|
zeilen.append(f"VORAUSSETZUNG FEHLT: Vite verweigert eine Datei hinter der Junction auf node_modules; mit echter Kopie von node_modules fahren | vitest: {summe}")
|
||||||
|
else:
|
||||||
|
# Bei einer Datei, die schon am Import scheitert, steht vor der Fehlermeldung nur
|
||||||
|
# die FAIL-Zeile; die Meldung selbst zeigt erst, ob der Grund der gemeinte ist.
|
||||||
|
erste = erste_fehlerzeile(ausgabe)
|
||||||
|
meldung = next((z.strip()[:220] for z in ausgabe.splitlines()
|
||||||
|
if z.strip().startswith(("Error:", "AssertionError", "TypeError", "ReferenceError", "SyntaxError"))), "")
|
||||||
|
zusatz = f" | {meldung}" if meldung and meldung not in erste else ""
|
||||||
|
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" vitest: {summe} | {erste}{zusatz}")
|
||||||
if not tests:
|
if not tests:
|
||||||
zeilen.append("ohne Testdatei unter den Zieldateien")
|
zeilen.append("ohne Testdatei unter den Zieldateien")
|
||||||
for zeile in zeilen:
|
for zeile in zeilen:
|
||||||
print(f"{task}: {zeile}")
|
print(f"{task}: {zeile}")
|
||||||
ergebnisse.append((task, zeile[:5].strip(), zeile))
|
ergebnisse.append((task, zeile[:5].strip(), zeile))
|
||||||
for ordner in (vor, rot):
|
for ordner in (vor, rot):
|
||||||
loesche(ordner)
|
loesche(ordner, frontend)
|
||||||
gruen = [e for e in ergebnisse if e[1] == "GRUEN"]
|
gruen = [e for e in ergebnisse if e[1] == "GRUEN"]
|
||||||
print(f"\n{len(ergebnisse)} Laeufe, davon {len(gruen)} gruen ohne Umsetzung.")
|
ohne_lauf = [e for e in ergebnisse if e[1] in ("VORAU", "ANWENDEN GESCHEITERT")]
|
||||||
|
print(f"\n{len(ergebnisse)} Laeufe, davon {len(gruen)} gruen ohne Umsetzung und {len(ohne_lauf)} ohne Lauf "
|
||||||
|
"(Voraussetzung fehlt oder Anwenden gescheitert).")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
type: resource
|
||||||
|
status: active
|
||||||
|
title: "Workflow-Prüfung: Attrappe, Fälle und Messreihe zum Verbrauch"
|
||||||
|
summary: "Node-Attrappe für die gespeicherten Workflows des Vaults mit Fällen je Workflow, dazu verbrauch_reihe.py, das aus den Workflow-Protokollen rechnet, wie viel vom 5-Stunden-Fenster und von der Woche zwischen zwei Verbrauchsablesungen verbraucht wurde. Entstanden 2026-09-24."
|
||||||
|
tags: [vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
---
|
||||||
|
|
||||||
|
# Workflow-Prüfung
|
||||||
|
|
||||||
|
Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]], Abschnitt „Verbrauch und Opus-Plätze". Hier steht nur, wie man die Werkzeuge benutzt und was belegt ist. Der Plan, mit dem sie entstanden sind: `docs/plans/2026-09-24-parallelitaet/00-uebersicht.md` in diesem Ordner.
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
1. **Nach jeder Änderung an einem Workflow unter `.claude/workflows/`:** `node .vault-tools/workflow-pruefung/faelle_workflows.mjs` im Vault-Wurzelordner. Mit einem Ordner als Argument prüft das Skript die Workflows dort, etwa eine Kopie mit angewendetem Plan. Jeder Workflow hat zuerst eine Grundlinie; scheitert sie, messen seine übrigen Fälle nichts.
|
||||||
|
2. **Ein neuer Fall braucht einen Rotlauf.** Die Fälle gegen den Stand vor der Änderung fahren, oder den Mutanten nachstellen, den der Fall fangen soll. Grün allein belegt nichts.
|
||||||
|
3. **Mutanten:** `python .vault-tools/workflow-pruefung/mutanten_workflows.py` (einige Minuten) und `python .vault-tools/workflow-pruefung/mutanten_reihe.py`. Ein veraltetes Muster bricht mit Exit 2 ab und muss an den neuen Code angepasst werden; ein wirkungsloser Mutant steht mit Grund in `WIRKUNGSLOS`. Wer einen Workflow ändert, ergänzt hier den Mutanten, den sein neuer Fall fangen soll.
|
||||||
|
4. **Messreihe:** `python .vault-tools/workflow-pruefung/verbrauch_reihe.py [--seit JJJJ-MM-TT]`. Die Ausgabe ist eine Tabelle je Paar aufeinanderfolgender Ablesungen eines Laufs: Minuten, Agentenminuten je Modellfamilie, Differenz im 5-Stunden-Fenster und in der Woche. Darunter steht eine Näherung in Prozent des 5-Stunden-Fensters je Opus-Agentenstunde. Ein `Reset` zwischen zwei Ablesungen zählt nicht mit.
|
||||||
|
|
||||||
|
## Was die Attrappe nachbildet
|
||||||
|
|
||||||
|
`attrappe.mjs` führt ein Workflow-Skript mit nachgebauten Globals aus, genauso streng wie das Laufzeitsystem: `workflow()` im Kind wirft, im Kind ist `phase()` wirkungslos und jeder Agent trägt die Phase `▸ <name>` (ab dem zweiten Aufruf desselben Kindes `▸ <name> #<n>`), ein unbekannter Workflow wirft, `Date.now()`, `new Date()` ohne Argument und `Math.random()` werfen. Ein Schema ohne `properties` wirft, ein Ergebnis ohne die `required`-Felder wird zu `null`, und ein `agent()` ohne `model` ist ein Verstoß. Aufrufe über `D:/Claude/.claude/workflows/...` leitet sie in den Ordner unter Test um. Sie zählt, wie viele Agenten je Phase und Modell gleichzeitig laufen.
|
||||||
|
|
||||||
|
**Was sie nicht sieht:** die Obergrenze gleichzeitiger Agenten je Lauf, Budget, Resume und das Verhalten echter Modelle. Wie lange echte Linsen bei drei gleichzeitigen Opus-Agenten laufen und wie viel Verbrauch sie erzeugen, zeigt nur ein echter Lauf.
|
||||||
|
|
||||||
|
## Was belegt ist
|
||||||
|
|
||||||
|
Stand 2026-09-24 nach Plankritik, Prüfung und Nachbesserung. Die Zahlen gelten für diesen Stand; wer Fälle oder Mutanten ergänzt, fährt die Läufe neu, statt die Zahlen hier nachzuziehen.
|
||||||
|
|
||||||
|
- **Rotlauf:** Gegen die vier alten Workflows scheiterten 122 von 134 Fällen (Stand vor der letzten Nachbesserung). Grün blieben die elf Selbsttests der Attrappe und „kritisch hebt auf xhigh“, ein Verhalten, das sich nicht geändert hat.
|
||||||
|
- **Mutanten an den Workflows:** 52 Mutanten, alle gefangen. Ausgenommen ist „harte Obergrenze fehlt“ in `verbrauch.js`: Er ist wirkungslos, solange keine Stufe mehr als vier Plätze gibt. Wer die Stufen anhebt, braucht dafür einen Fall.
|
||||||
|
- **Selbsttests der Attrappe:** Ein Mutant, bei dem `Date.now()` nicht mehr wirft, fällt auf.
|
||||||
|
- **`verbrauch_reihe.py`:** 27 Fälle, 18 Mutanten, alle gefangen. Die Plankritik fand den wichtigsten Fehler, den kein Fall gesehen hatte: `resetsAt` schwankt bei jeder Ablesung um Millisekunden und springt über Sekunden- und Datumsgrenzen. Das Skript hielt deshalb jedes echte Paar für einen Reset, weil die Testdaten denselben festen Zeitstempel annahmen wie der Code. Seitdem gilt eine Toleranz von fünf Minuten, und die Grundlinie trägt echte Zitterwerte.
|
||||||
|
- **`verbrauch.js` im echten Laufzeitsystem** (wf_8746f9c7-f1f): Die Ablesung ergab 43 % und 12 %, genau den Wert, den der Koordinator zur selben Zeit selbst über `get_usage` las, und damit 3 Plätze. Mit `probe` bei 95 % kamen 0 Plätze und `gebremst` heraus; ein Lauf mit `probe` startet keinen Agenten und steht deshalb in keinem Journal.
|
||||||
|
- **Neue `plankritik.js` im echten Laufzeitsystem** (wf_32edc4e9-501): Nach der Ablesung von 44 % starteten die drei Linsen innerhalb einer Sekunde; die Widerlegung lief zu zweit. Die Ablesungen stehen im Journal unter den Phasen `▸ verbrauch`, `▸ verbrauch #2` und `▸ verbrauch #3`. `verbrauch_reihe.py` wertet sie aus: erstes Paar `Reset` (das Fenster wurde um 18:30 UTC zurückgesetzt), zweites Paar 3 Punkte bei 19 Opus-Minuten.
|
||||||
|
- **Neue `pruefung.js` im echten Laufzeitsystem** (wf_e0294c82-76c): Ablesungen 13 %/16 % vor und 18 %/18 % nach einem `pruefer` von 24 Minuten.
|
||||||
|
- **Neue `planung.js` im echten Laufzeitsystem** (wf_5df09faa-a4d, Probe mit drei Tasks im Scratchpad): Task 1 lief allein, Task 2 und 3 starteten erst nach seinem Ende und liefen gleichzeitig; beide übernahmen die Zieldatei-Zeile aus Task 1, haben seine Taskdatei also gelesen.
|
||||||
|
- **Erster Eichwert** am Abend des 2026-09-24, Plan laut App „Pro“: `verbrauch_reihe.py --seit 2026-09-24` ergab 8,1 % des 5-Stunden-Fensters je Opus-Agentenstunde (5 Paare, 1,7 Opus-Stunden, darunter ein Plankritik-Lauf der Trading-Sitzung). Die Woche stieg in denselben Paaren um 5 Punkte. Der Verbrauch anderer Sitzungen und der Hauptsitzung steckt mit drin; die Zahl ist also eher zu hoch. Für das Nachstellen der Stufen reicht ein Abend nicht.
|
||||||
|
|
||||||
|
**Offen:** Die Trading-Sitzung hat `plankritik.js` am 2026-09-24 nach einem echten Lauf nachgebessert (Ausgabe der Planwerkzeuge nur bei `EXIT 0`). Für diese Stelle gibt es noch keinen Fall.
|
||||||
|
|
||||||
|
## Was die Skripte nicht sehen
|
||||||
|
|
||||||
|
Den Verbrauch anderer Sitzungen und der Hauptsitzung: Er steckt in jeder Differenz mit drin. Ablesungen über `probe` starten keinen Agenten und fehlen in der Messreihe. `get_usage` gibt es nur in der Desktop-App; anderswo gilt über den Rückfall die Zwei, und die Messreihe bleibt leer. Die Stufen in `verbrauch.js` sind vorläufig. Sie sind ein Fangnetz und kein Nachweis dafür, dass ein Lauf das Fenster nicht reißt.
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
// Attrappe für die Workflow-Skripte unter .claude/workflows/: führt ein Skript mit nachgebauten
|
||||||
|
// Globals aus und protokolliert jeden agent()-Aufruf mit Modell, Phase, Start und Ende.
|
||||||
|
//
|
||||||
|
// Grenzen wie im Ziel (Workflow-Referenz der Claude-Code-Sitzung vom 2026-09-24, dazu die
|
||||||
|
// Plankritik vom selben Tag am Laufzeitsystem 2.1.280):
|
||||||
|
// - workflow() in einem Kind wirft; Verschachtelung geht nur eine Ebene tief. Das Ziel meldet
|
||||||
|
// "workflow() cannot be called from within a child workflow", dann einen Gedankenstrich und
|
||||||
|
// "nesting is limited to one level"; die Attrappe setzt einen Doppelpunkt statt des Strichs.
|
||||||
|
// - Im Kind ist phase() wirkungslos, und jeder agent() bekommt die Phase "▸ <meta.name>", ab dem
|
||||||
|
// zweiten Aufruf desselben Kindes "▸ <meta.name> #<n>" (Bundle 2.1.280 und meta.json eines
|
||||||
|
// echten Laufs mit drei Ablesungen: "▸ verbrauch", "▸ verbrauch #2", "▸ verbrauch #3").
|
||||||
|
// - workflow() mit unbekanntem Namen oder unlesbarem scriptPath wirft.
|
||||||
|
// - Date.now(), new Date() ohne Argument und Math.random() werfen.
|
||||||
|
// - agent() mit schema braucht type object und properties, required muss in properties stehen.
|
||||||
|
// Ein Ergebnis ohne die required-Felder wird wie ein gescheiterter Agent zu null.
|
||||||
|
// - parallel() ist eine Barriere; ein werfender Thunk wird zu null.
|
||||||
|
// Zusätzlich als Verstoß protokolliert (Stamm, "Agenten & Modelle"): agent() ohne model.
|
||||||
|
// Was die Attrappe nicht nachbildet: die Obergrenze gleichzeitiger Agenten je Lauf, Budget,
|
||||||
|
// Resume und das Verhalten echter Modelle. Sie ist ein Fangnetz für die Steuerlogik der
|
||||||
|
// Skripte, kein Nachweis für einen echten Lauf.
|
||||||
|
import { readFileSync, existsSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||||
|
// Aufrufe über den echten Pfad werden in den Ordner unter Test umgeleitet.
|
||||||
|
const ECHTER_ORDNER = 'D:/Claude/.claude/workflows/'
|
||||||
|
|
||||||
|
class DatumAttrappe extends Date {
|
||||||
|
constructor(...a) {
|
||||||
|
if (a.length === 0) throw new Error('new Date() ohne Argument ist im Workflow nicht erlaubt')
|
||||||
|
super(...a)
|
||||||
|
}
|
||||||
|
static now() { throw new Error('Date.now() ist im Workflow nicht erlaubt') }
|
||||||
|
}
|
||||||
|
const MatheAttrappe = Object.create(Math, {
|
||||||
|
random: { value: () => { throw new Error('Math.random() ist im Workflow nicht erlaubt') } },
|
||||||
|
})
|
||||||
|
|
||||||
|
function lade(datei) {
|
||||||
|
if (!existsSync(datei)) throw new Error('Workflow-Datei nicht lesbar: ' + datei)
|
||||||
|
const text = readFileSync(datei, 'utf8')
|
||||||
|
if (!/^export const meta = /m.test(text)) throw new Error(datei + ': export const meta fehlt')
|
||||||
|
const name = /^export const meta = \{[\s\S]*?\bname:\s*'([^']+)'/m.exec(text)
|
||||||
|
const quelle = text.replace(/^export const meta = /m, 'const meta = ')
|
||||||
|
return { name: name ? name[1] : path.basename(datei, '.js'), fn: new AsyncFunction('agent', 'parallel', 'pipeline', 'phase', 'log', 'workflow', 'args', 'budget', 'Date', 'Math', quelle) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const warte = ms => new Promise(r => setTimeout(r, ms))
|
||||||
|
|
||||||
|
// antworte(aufruf) liefert { wert, dauer } oder undefined (Agent ohne Ergebnis) oder wirft.
|
||||||
|
// aufruf trägt label, phase, modell, effort, agentType, prompt, tiefe und schema.
|
||||||
|
export async function fuehreAus(datei, args, antworte, optionen = {}) {
|
||||||
|
const ordner = optionen.ordner || path.dirname(datei)
|
||||||
|
const protokoll = { agents: [], logs: [], phasen: [], verstoesse: [], spitze: {}, spitzeJePhase: {}, workflows: [] }
|
||||||
|
const laufend = {}
|
||||||
|
const laufendJePhase = {}
|
||||||
|
const kindAufrufe = {}
|
||||||
|
let uhr = 0
|
||||||
|
|
||||||
|
function aufloesen(ref) {
|
||||||
|
if (typeof ref === 'string') return path.join(ordner, ref + '.js')
|
||||||
|
if (ref && typeof ref.scriptPath === 'string') {
|
||||||
|
const s = ref.scriptPath.replace(/\\/g, '/')
|
||||||
|
return s.startsWith(ECHTER_ORDNER) ? path.join(ordner, s.slice(ECHTER_ORDNER.length)) : s
|
||||||
|
}
|
||||||
|
throw new Error('workflow(): Name oder { scriptPath } erwartet')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lauf(datei, a, tiefe) {
|
||||||
|
const { name, fn } = lade(datei)
|
||||||
|
let kindPhase = null
|
||||||
|
if (tiefe >= 1) {
|
||||||
|
kindAufrufe[name] = (kindAufrufe[name] || 0) + 1
|
||||||
|
kindPhase = '▸ ' + name + (kindAufrufe[name] > 1 ? ' #' + kindAufrufe[name] : '')
|
||||||
|
}
|
||||||
|
let aktuellePhase = null
|
||||||
|
|
||||||
|
async function agent(prompt, opts = {}) {
|
||||||
|
if (typeof prompt !== 'string' || !prompt.trim()) protokoll.verstoesse.push('agent() ohne Auftragstext: ' + (opts.label || '?'))
|
||||||
|
if (!opts.model) protokoll.verstoesse.push('agent() ohne model: ' + (opts.label || '?'))
|
||||||
|
if (opts.schema !== undefined) {
|
||||||
|
if (!opts.schema || opts.schema.type !== 'object' || !opts.schema.properties) throw new Error('Schema braucht type object und properties: ' + opts.label)
|
||||||
|
if ((opts.schema.required || []).some(k => !(k in opts.schema.properties))) throw new Error('required nicht in properties: ' + opts.label)
|
||||||
|
}
|
||||||
|
const modell = opts.model || '(erbt)'
|
||||||
|
const phase = kindPhase || opts.phase || aktuellePhase || '(ohne)'
|
||||||
|
const eintrag = { label: opts.label || null, phase, modell, effort: opts.effort, agentType: opts.agentType, prompt, tiefe, start: ++uhr, ende: null, ergebnis: undefined }
|
||||||
|
protokoll.agents.push(eintrag)
|
||||||
|
const k = phase + '|' + modell
|
||||||
|
laufend[modell] = (laufend[modell] || 0) + 1
|
||||||
|
laufendJePhase[k] = (laufendJePhase[k] || 0) + 1
|
||||||
|
protokoll.spitze[modell] = Math.max(protokoll.spitze[modell] || 0, laufend[modell])
|
||||||
|
protokoll.spitzeJePhase[k] = Math.max(protokoll.spitzeJePhase[k] || 0, laufendJePhase[k])
|
||||||
|
let wert = null
|
||||||
|
try {
|
||||||
|
const antwort = await antworte({ ...eintrag, schema: opts.schema })
|
||||||
|
await warte(antwort && typeof antwort.dauer === 'number' ? antwort.dauer : 5)
|
||||||
|
wert = antwort && typeof antwort === 'object' && 'wert' in antwort ? antwort.wert : null
|
||||||
|
if (wert !== null && opts.schema) {
|
||||||
|
const fehlt = (opts.schema.required || []).filter(f => !wert || typeof wert !== 'object' || !(f in wert))
|
||||||
|
if (fehlt.length) {
|
||||||
|
protokoll.verstoesse.push(`${opts.label}: Ergebnis ohne ${fehlt.join(', ')}`)
|
||||||
|
wert = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
protokoll.logs.push(`agent ${opts.label} geworfen: ${e && e.message}`)
|
||||||
|
wert = null
|
||||||
|
} finally {
|
||||||
|
laufend[modell]--
|
||||||
|
laufendJePhase[k]--
|
||||||
|
eintrag.ende = ++uhr
|
||||||
|
}
|
||||||
|
eintrag.ergebnis = wert
|
||||||
|
return wert
|
||||||
|
}
|
||||||
|
|
||||||
|
const parallel = thunks => Promise.all(thunks.map(t => Promise.resolve().then(t).catch(e => {
|
||||||
|
protokoll.logs.push('parallel: ' + (e && e.message))
|
||||||
|
return null
|
||||||
|
})))
|
||||||
|
const pipeline = (items, ...stufen) => Promise.all(items.map(async (item, i) => {
|
||||||
|
try {
|
||||||
|
let r = item
|
||||||
|
for (const stufe of stufen) r = await stufe(r, item, i)
|
||||||
|
return r
|
||||||
|
} catch (e) {
|
||||||
|
protokoll.logs.push('pipeline: ' + (e && e.message))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
const phase = titel => { if (!kindPhase) { aktuellePhase = titel; protokoll.phasen.push(titel) } }
|
||||||
|
const log = text => { protokoll.logs.push(String(text)) }
|
||||||
|
const workflow = async (ref, kindArgs) => {
|
||||||
|
if (tiefe >= 1) throw new Error('workflow() cannot be called from within a child workflow: nesting is limited to one level')
|
||||||
|
const ziel = aufloesen(ref)
|
||||||
|
protokoll.workflows.push({ ref: typeof ref === 'string' ? ref : ref.scriptPath, args: kindArgs, tiefe: tiefe + 1 })
|
||||||
|
return lauf(ziel, kindArgs, tiefe + 1)
|
||||||
|
}
|
||||||
|
const budget = { total: null, spent: () => 0, remaining: () => Infinity }
|
||||||
|
return fn(agent, parallel, pipeline, phase, log, workflow, a, budget, DatumAttrappe, MatheAttrappe)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ergebnis = await lauf(datei, args, optionen.tiefe || 0)
|
||||||
|
return { ergebnis, fehler: null, protokoll }
|
||||||
|
} catch (e) {
|
||||||
|
return { ergebnis: null, fehler: (e && e.message) || String(e), protokoll }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
type: plan
|
||||||
|
title: "Parallelität im Codeworkflow: Verbrauchsbremse, Linsen gleichzeitig, Planer nach Voraussetzungen"
|
||||||
|
status: active
|
||||||
|
summary: "Nach dem Plan liest ein neuer Workflow verbrauch den Planverbrauch ab und rechnet die erlaubten Opus-Plätze, die Plankritik fährt ihre Linsen danach gleichzeitig, ein neuer Workflow planung startet Planer nach ihren Voraussetzungen, pruefung und welle-mit-pruefung schreiben den Verbrauch mit, und Attrappe, Fälle, Mutantenläufe und ein Auswerteskript für die Messreihe liegen unter .vault-tools/workflow-pruefung/."
|
||||||
|
tags: [plan, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
---
|
||||||
|
|
||||||
|
# Parallelität im Codeworkflow
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Pascal hat am 2026-09-24 im Chat alle drei Stufen der Vorschläge freigegeben („Setze alle Stufen so um.“); die Vorschläge stehen in [[AI/secondbrain/regelwerk-memory/2026-09-24-parallelitaet-codeworkflow]]. „So“ heißt wie vorgeschlagen: Stufe 3, drei bis vier Opus-Plätze für Reviews und Erstplanung, kommt erst nach der Messreihe und der Eichung der Stufen. Nach diesem Plan gilt:
|
||||||
|
|
||||||
|
- **`verbrauch.js`** liest über `get_usage` das 5-Stunden-Fenster und die Woche ab und rechnet, wie viele Opus-Agenten gleichzeitig laufen dürfen. Die Mechanik reicht bis vier, bei steigendem Verbrauch sinkt die Zahl, null heißt gebremst. Ohne lesbare Ablesung gilt die alte Zwei.
|
||||||
|
- **`plankritik.js`** fährt ihre Linsen mit so vielen Plätzen, wie `verbrauch.js` erlaubt; bei drei Linsen sind das bis zu drei (Stufe 2). Die Widerlegung bleibt bei höchstens zwei. Die Aufträge sagen, wo Tests laufen dürfen.
|
||||||
|
- **`planung.js`** (neu) startet je Task einen `planer`, sobald die Taskdateien seiner Voraussetzungen geschrieben sind, statt paarweise nach Wellen. Vorgegeben sind zwei Plätze (Stufe 1); mehr erst nach der Eichung.
|
||||||
|
- **`pruefung.js`** und **`welle-mit-pruefung.js`** schreiben den Verbrauch um die Prüfung mit. `welle-mit-pruefung` kann mit `bis: "vergleich"` vor der Prüfung enden, damit der Koordinator seine Gegenprüfung parallel zum `pruefer` fährt.
|
||||||
|
- **`.vault-tools/workflow-pruefung/`** enthält die Node-Attrappe mit Fällen für alle geänderten Workflows, die beiden Mutantenläufe und `verbrauch_reihe.py` für die Messreihe.
|
||||||
|
|
||||||
|
## Nicht im Umfang
|
||||||
|
|
||||||
|
- Die Regeltexte: `CLAUDE.md` („Agenten & Modelle"), `AI/Regelwerk/codeworkflow.md`, `.claude/agents/planer.md` (Delta-Nachbesserung) und der Skill `plan-executer-pruefung`. Sie kommen vom Koordinator.
|
||||||
|
- `.claude/workflows/executer-welle.js` bleibt unverändert: Opus läuft dort nur bei der Hochstufung nach zwei Fehlschlägen, und dort bleibt die Zwei.
|
||||||
|
- Die Prüfstände für die offenen Hebel (vierte Linse „Nebenläufigkeit", Widerlegung auf Sonnet, eigene Kopie je Agent, Rot-Nachweis in der Vorbereitung). Sie waren in den Vorschlägen ein späterer Schritt, keine der drei Stufen.
|
||||||
|
- `LIESMICH.md` des Werkzeugordners schreibt der Koordinator mit den Belegen dieses Laufs.
|
||||||
|
- Kein Commit. Im Vault-Repo committet nur Pascal oder die Aufgabe um 04:00.
|
||||||
|
|
||||||
|
## Globale Vorgaben
|
||||||
|
|
||||||
|
- Arbeitsverzeichnis ist `D:/Claude`, alle Pfade in den Taskdateien gelten von dort.
|
||||||
|
- Code, der im Plan steht, wird byte-genau übernommen, auch Kommentare, Umlaute und die Zeichen `·` und `▸`. Jede Datei endet mit genau einem Zeilenumbruch, Zeilenenden LF.
|
||||||
|
- Ein Widerspruch zwischen Plan und Baum wird gemeldet und nicht still aufgelöst.
|
||||||
|
- Jeder Executer fasst nur die Dateien unter „Zieldateien" seiner Taskdatei an.
|
||||||
|
- `.claude/workflows/` und `.vault-tools/` gehen um 04:00 ins öffentliche Repo. Die Dateien dieses Plans enthalten keine Kundennamen, Domains oder Zugangsdaten.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```text
|
||||||
|
plankritik / planung / pruefung / welle-mit-pruefung
|
||||||
|
└─ workflow({ scriptPath: "D:/Claude/.claude/workflows/verbrauch.js" }, { anlass, wunsch })
|
||||||
|
└─ agent (haiku): ToolSearch -> mcp__ccd_session_mgmt__get_usage -> fenster
|
||||||
|
└─ Skript: Fenster einordnen, Stufen anwenden -> { status, plaetze, gebremst, grund }
|
||||||
|
Rückfall, wenn verbrauch.js nicht läuft (etwa als Kind): plaetze = min(wunsch, 2)
|
||||||
|
|
||||||
|
welle-mit-pruefung ── workflow('executer-welle') ── workflow('pruefung', { verbrauch: false })
|
||||||
|
└─ liest selbst vor und nach der Prüfung ab
|
||||||
|
|
||||||
|
.vault-tools/workflow-pruefung/
|
||||||
|
attrappe.mjs Globals des Laufzeitsystems nachgebaut, streng wie das Ziel
|
||||||
|
faelle_workflows.mjs Grundlinie und Angriffsliste je Workflow
|
||||||
|
mutanten_workflows.py Mutantenliste gegen die Workflows
|
||||||
|
verbrauch_reihe.py Differenzen zwischen Ablesungen aus journal.jsonl
|
||||||
|
faelle_verbrauch_reihe.py, mutanten_reihe.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Entscheidungsliste (pruefwerkzeuge.md, Abschnitt 1)
|
||||||
|
|
||||||
|
| Frage | Herkunft | Antwort |
|
||||||
|
|---|---|---|
|
||||||
|
| Welche Fenster zählen? | Quelle | `get_usage` in der Desktop-App lieferte am 2026-09-24 `5-hour limit`, `Weekly · all models`, `Weekly · Fable`. Von 19 typisierten Limit-Abbrüchen seit 22.08. waren 16 `five_hour`, 3 `seven_day`, keiner `seven_day_opus`. Gezählt werden 5 Stunden und Woche aller Modelle, dazu `Weekly · Opus`, falls es auftaucht. |
|
||||||
|
| Woran erkennt das Skript sie? | Quelle, Annahme aufgelöst | An den Labels (Regex, Groß- und Kleinschreibung egal). Ein unbekanntes oder fehlendes Fenster ist der Status `unbekannte_fenster` mit Rückfall auf zwei, nie ein stilles Grün. |
|
||||||
|
| Was ist ein gültiger Wert? | Projektvorgabe | Eine endliche Zahl ab 0. Über 100 % heißt überschritten und bremst. |
|
||||||
|
| Welche Stufen? | Projektvorgabe, vorläufig | Stehen nur in `verbrauch.js`: 5 Stunden unter 50 % vier, unter 70 % zwei, unter 85 % einer, sonst gebremst; Woche mit 70, 85 und 95 %. Nachgestellt wird nach der Messreihe. |
|
||||||
|
| Harte Obergrenze? | Projektvorgabe | Vier (Mechanik für Stufe 3). |
|
||||||
|
| Was gilt ohne Ablesung? | Projektvorgabe | Zwei, die bisherige Stammregel. |
|
||||||
|
| Wer wünscht wie viele? | Projektvorgabe | Plankritik: so viele wie Linsen (Stufe 2). Planung: zwei (Stufe 1), drei bis vier über `plaetze` erst nach der Eichung (Stufe 3). Widerlegung: höchstens zwei. |
|
||||||
|
| Wer entscheidet? | Projektvorgabe | Das Skript. Der Haiku-Agent überträgt nur Zahlen (Harness-Gedächtnis: `lokales-modell-urteilt-nicht`). |
|
||||||
|
| Wann ist zwischen zwei Ablesungen ein Reset? | Quelle | `resetsAt` schwankt je Ablesung um Millisekunden und springt über Sekunden- und Datumsgrenzen (gemessen 2026-09-24). Reset ist eine Verschiebung um mehr als fünf Minuten oder ein fallender Wert. |
|
||||||
|
| Wie oft wird abgelesen? | Projektvorgabe | Plankritik vor den Linsen, vor der Widerlegung, am Ende. Planung am Anfang, nach je vier fertigen Planern, solange Tasks offen sind, und am Ende. Prüfung vor und nach dem `pruefer`. Eine Ablesung kostete 12 bis 13 s. |
|
||||||
|
|
||||||
|
## Dateien und Tasks
|
||||||
|
|
||||||
|
| Task | Zieldatei | Art | Modell | Welle |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 01 | `.claude/workflows/verbrauch.js` | neu | sonnet, low | 0 (ausgeführt) |
|
||||||
|
| 02 | `.claude/workflows/planung.js` | neu | haiku | 1 |
|
||||||
|
| 03 | `.claude/workflows/plankritik.js` | ersetzt | haiku | 1 |
|
||||||
|
| 04 | `.claude/workflows/pruefung.js` | ersetzt | haiku | 1 |
|
||||||
|
| 05 | `.claude/workflows/welle-mit-pruefung.js` | ersetzt | haiku | 1 |
|
||||||
|
| 06 | `.vault-tools/workflow-pruefung/attrappe.mjs`, `faelle_workflows.mjs`, `mutanten_workflows.py` | neu | sonnet, low | 2 |
|
||||||
|
| 07 | `.vault-tools/workflow-pruefung/verbrauch_reihe.py`, `faelle_verbrauch_reihe.py`, `mutanten_reihe.py` | neu | sonnet, low | 1 |
|
||||||
|
| 08 | `.claude/workflows/verbrauch.js` | Delta zu 01 | haiku | 1 |
|
||||||
|
| 09 | acht Dateien aus 02 bis 07 | Delta nach der Prüfung | sonnet, low | 3 |
|
||||||
|
|
||||||
|
Die Zieldateien einer Welle sind disjunkt. **Welle 0** zog Task 01 vor die Plankritik, damit sie schon mit echter Ablesung lief. **Task 08** bessert Task 01 nach der Plankritik als Delta nach; Task 01 bleibt byte-gleich. **Welle 2** folgt auf Welle 1, weil Task 06 die Fälle und die Mutanten gegen die Dateien aus Welle 1 fährt.
|
||||||
|
|
||||||
|
## Plankritik
|
||||||
|
|
||||||
|
Gelaufen am 2026-09-24 mit der neuen `plankritik.js` über `scriptPath` (wf_32edc4e9-501, 26 Minuten, drei Linsen gleichzeitig nach einer Ablesung von 44 %). 32 Befunde nach dem Zusammenfassen, 12 bestätigt, keiner widerlegt, 20 kleine ungeprüft. Eingearbeitet:
|
||||||
|
|
||||||
|
- **Blockierend, alle drei Linsen:** `verbrauch_reihe.py` hielt jedes echte Paar für einen Reset, weil `resetsAt` je Ablesung um Millisekunden schwankt. Die Fälle hatten denselben festen Zeitstempel wie der Produktivcode angenommen. Jetzt gilt die Toleranz aus der Entscheidungsliste; die Grundlinie trägt echte Zitterwerte.
|
||||||
|
- **Fehlende Fälle:** gesenkte Plätze vor der Widerlegung, gesenkte und gehobene Plätze beim Nachlesen der Planung, Bremse am Anfang der Planung, Startreihenfolge, kein Nachlesen ohne offene Tasks, Testort in allen Aufträgen, Wochenschwellen knapp darunter, Opus-Woche unter der Woche aller Modelle, Kappen am Ende eines Paars, höchstes statt erstes Fenster, Ablesezeit.
|
||||||
|
- **Mutantenliste nicht im Plan:** Die Mutantenläufe liegen jetzt als Zieldateien in Task 06 und 07.
|
||||||
|
- **Stufe 3 zu früh:** `planung.js` gibt ohne `plaetze` zwei Plätze vor.
|
||||||
|
- **Über 100 %:** bremst jetzt (Task 08).
|
||||||
|
- **Klein, mit erledigt:** `zaehlung` bleibt im gebremsten Fall ein Objekt, ein Planer-Ergebnis zählt nur mit der erwarteten Taskdatei, die Attrappe führt Agenten im Kind unter `▸ <name>` und wirft die Meldung des Ziels, `ascii` heißt `nur_ascii`, die Kommentare zur Hilfsfunktion stimmen.
|
||||||
|
- **Offen gelassen:** die Laufkopie unter `_lauf/` löscht der Koordinator nach dem letzten Lauf; ob gleichzeitige Linsen sich in einer gemeinsamen Kopie stören, ist ungemessen (siehe Risiken).
|
||||||
|
|
||||||
|
## Prüfung
|
||||||
|
|
||||||
|
Gelaufen am 2026-09-24 mit der neuen `pruefung.js` über `scriptPath` (wf_e0294c82-76c, `pruefer` auf `high`, 24 Minuten, Ablesungen 13 %/16 % vorher und 18 %/18 % nachher). Kein blockierender Befund, 2 wichtige, 12 kleine. Der Prüfer riet von einer vollen weiteren Runde ab und nannte die Nacharbeit mit Substanz. Eingearbeitet als **Task 09** (Delta, 17 Blöcke):
|
||||||
|
|
||||||
|
- **Wichtig:** Die Reset-Toleranz hatte keinen Fall für ihren Wert; eine Toleranz von 1 s bis 6 h blieb grün. Jetzt vier Fälle (4:59, 5:01 vor und zurück, fünf Stunden bei steigendem Wert) und drei Mutanten am Wert.
|
||||||
|
- **Wichtig:** `planung.js` war nie im echten Laufzeitsystem gelaufen. Nachgeholt mit einer Probe aus drei Tasks, siehe Check 9.
|
||||||
|
- **Klein, erledigt:** doppelte Linsen werden abgewiesen; `welle-mit-pruefung` meldet `status: ohne_pruefung`, wenn keine Prüfung lief; der Dateiname des Planer-Ergebnisses wird mit Pfadgrenze verglichen; die Attrappe nummeriert wiederholte Kinder wie das Ziel (`▸ name #2`); Fälle für den Nachlesetakt, den Lesehinweis, ein fehlendes `verbrauch.js` und das verankerte 5-Stunden-Label.
|
||||||
|
- **Klein, bewusst nicht geändert:** Fehlt das 5-Stunden-Fenster, gilt der Rückfall auf zwei, auch wenn die Woche hoch steht (so in der Entscheidungsliste). Die Platzregel bis zur Eichung steht an drei Stellen (Stamm, `codeworkflow.md`, `planung.js`) und muss bei der Freigabe von Stufe 3 gemeinsam nachgezogen werden.
|
||||||
|
|
||||||
|
**Fremde Änderung im Baum:** Die Trading-Sitzung hat `plankritik.js` am 2026-09-24 um 21:47 Uhr nach einem echten Lauf nachgebessert. Die Ausgabe der Planwerkzeuge gilt nur noch bei `EXIT 0` als gelaufen. Dabei hat sie die Datei im Arbeitsbaum auf CRLF umgestellt; Git normalisiert das beim Commit. Diese Stelle steht nicht in diesem Plan und hat noch keinen eigenen Fall in `faelle_workflows.mjs`. Task 09 liegt sauber darüber.
|
||||||
|
|
||||||
|
## Rot-Nachweis
|
||||||
|
|
||||||
|
Erbracht an den Entwürfen; die Executer brauchen keine Produktivdatei beiseitezulegen.
|
||||||
|
|
||||||
|
- `faelle_workflows.mjs` gegen die vier alten Workflows ohne `verbrauch.js` und `planung.js` (Stand vor Task 09): 122 von 134 Fällen rot. Grün blieben die elf Selbsttests der Attrappe und „kritisch hebt auf xhigh", ein unverändertes Verhalten.
|
||||||
|
- `mutanten_workflows.py` nach Task 09: 52 Mutanten, alle gefangen bis auf den in `WIRKUNGSLOS` begründeten („harte Obergrenze fehlt": keine Stufe gibt mehr als vier Plätze).
|
||||||
|
- `mutanten_reihe.py` nach Task 09: 18 Mutanten, alle gefangen.
|
||||||
|
|
||||||
|
## Abnahme — Fertig heißt
|
||||||
|
|
||||||
|
1. `node .vault-tools/workflow-pruefung/faelle_workflows.mjs`: Die letzte Zeile enthält `0 gescheitert`, Exit 0.
|
||||||
|
2. `python .vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py`: Die letzte Zeile enthält `0 gescheitert`, Exit 0.
|
||||||
|
3. Jede Zieldatei ist byte-gleich mit der Kopie, auf die der ganze Plan angewendet ist (Task 01, 08, 02 bis 07, 06), belegt mit `cmp` und `welle_vergleich.py`.
|
||||||
|
4. `python .vault-tools/workflow-pruefung/mutanten_workflows.py` und `mutanten_reihe.py` im echten Baum: jeweils `0 ueberlebt, 0 veraltet`, Exit 0.
|
||||||
|
5. `verbrauch.js` liest im echten Laufzeitsystem ab: Status `ok`, 5-Stunden-Wert gleich der eigenen `get_usage`-Ablesung des Koordinators (erfüllt am 2026-09-24, wf_8746f9c7-f1f: 43 % und 12 % auf beiden Seiten).
|
||||||
|
6. `verbrauch.js` mit `wunsch` 3 und `probe` bei 95 % im echten Laufzeitsystem: `plaetze` 0, `gebremst` true (erfüllt, wf_8746f9c7-f1f).
|
||||||
|
7. `verbrauch_reihe.py` wertet die drei Ablesungen der Plankritik wf_32edc4e9-501 aus: Das Paar „vor Linsen → vor Widerlegung" steht im 5-Stunden-Fenster als `Reset` (das Fenster wurde um 18:30 UTC zurückgesetzt), das Paar „vor Widerlegung → Ende" mit einer Zahl.
|
||||||
|
8. `pruefung.js` läuft im echten Laufzeitsystem mit zwei Ablesungen im Ergebnis (erfüllt, wf_e0294c82-76c).
|
||||||
|
9. `planung.js` läuft im echten Laufzeitsystem mit einer Probe aus drei Tasks (2 und 3 hängen an 1): Task 1 läuft allein, 2 und 3 starten erst nach seinem Ende und laufen gleichzeitig, jede Taskdatei liegt im Planordner (erfüllt, wf_5df09faa-a4d: Task 1 20:06:59 bis 20:07:10 UTC, Task 2 und 3 ab 20:07:12; beide übernahmen die Zieldatei-Zeile aus Task 1).
|
||||||
|
|
||||||
|
## Lauf mit echten Daten
|
||||||
|
|
||||||
|
Die Checks 5 bis 8 sind der Lauf mit echten Daten. Die Plankritik dieses Plans war der erste echte Lauf mit drei gleichzeitigen Opus-Linsen: Sie starteten innerhalb einer Sekunde, die Linsenphase dauerte bis zur letzten Linse, und die Plankritik brauchte insgesamt 26 Minuten.
|
||||||
|
|
||||||
|
## Risiken
|
||||||
|
|
||||||
|
- **Andere Sitzungen nutzen die gespeicherten Workflows.** Die Trading-Sitzung fährt heute ab 22:04 Wellen und Prüfungen. Die neuen Fassungen sind rückwärtsverträglich: Alle neuen Argumente sind freiwillig, alle bisherigen Rückgabefelder bleiben. Ein altes `welle-mit-pruefung` mit neuem Kind `pruefung` ergibt Ablesungen mit `nicht_gelesen`, die Prüfung läuft trotzdem.
|
||||||
|
- **Verzögertes Nachladen.** Ein Aufruf über den Namen kann noch eine Weile die alte Fassung treffen (am 2026-09-23 gut eine Stunde, am 2026-09-24 gut 20 Minuten). Aufrufe über `scriptPath` lesen die Datei direkt.
|
||||||
|
- **Gemeinsame Kopie.** Bis zu drei Linsen und zwei Widerleger fahren Tests in derselben Kopie. Schon bei zwei Widerlegern liefen in 68 % ihrer Laufzeit zwei gleichzeitig. Ob sie sich dabei stören, ist nicht gemessen; es hängt davon ab, ob die Suite des Projekts gemeinsamen Zustand in die Kopie schreibt. Mutanten entstehen nach dem Auftrag in einer eigenen Kopie der Kopie.
|
||||||
|
- **`get_usage` gibt es nur in der Desktop-App.** In anderen Sitzungen gilt über den Rückfall die Zwei.
|
||||||
|
- **Die Stufen sind ungeeicht.** Ein erster Messpunkt: Ein Workflow mit 21 Opus-Agenten auf `high` hob das 5-Stunden-Fenster mit dem übrigen Sitzungsverbrauch um rund 36 Punkte und die Woche um rund 10 Punkte (Plan laut App „Pro").
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 01 — verbrauch.js: Planverbrauch lesen und Opus-Plätze rechnen"
|
||||||
|
status: draft
|
||||||
|
summary: "Neuer gespeicherter Workflow, der über get_usage das 5-Stunden-Fenster und die Woche abliest und daraus die erlaubten Opus-Plätze rechnet; die Stufen stehen nur hier."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
modell: sonnet
|
||||||
|
effort: low
|
||||||
|
modell_begruendung: "Abschrift: jede Zeile steht wörtlich im Plan, Zieldateien und Verbote sind genannt. Sonnet low statt Haiku, weil der Code das Zeichen · (U+00B7) trägt und Haiku in der Messung vom 2026-09-23 typografische Zeichen still ersetzt hat."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 01 — verbrauch.js: Planverbrauch lesen und Opus-Plätze rechnen
|
||||||
|
|
||||||
|
Die globalen Vorgaben stehen in `00-uebersicht.md` (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner `D:/Claude`; alle Pfade gelten von dort.
|
||||||
|
|
||||||
|
Was danach anders ist:
|
||||||
|
|
||||||
|
- **Es gibt `verbrauch.js`.** Ein Haiku-Agent lädt `mcp__ccd_session_mgmt__get_usage` und überträgt die Fenster; das Skript ordnet sie ein (`5-hour ...`, `Weekly ... all models`, falls vorhanden `Weekly ... Opus`) und rechnet `plaetze = min(wunsch, Stufe 5 Stunden, Stufe Woche, 4)`.
|
||||||
|
- **Ohne lesbare Ablesung gilt zwei**, wie bisher im Stamm. `nicht_gelesen` und `unbekannte_fenster` sind eigene Stati, nie ein stilles Grün.
|
||||||
|
- **`wunsch: 0` misst nur** und bremst nie; `probe` ersetzt den Agenten für den Regressionslauf im echten Laufzeitsystem.
|
||||||
|
|
||||||
|
## Zieldateien
|
||||||
|
|
||||||
|
- `.claude/workflows/verbrauch.js` (neu)
|
||||||
|
|
||||||
|
## Verbote
|
||||||
|
|
||||||
|
- Keine andere Datei, auch nicht die übrigen Workflows unter `.claude/workflows/` und nichts unter `AI/Regelwerk/`, `.claude/agents/` oder `CLAUDE.md`.
|
||||||
|
- Kein Commit, kein Push, kein Lauf eines echten Workflows.
|
||||||
|
- Kein Umformulieren, auch nicht von Kommentaren oder Sonderzeichen; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
### `.claude/workflows/verbrauch.js`
|
||||||
|
|
||||||
|
Die ganze Datei ist neu:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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 von 0 bis 100 trägt, ist ein Befund und wird nicht still übergangen.
|
||||||
|
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 || p > 100) return { fehler: `${f.label}: percentUsed ${JSON.stringify(p)} ist keine Zahl von 0 bis 100` }
|
||||||
|
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 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Im Vault-Wurzelordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "const t=require('fs').readFileSync(process.argv[1],'utf8').replace(/^export const meta = /m,'const meta = ');new (Object.getPrototypeOf(async function(){}).constructor)('agent','parallel','pipeline','phase','log','workflow','args','budget',t);console.log('PARSE OK')" .claude/workflows/verbrauch.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `PARSE OK`, Exit 0.
|
||||||
|
|
||||||
|
Danach `git status --porcelain -- .claude/workflows/verbrauch.js` zeigen.
|
||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 02 — planung.js: Planer starten, sobald ihre Voraussetzungen stehen"
|
||||||
|
status: draft
|
||||||
|
summary: "Neuer gespeicherter Workflow für die Erstplanung: je Task ein planer, gestartet nach den Voraussetzungen statt paarweise, Plätze nach verbrauch.js bis vier."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
modell: haiku
|
||||||
|
modell_begruendung: "Abschrift: jede Zeile steht wörtlich im Plan, Zieldateien und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche, Auslassungspunkte oder das Zeichen ·."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 02 — planung.js: Planer starten, sobald ihre Voraussetzungen stehen
|
||||||
|
|
||||||
|
Die globalen Vorgaben stehen in `00-uebersicht.md` (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner `D:/Claude`; alle Pfade gelten von dort.
|
||||||
|
|
||||||
|
Was danach anders ist:
|
||||||
|
|
||||||
|
- **Kein Warten auf Paare mehr.** Ein Task startet, sobald die Planer aller seiner Voraussetzungen ein Ergebnis mit der erwarteten Taskdatei geliefert haben; Tasks hinter einer gescheiterten Voraussetzung gelten als blockiert.
|
||||||
|
- **Plätze nach Verbrauch, vorgegeben zwei.** Am Anfang und nach je `nachlesen_alle` fertigen Planern liest `verbrauch.js` ab und senkt oder hebt die Plätze; gebremst heißt kein weiterer Start, die laufenden enden. Drei oder vier Plätze (Stufe 3) gibt es erst nach der Eichung über `plaetze`.
|
||||||
|
- **Fehler im Auftrag fallen vor dem ersten Agenten auf:** Verweis ins Leere, Zyklus, doppelte Nummer oder Datei, unbekanntes Modell.
|
||||||
|
|
||||||
|
## Zieldateien
|
||||||
|
|
||||||
|
- `.claude/workflows/planung.js` (neu)
|
||||||
|
|
||||||
|
## Verbote
|
||||||
|
|
||||||
|
- Keine andere Datei, auch nicht die übrigen Workflows unter `.claude/workflows/` und nichts unter `AI/Regelwerk/`, `.claude/agents/` oder `CLAUDE.md`.
|
||||||
|
- Kein Commit, kein Push, kein Lauf eines echten Workflows.
|
||||||
|
- Kein Umformulieren, auch nicht von Kommentaren oder Sonderzeichen; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
### `.claude/workflows/planung.js`
|
||||||
|
|
||||||
|
Die ganze Datei ist neu:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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.',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Im Vault-Wurzelordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "const t=require('fs').readFileSync(process.argv[1],'utf8').replace(/^export const meta = /m,'const meta = ');new (Object.getPrototypeOf(async function(){}).constructor)('agent','parallel','pipeline','phase','log','workflow','args','budget',t);console.log('PARSE OK')" .claude/workflows/planung.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `PARSE OK`, Exit 0.
|
||||||
|
|
||||||
|
Danach `git status --porcelain -- .claude/workflows/planung.js` zeigen.
|
||||||
+365
@@ -0,0 +1,365 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 03 — plankritik.js: Linsen gleichzeitig nach Verbrauch, Testort im Auftrag"
|
||||||
|
status: draft
|
||||||
|
summary: "Die Linsen laufen mit so vielen Opus-Plätzen, wie verbrauch.js erlaubt, die Widerlegung mit höchstens zwei; beide Phasen können bremsen; die Aufträge nennen, wo Tests laufen dürfen."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
modell: haiku
|
||||||
|
modell_begruendung: "Abschrift: jede Zeile steht wörtlich im Plan, Zieldateien und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche, Auslassungspunkte oder das Zeichen ·."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 03 — plankritik.js: Linsen gleichzeitig nach Verbrauch, Testort im Auftrag
|
||||||
|
|
||||||
|
Die globalen Vorgaben stehen in `00-uebersicht.md` (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner `D:/Claude`; alle Pfade gelten von dort.
|
||||||
|
|
||||||
|
Was danach anders ist:
|
||||||
|
|
||||||
|
- **Drei Linsen gleichzeitig**, sofern `verbrauch.js` drei Plätze gibt; bisher wartete die dritte Linse im Mittel 14,5 Minuten auf einen Opus-Platz (fünf echte Läufe).
|
||||||
|
- **Die Widerlegung bleibt bei höchstens zwei.** Vor Linsen und Widerlegung wird abgelesen; gebremst kommt als `status: gebremst` mit `gebremst_vor` zurück, ungeprüfte Befunde tragen `nicht geprüft (gebremst)`.
|
||||||
|
- **Testort im Auftrag:** Tests nur in der Kopie, nie im echten Projektbaum; Mutanten nur in einer eigenen Kopie der Kopie. Ohne Kopie keine Tests.
|
||||||
|
- **Die bisherigen Rückgabefelder bleiben**, auch `zaehlung` als Objekt im gebremsten Fall; neu sind `status`, `gebremst_vor`, `verbrauch`, `plaetze` und `naechster_schritt`.
|
||||||
|
|
||||||
|
## Zieldateien
|
||||||
|
|
||||||
|
- `.claude/workflows/plankritik.js` (geändert)
|
||||||
|
|
||||||
|
## Verbote
|
||||||
|
|
||||||
|
- Keine andere Datei, auch nicht die übrigen Workflows unter `.claude/workflows/` und nichts unter `AI/Regelwerk/`, `.claude/agents/` oder `CLAUDE.md`.
|
||||||
|
- Kein Commit, kein Push, kein Lauf eines echten Workflows.
|
||||||
|
- Kein Umformulieren, auch nicht von Kommentaren oder Sonderzeichen; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
### `.claude/workflows/plankritik.js`
|
||||||
|
|
||||||
|
Die ganze Datei ist neu (sie ersetzt die bisherige Fassung vollständig):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const meta = {
|
||||||
|
name: 'plankritik',
|
||||||
|
description: 'Liest einen Planordner mit getrennten pruefer-Linsen, gleichzeitig nach Verbrauch bis vier, fasst doppelte Befunde zusammen und lässt jeden wichtigen oder blockierenden Befund von einem zweiten pruefer zu widerlegen versuchen, höchstens zwei gleichzeitig',
|
||||||
|
whenToUse: 'Schritt 2 der Standardschleife vor dem ersten Executer, Pflicht ab etwa drei Tasks (AI/Regelwerk/codeworkflow.md, "Plankritik vor dem ersten Executer"). Args: {projekt, planordner, spec?, kopie?, materialisieren?: {ohne: [relpfade], tasks?: [taskdateien]}, suite?, linsen?, plaetze?: 1..4, verifizieren?: true, verifizieren_ab?: "wichtig"|"blockierend", effort?, werkzeuge?, zusatz?}. Ohne kopie baut materialisieren sie selbst (kopie_bauen.py plus plan_materialisiere.py); ohne() muss Geheimnisdateien und große Datenordner nennen. 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.',
|
||||||
|
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' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, "'\\''") + "'"
|
||||||
|
|
||||||
|
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.'}`,
|
||||||
|
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.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')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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' })
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
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')
|
||||||
|
if (r && r.laeufe && r.laeufe[0]) ctx.werkzeuge = r.laeufe[0].ausgabe
|
||||||
|
else log('Planwerkzeuge: Hilfsagent ohne Ergebnis; Linse bezeichner fährt sie selbst')
|
||||||
|
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(),
|
||||||
|
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(),
|
||||||
|
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.',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Im Vault-Wurzelordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "const t=require('fs').readFileSync(process.argv[1],'utf8').replace(/^export const meta = /m,'const meta = ');new (Object.getPrototypeOf(async function(){}).constructor)('agent','parallel','pipeline','phase','log','workflow','args','budget',t);console.log('PARSE OK')" .claude/workflows/plankritik.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `PARSE OK`, Exit 0.
|
||||||
|
|
||||||
|
Danach `git status --porcelain -- .claude/workflows/plankritik.js` zeigen.
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 04 — pruefung.js: Verbrauch vor und nach dem pruefer mitschreiben"
|
||||||
|
status: draft
|
||||||
|
summary: "Die Prüfung liest vor und nach dem pruefer den Planverbrauch ab, nur zum Messen; verbrauch false schaltet das ab, wenn der Elternworkflow selbst liest."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
modell: haiku
|
||||||
|
modell_begruendung: "Abschrift: jede Zeile steht wörtlich im Plan, Zieldateien und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche, Auslassungspunkte oder das Zeichen ·."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 04 — pruefung.js: Verbrauch vor und nach dem pruefer mitschreiben
|
||||||
|
|
||||||
|
Die globalen Vorgaben stehen in `00-uebersicht.md` (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner `D:/Claude`; alle Pfade gelten von dort.
|
||||||
|
|
||||||
|
Was danach anders ist:
|
||||||
|
|
||||||
|
- **Zwei Ablesungen um den pruefer**, `wunsch: 0`, also Messung ohne Bremse. Das Ergebnis trägt `verbrauch`.
|
||||||
|
- **Als Kind** kann `pruefung` keinen weiteren Workflow aufrufen; ohne `verbrauch: false` stehen die Ablesungen dann als `nicht_gelesen` da, die Prüfung läuft trotzdem.
|
||||||
|
|
||||||
|
## Zieldateien
|
||||||
|
|
||||||
|
- `.claude/workflows/pruefung.js` (geändert)
|
||||||
|
|
||||||
|
## Verbote
|
||||||
|
|
||||||
|
- Keine andere Datei, auch nicht die übrigen Workflows unter `.claude/workflows/` und nichts unter `AI/Regelwerk/`, `.claude/agents/` oder `CLAUDE.md`.
|
||||||
|
- Kein Commit, kein Push, kein Lauf eines echten Workflows.
|
||||||
|
- Kein Umformulieren, auch nicht von Kommentaren oder Sonderzeichen; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
### `.claude/workflows/pruefung.js`
|
||||||
|
|
||||||
|
Die ganze Datei ist neu (sie ersetzt die bisherige Fassung vollständig):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Im Vault-Wurzelordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "const t=require('fs').readFileSync(process.argv[1],'utf8').replace(/^export const meta = /m,'const meta = ');new (Object.getPrototypeOf(async function(){}).constructor)('agent','parallel','pipeline','phase','log','workflow','args','budget',t);console.log('PARSE OK')" .claude/workflows/pruefung.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `PARSE OK`, Exit 0.
|
||||||
|
|
||||||
|
Danach `git status --porcelain -- .claude/workflows/pruefung.js` zeigen.
|
||||||
+246
@@ -0,0 +1,246 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 05 — welle-mit-pruefung.js: Rückkehrpunkt nach dem Vergleich und Ablesung um die Prüfung"
|
||||||
|
status: draft
|
||||||
|
summary: "Mit bis vergleich endet der Workflow vor der Prüfung und gibt pruefung_args zurück, damit der Koordinator seine Gegenprüfung parallel zur Prüfung fahren kann; sonst liest er vor und nach der Prüfung ab."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
modell: haiku
|
||||||
|
modell_begruendung: "Abschrift: jede Zeile steht wörtlich im Plan, Zieldateien und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche, Auslassungspunkte oder das Zeichen ·."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 05 — welle-mit-pruefung.js: Rückkehrpunkt nach dem Vergleich und Ablesung um die Prüfung
|
||||||
|
|
||||||
|
Die globalen Vorgaben stehen in `00-uebersicht.md` (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner `D:/Claude`; alle Pfade gelten von dort.
|
||||||
|
|
||||||
|
Was danach anders ist:
|
||||||
|
|
||||||
|
- **`bis: "vergleich"`** gibt `pruefung_args` mit den Rohbelegen im `schwerpunkt` zurück. Der Koordinator startet `pruefung` damit selbst und fährt währenddessen seine Gegenprüfung, nur lesend.
|
||||||
|
- **Ohne `bis`** liest der Workflow vor und nach dem Kind `pruefung` ab und gibt ihm `verbrauch: false` mit.
|
||||||
|
|
||||||
|
## Zieldateien
|
||||||
|
|
||||||
|
- `.claude/workflows/welle-mit-pruefung.js` (geändert)
|
||||||
|
|
||||||
|
## Verbote
|
||||||
|
|
||||||
|
- Keine andere Datei, auch nicht die übrigen Workflows unter `.claude/workflows/` und nichts unter `AI/Regelwerk/`, `.claude/agents/` oder `CLAUDE.md`.
|
||||||
|
- Kein Commit, kein Push, kein Lauf eines echten Workflows.
|
||||||
|
- Kein Umformulieren, auch nicht von Kommentaren oder Sonderzeichen; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
### `.claude/workflows/welle-mit-pruefung.js`
|
||||||
|
|
||||||
|
Die ganze Datei ist neu (sie ersetzt die bisherige Fassung vollständig):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const meta = {
|
||||||
|
name: 'welle-mit-pruefung',
|
||||||
|
description: 'Schritt 3 und 4 in einem Aufruf: Kopie mit angewendetem Plan vor der Welle, executer-welle, Byte-Vergleich und Suite, danach pruefung mit den Rohbelegen; mit bis "vergleich" endet er vor der Prüfung',
|
||||||
|
whenToUse: 'Nach der Plankritik, sobald eine Welle mit disjunkten Zieldateien steht (AI/Regelwerk/codeworkflow.md, Schritte 3 und 4). Args: {projekt, planordner, tasks: [{datei, modell, effort?}], kopie? oder ohne: [relpfade, nicht leer], prosa?: [relpfade], suite?, kritisch?, abschluss?, effort?, version?, schwerpunkt?, rot_nachweis_erbracht?, zusatz?, werkzeuge?, bis?: "vergleich"}. Ohne kopie wird sie vor der Welle gebaut; ohne nennt dann .git und Geheimnisdateien. Mit bis "vergleich" kommt pruefung_args zurück: pruefung damit selbst starten und währenddessen die eigene Gegenprüfung fahren. Ersetzt die eigene Gegenprüfung des Koordinators nicht.',
|
||||||
|
phases: [
|
||||||
|
{ title: 'Kopie', detail: 'Hilfsagent: Stand vor der Welle kopieren und den Plan der Welle anwenden' },
|
||||||
|
{ title: 'Welle', detail: 'executer-welle' },
|
||||||
|
{ title: 'Vergleich', detail: 'Hilfsagent: welle_vergleich.py und Suite im echten Baum' },
|
||||||
|
{ title: 'Prüfung', detail: 'pruefung mit den Rohbelegen' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ruft die gespeicherten Workflows executer-welle und pruefung verschachtelt auf, damit ihre
|
||||||
|
// Aufträge und Rollenparameter an genau einer Stelle stehen. Der Hilfsagent hat keinen
|
||||||
|
// agentType und bekommt seinen Effort deshalb mit (low). Opus läuft nur in den Kindern,
|
||||||
|
// und die laufen nacheinander; die Obergrenze von zwei hält executer-welle selbst.
|
||||||
|
// 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: 'fertig',
|
||||||
|
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.',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Im Vault-Wurzelordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "const t=require('fs').readFileSync(process.argv[1],'utf8').replace(/^export const meta = /m,'const meta = ');new (Object.getPrototypeOf(async function(){}).constructor)('agent','parallel','pipeline','phase','log','workflow','args','budget',t);console.log('PARSE OK')" .claude/workflows/welle-mit-pruefung.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `PARSE OK`, Exit 0.
|
||||||
|
|
||||||
|
Danach `git status --porcelain -- .claude/workflows/welle-mit-pruefung.js` zeigen.
|
||||||
+1116
File diff suppressed because it is too large.
Load diff
+661
@@ -0,0 +1,661 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 07 — verbrauch_reihe.py: Messreihe aus den Workflow-Protokollen"
|
||||||
|
status: draft
|
||||||
|
summary: "Auswertung der Ablesungen: Differenz im 5-Stunden-Fenster und in der Woche zwischen zwei Ablesungen und die Agentenminuten dazwischen, dazu die Fälle."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
modell: sonnet
|
||||||
|
effort: low
|
||||||
|
modell_begruendung: "Abschrift: jede Zeile steht wörtlich im Plan, Zieldateien und Verbote sind genannt. Sonnet low statt Haiku, weil der Code das Zeichen · (U+00B7) trägt und Haiku in der Messung vom 2026-09-23 typografische Zeichen still ersetzt hat."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 07 — verbrauch_reihe.py: Messreihe aus den Workflow-Protokollen
|
||||||
|
|
||||||
|
Die globalen Vorgaben stehen in `00-uebersicht.md` (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner `D:/Claude`; alle Pfade gelten von dort.
|
||||||
|
|
||||||
|
Was danach anders ist:
|
||||||
|
|
||||||
|
- **`verbrauch_reihe.py`** liest aus `~/.claude/projects/D--Claude/*/subagents/workflows/wf_*/` die Agenten mit Label `verbrauch:` und rechnet je Paar die Differenz. Ein Reset ist eine Verschiebung von `resetsAt` um mehr als fünf Minuten oder ein fallender Wert; das Zittern um Millisekunden zählt nicht.
|
||||||
|
- **Die Ausgabe ist reines ASCII**, kaputte Zeilen und Abstürze einzelner Läufe sind Hinweise und kein Abbruch.
|
||||||
|
- **`mutanten_reihe.py`** fährt die Mutantenliste gegen das Skript.
|
||||||
|
|
||||||
|
## Zieldateien
|
||||||
|
|
||||||
|
- `.vault-tools/workflow-pruefung/verbrauch_reihe.py` (neu)
|
||||||
|
- `.vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py` (neu)
|
||||||
|
- `.vault-tools/workflow-pruefung/mutanten_reihe.py` (neu)
|
||||||
|
|
||||||
|
## Verbote
|
||||||
|
|
||||||
|
- Keine andere Datei, auch nicht die übrigen Workflows unter `.claude/workflows/` und nichts unter `AI/Regelwerk/`, `.claude/agents/` oder `CLAUDE.md`.
|
||||||
|
- Kein Commit, kein Push, kein Lauf eines echten Workflows.
|
||||||
|
- Kein Umformulieren, auch nicht von Kommentaren oder Sonderzeichen; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/verbrauch_reihe.py`
|
||||||
|
|
||||||
|
Die ganze Datei ist neu:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Wertet die Verbrauchsablesungen der Workflows aus: wie viel Prozent des 5-Stunden-Fensters und der Woche zwischen zwei Ablesungen verbraucht wurden und welche Agenten dazwischen liefen.
|
||||||
|
|
||||||
|
Koordinator-Werkzeug (entstanden 2026-09-24). Es misst und beurteilt nichts; die Stufen in
|
||||||
|
.claude/workflows/verbrauch.js stellt der Mensch nach dieser Messreihe ein.
|
||||||
|
Quelle der Daten ist der Session-Speicher von Claude Code. Je Workflow-Lauf liegen unter
|
||||||
|
<speicher>/<sitzung>/subagents/workflows/<lauf>/ die Datei journal.jsonl (Ereignisse "started"
|
||||||
|
mit agentId und label, "result" mit agentId und result), je Agent agent-<id>.jsonl (Zeilen mit
|
||||||
|
timestamp) und agent-<id>.meta.json (model). Ablesungen sind Agenten, deren label mit
|
||||||
|
"verbrauch:" beginnt; ihr result traegt fenster mit label, percentUsed und resetsAt.
|
||||||
|
Die Fenster werden wie in verbrauch.js erkannt: "5-hour ..." und "Weekly ... all models".
|
||||||
|
Was das Skript nicht sieht: den Verbrauch anderer Sitzungen und der Hauptsitzung im selben
|
||||||
|
Zeitraum (er steckt in der Differenz mit drin), Ablesungen ueber probe (sie starten keinen
|
||||||
|
Agenten) und Laeufe, deren Protokolle geloescht sind. Die Kennzahl je Opus-Agentenstunde ist eine
|
||||||
|
Naeherung, kein Nachweis.
|
||||||
|
Aufruf: python verbrauch_reihe.py [--speicher <pfad>] [--seit JJJJ-MM-TT]
|
||||||
|
Exit 0 mit Auswertung (auch bei null Ablesungen), 2 wenn der Speicher fehlt oder --seit kein
|
||||||
|
Datum ist. Die Ausgabe ist reines ASCII, damit sie auch auf einer cp1252-Konsole durchlaeuft.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STANDARD_SPEICHER = Path.home() / ".claude" / "projects" / "D--Claude"
|
||||||
|
RESET_TOLERANZ = timedelta(minutes=5)
|
||||||
|
FUENF = re.compile(r"^\s*5[- ]?hour", re.I)
|
||||||
|
WOCHE = re.compile(r"^\s*weekly\b.*\ball models\b", re.I)
|
||||||
|
FAMILIEN = ("opus", "sonnet", "haiku", "fable")
|
||||||
|
|
||||||
|
|
||||||
|
def zeit(text):
|
||||||
|
return datetime.fromisoformat(text.replace("Z", "+00:00"))
|
||||||
|
|
||||||
|
|
||||||
|
def lies_jsonl(pfad, befunde):
|
||||||
|
"""Liest eine JSONL-Datei zeilenweise; kaputte Zeilen sind ein Befund, kein Absturz."""
|
||||||
|
zeilen = []
|
||||||
|
try:
|
||||||
|
with pfad.open(encoding="utf-8") as datei:
|
||||||
|
for nr, zeile in enumerate(datei, 1):
|
||||||
|
if not zeile.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
zeilen.append(json.loads(zeile))
|
||||||
|
except ValueError:
|
||||||
|
befunde.append(f"{pfad.parent.name}/{pfad.name}: Zeile {nr} ist kein JSON, uebergangen")
|
||||||
|
except OSError as e:
|
||||||
|
befunde.append(f"{pfad.parent.name}/{pfad.name}: nicht lesbar ({type(e).__name__})")
|
||||||
|
return zeilen
|
||||||
|
|
||||||
|
|
||||||
|
def einordnen(fenster):
|
||||||
|
"""(prozent, resetsAt) fuer 5 Stunden und Woche; None, wo das Fenster fehlt oder kaputt ist."""
|
||||||
|
fuenf = woche = None
|
||||||
|
if not isinstance(fenster, list):
|
||||||
|
return None, None
|
||||||
|
for f in fenster:
|
||||||
|
if not isinstance(f, dict) or not isinstance(f.get("label"), str):
|
||||||
|
return None, None
|
||||||
|
art = "fuenf" if FUENF.match(f["label"]) else "woche" if WOCHE.match(f["label"]) else None
|
||||||
|
if art is None:
|
||||||
|
continue
|
||||||
|
p = f.get("percentUsed")
|
||||||
|
if isinstance(p, bool) or not isinstance(p, (int, float)) or not math.isfinite(p) or p < 0:
|
||||||
|
return None, None
|
||||||
|
eintrag = (float(p), f.get("resetsAt"))
|
||||||
|
if art == "fuenf" and (fuenf is None or p > fuenf[0]):
|
||||||
|
fuenf = eintrag
|
||||||
|
elif art == "woche" and (woche is None or p > woche[0]):
|
||||||
|
woche = eintrag
|
||||||
|
return fuenf, woche
|
||||||
|
|
||||||
|
|
||||||
|
def zeitspanne(ordner, agent_id, befunde):
|
||||||
|
stempel = []
|
||||||
|
for o in lies_jsonl(ordner / f"agent-{agent_id}.jsonl", befunde):
|
||||||
|
t = o.get("timestamp") if isinstance(o, dict) else None
|
||||||
|
if isinstance(t, str):
|
||||||
|
try:
|
||||||
|
stempel.append(zeit(t))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return (min(stempel), max(stempel)) if stempel else None
|
||||||
|
|
||||||
|
|
||||||
|
def familie(ordner, agent_id):
|
||||||
|
try:
|
||||||
|
modell = json.loads((ordner / f"agent-{agent_id}.meta.json").read_text(encoding="utf-8")).get("model")
|
||||||
|
except (OSError, ValueError, AttributeError):
|
||||||
|
return "?"
|
||||||
|
if not isinstance(modell, str):
|
||||||
|
return "?"
|
||||||
|
for f in FAMILIEN:
|
||||||
|
if f in modell.lower():
|
||||||
|
return f
|
||||||
|
return modell
|
||||||
|
|
||||||
|
|
||||||
|
def lauf_auswerten(ordner, befunde):
|
||||||
|
labels, ergebnisse = {}, {}
|
||||||
|
for o in lies_jsonl(ordner / "journal.jsonl", befunde):
|
||||||
|
if not isinstance(o, dict) or not isinstance(o.get("agentId"), str):
|
||||||
|
continue
|
||||||
|
if o.get("type") == "started":
|
||||||
|
labels[o["agentId"]] = o.get("label") if isinstance(o.get("label"), str) else ""
|
||||||
|
elif o.get("type") == "result":
|
||||||
|
ergebnisse[o["agentId"]] = o.get("result")
|
||||||
|
ablesungen, agenten = [], []
|
||||||
|
for agent_id, label in labels.items():
|
||||||
|
spanne = zeitspanne(ordner, agent_id, befunde)
|
||||||
|
if label.startswith("verbrauch:"):
|
||||||
|
if spanne is None:
|
||||||
|
befunde.append(f"{ordner.name}: Ablesung {label} ohne Zeitstempel, uebergangen")
|
||||||
|
continue
|
||||||
|
ergebnis = ergebnisse.get(agent_id)
|
||||||
|
fuenf, woche = einordnen(ergebnis.get("fenster") if isinstance(ergebnis, dict) else None)
|
||||||
|
# Ablesezeit ist das Ende des Ableseagenten: get_usage laeuft kurz vor seinem Schluss.
|
||||||
|
ablesungen.append({"anlass": label[len("verbrauch:"):], "zeit": spanne[1], "fuenf": fuenf, "woche": woche})
|
||||||
|
elif spanne is not None:
|
||||||
|
agenten.append({"familie": familie(ordner, agent_id), "start": spanne[0], "ende": spanne[1]})
|
||||||
|
ablesungen.sort(key=lambda a: a["zeit"])
|
||||||
|
return ablesungen, agenten
|
||||||
|
|
||||||
|
|
||||||
|
def zeitpunkt(text):
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return zeit(text)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def differenz(a, b):
|
||||||
|
"""Prozentpunkte von a nach b im selben Fenster.
|
||||||
|
|
||||||
|
get_usage rechnet resetsAt bei jeder Ablesung neu aus; der Wert schwankt um Millisekunden und
|
||||||
|
springt dabei auch ueber eine Sekunden- oder Datumsgrenze (gemessen 2026-09-24: 18:29:59.664Z,
|
||||||
|
18:30:00.267Z, 18:30:00.459Z fuer dasselbe Fenster). Als Reset gilt deshalb erst eine
|
||||||
|
Verschiebung um mehr als RESET_TOLERANZ oder ein fallender Wert.
|
||||||
|
"""
|
||||||
|
if a is None or b is None:
|
||||||
|
return "unbekannt"
|
||||||
|
ra, rb = zeitpunkt(a[1]), zeitpunkt(b[1])
|
||||||
|
if ra is None or rb is None or (ra.tzinfo is None) != (rb.tzinfo is None):
|
||||||
|
return "unbekannt"
|
||||||
|
if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:
|
||||||
|
return "Reset"
|
||||||
|
return round(b[0] - a[0], 1)
|
||||||
|
|
||||||
|
|
||||||
|
def paare(ordner, ablesungen, agenten):
|
||||||
|
for a, b in zip(ablesungen, ablesungen[1:]):
|
||||||
|
minuten = {}
|
||||||
|
for g in agenten:
|
||||||
|
anteil = (min(g["ende"], b["zeit"]) - max(g["start"], a["zeit"])).total_seconds()
|
||||||
|
if anteil > 0:
|
||||||
|
minuten[g["familie"]] = minuten.get(g["familie"], 0.0) + anteil / 60
|
||||||
|
yield {
|
||||||
|
"lauf": ordner.name,
|
||||||
|
"von": a["anlass"],
|
||||||
|
"bis": b["anlass"],
|
||||||
|
"minuten": round((b["zeit"] - a["zeit"]).total_seconds() / 60, 1),
|
||||||
|
"agenten": {k: round(v, 1) for k, v in sorted(minuten.items())},
|
||||||
|
"d5": differenz(a["fuenf"], b["fuenf"]),
|
||||||
|
"dw": differenz(a["woche"], b["woche"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "Ä": "Ae", "Ö": "Oe", "Ü": "Ue", "ß": "ss"})
|
||||||
|
|
||||||
|
|
||||||
|
def nur_ascii(text):
|
||||||
|
return str(text).translate(UMLAUTE).encode("ascii", "replace").decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
p = argparse.ArgumentParser(description="Messreihe der Verbrauchsablesungen aus den Workflow-Protokollen")
|
||||||
|
p.add_argument("--speicher", type=Path, default=STANDARD_SPEICHER)
|
||||||
|
p.add_argument("--seit", default=None, help="nur Ablesungen ab diesem Tag (JJJJ-MM-TT, UTC)")
|
||||||
|
a = p.parse_args(argv)
|
||||||
|
if not a.speicher.is_dir():
|
||||||
|
print(nur_ascii(f"Speicher fehlt: {a.speicher}"))
|
||||||
|
return 2
|
||||||
|
seit = None
|
||||||
|
if a.seit:
|
||||||
|
try:
|
||||||
|
seit = datetime.fromisoformat(a.seit).replace(tzinfo=timezone.utc)
|
||||||
|
except ValueError:
|
||||||
|
print(nur_ascii(f"--seit {a.seit!r} ist kein Datum JJJJ-MM-TT"))
|
||||||
|
return 2
|
||||||
|
befunde, zeilen, anzahl = [], [], 0
|
||||||
|
for ordner in sorted(a.speicher.glob("*/subagents/workflows/wf_*")):
|
||||||
|
if not ordner.is_dir():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ablesungen, agenten = lauf_auswerten(ordner, befunde)
|
||||||
|
if seit is not None:
|
||||||
|
ablesungen = [x for x in ablesungen if x["zeit"] >= seit]
|
||||||
|
anzahl += len(ablesungen)
|
||||||
|
zeilen.extend(paare(ordner, ablesungen, agenten))
|
||||||
|
except Exception as e: # Ein Absturz ist ein Befund (pruefwerkzeuge.md, Abschnitt 4).
|
||||||
|
befunde.append(f"{ordner.name}: Auswertung abgebrochen ({type(e).__name__}: {e})")
|
||||||
|
print(f"{anzahl} Ablesungen, {len(zeilen)} Paare")
|
||||||
|
if zeilen:
|
||||||
|
print("| Lauf | von -> bis | Minuten | Agentenminuten | d 5h % | d Woche % |")
|
||||||
|
print("|---|---|---|---|---|---|")
|
||||||
|
for z in zeilen:
|
||||||
|
agenten = ", ".join(f"{k} {v}" for k, v in z["agenten"].items()) or "-"
|
||||||
|
print(nur_ascii(f"| {z['lauf']} | {z['von']} -> {z['bis']} | {z['minuten']} | {agenten} | {z['d5']} | {z['dw']} |"))
|
||||||
|
werte = [z for z in zeilen if isinstance(z["d5"], float) and z["agenten"].get("opus", 0) > 0]
|
||||||
|
if werte:
|
||||||
|
prozent = sum(z["d5"] for z in werte)
|
||||||
|
stunden = sum(z["agenten"]["opus"] for z in werte) / 60
|
||||||
|
print(f"Naeherung: {prozent / stunden:.1f} % des 5-Stunden-Fensters je Opus-Agentenstunde "
|
||||||
|
f"({len(werte)} Paare, {stunden:.1f} Opus-Stunden; andere Sitzungen und Nicht-Opus-Agenten stecken in der Differenz)")
|
||||||
|
else:
|
||||||
|
print("Keine auswertbaren Paare mit Opus-Agenten.")
|
||||||
|
if befunde:
|
||||||
|
print("Hinweise:")
|
||||||
|
for b in befunde:
|
||||||
|
print(nur_ascii("- " + b))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py`
|
||||||
|
|
||||||
|
Die ganze Datei ist neu:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Faelle fuer verbrauch_reihe.py: baut je Fall einen kuenstlichen Session-Speicher und faehrt das Skript als eigenen Prozess.
|
||||||
|
|
||||||
|
Die Grundlinie laeuft zuerst; scheitert sie, messen die uebrigen Faelle nichts. Der Prozess laeuft
|
||||||
|
ohne PYTHONIOENCODING, damit eine Ausgabe ausserhalb von ASCII auf cp1252 auffaellt.
|
||||||
|
Aufruf: python faelle_verbrauch_reihe.py
|
||||||
|
Exit 0, wenn alle Faelle bestehen, sonst 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SKRIPT = Path(__file__).with_name("verbrauch_reihe.py")
|
||||||
|
|
||||||
|
|
||||||
|
def fenster(fuenf, woche, reset5="2026-09-24T18:30:00Z", resetw="2026-09-28T00:00:00Z"):
|
||||||
|
return [
|
||||||
|
{"label": "5-hour limit", "percentUsed": fuenf, "resetsAt": reset5},
|
||||||
|
{"label": "Weekly · all models", "percentUsed": woche, "resetsAt": resetw},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def baue(speicher, laeufe):
|
||||||
|
"""laeufe: {lauf: [agent]}; agent: {id, label, start, ende, modell, ergebnis, ohne_zeit, kaputt}."""
|
||||||
|
for lauf, agenten in laeufe.items():
|
||||||
|
ordner = speicher / "sitzung-1" / "subagents" / "workflows" / lauf
|
||||||
|
ordner.mkdir(parents=True)
|
||||||
|
with (ordner / "journal.jsonl").open("w", encoding="utf-8") as journal:
|
||||||
|
journal.write(json.dumps({"type": "launched"}) + "\n")
|
||||||
|
for a in agenten:
|
||||||
|
journal.write(json.dumps({"type": "started", "agentId": a["id"], "label": a["label"]}) + "\n")
|
||||||
|
if a.get("kaputt"):
|
||||||
|
journal.write("{kein json\n")
|
||||||
|
if "ergebnis" in a:
|
||||||
|
journal.write(json.dumps({"type": "result", "agentId": a["id"], "result": a["ergebnis"]}) + "\n")
|
||||||
|
for a in agenten:
|
||||||
|
(ordner / f"agent-{a['id']}.meta.json").write_text(json.dumps({"model": a.get("modell", "haiku")}), encoding="utf-8")
|
||||||
|
if a.get("ohne_zeit"):
|
||||||
|
continue
|
||||||
|
with (ordner / f"agent-{a['id']}.jsonl").open("w", encoding="utf-8") as d:
|
||||||
|
d.write(json.dumps({"timestamp": a["start"], "type": "user"}) + "\n")
|
||||||
|
d.write(json.dumps({"timestamp": a["ende"], "type": "assistant"}) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def ablesung(nr, anlass, zeitpunkt, fuenf, woche, **extra):
|
||||||
|
ende = extra.pop("bis", None) or zeitpunkt
|
||||||
|
return {"id": f"v{nr}", "label": "verbrauch:" + anlass, "start": zeitpunkt, "ende": ende, "modell": "haiku",
|
||||||
|
"ergebnis": {"verfuegbar": True, "status": "ok", "plan": "Pro", "fenster": extra.pop("liste", None) or fenster(fuenf, woche, **extra), "fehler": ""}}
|
||||||
|
|
||||||
|
|
||||||
|
# resetsAt schwankt bei echten Ablesungen um Millisekunden und springt ueber Sekunden- und
|
||||||
|
# Datumsgrenzen (gemessen 2026-09-24); die Grundlinie traegt deshalb solche Werte.
|
||||||
|
ZITTER = {"reset5": "2026-09-24T18:29:59.664Z", "resetw": "2026-09-27T23:59:59.664Z"}
|
||||||
|
|
||||||
|
|
||||||
|
def opus(nr, start, ende, modell="opus"):
|
||||||
|
return {"id": f"o{nr}", "label": f"plankritik:linse{nr}", "start": start, "ende": ende, "modell": modell, "ergebnis": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def fahre(speicher, *zusatz):
|
||||||
|
umgebung = {k: v for k, v in os.environ.items() if k not in ("PYTHONIOENCODING", "PYTHONUTF8")}
|
||||||
|
lauf = subprocess.run([sys.executable, str(SKRIPT), "--speicher", str(speicher), *zusatz],
|
||||||
|
capture_output=True, env=umgebung)
|
||||||
|
return lauf.returncode, lauf.stdout.decode("ascii", "replace"), lauf.stderr.decode("utf-8", "replace")
|
||||||
|
|
||||||
|
|
||||||
|
GRUND = {
|
||||||
|
"wf_a": [
|
||||||
|
ablesung(1, "plankritik:vor Linsen", "2026-09-24T10:00:00Z", 10, 5),
|
||||||
|
opus(1, "2026-09-24T10:00:10Z", "2026-09-24T10:30:10Z"),
|
||||||
|
ablesung(2, "plankritik:Ende", "2026-09-24T10:30:10Z", 14, 6, **ZITTER),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def f_grundlinie(tmp):
|
||||||
|
baue(tmp, GRUND)
|
||||||
|
code, aus, err = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0:
|
||||||
|
fehler.append(f"Exit {code}: {err[-300:]}")
|
||||||
|
for erwartet in ["2 Ablesungen, 1 Paare", "| wf_a | plankritik:vor Linsen -> plankritik:Ende | 30.2 | opus 30.0 | 4.0 | 1.0 |",
|
||||||
|
"Naeherung: 8.0 % des 5-Stunden-Fensters je Opus-Agentenstunde (1 Paare, 0.5 Opus-Stunden"]:
|
||||||
|
if erwartet not in aus:
|
||||||
|
fehler.append("fehlt: " + erwartet)
|
||||||
|
if "Hinweise" in aus:
|
||||||
|
fehler.append("unerwartete Hinweise")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_reset(tmp):
|
||||||
|
baue(tmp, {"wf_r": [ablesung(1, "a", "2026-09-24T10:00:00Z", 80, 5), opus(1, "2026-09-24T10:01:00Z", "2026-09-24T10:20:00Z"),
|
||||||
|
ablesung(2, "b", "2026-09-24T10:30:00Z", 3, 6, reset5="2026-09-24T15:30:00Z")]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0 or "| Reset | 1.0 |" not in aus:
|
||||||
|
fehler.append("Reset nicht erkannt: " + aus[-300:])
|
||||||
|
if "Keine auswertbaren Paare mit Opus-Agenten." not in aus:
|
||||||
|
fehler.append("Reset-Paar wurde mitgerechnet")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_speicher_fehlt(tmp):
|
||||||
|
code, aus, _ = fahre(tmp / "gibt-es-nicht")
|
||||||
|
return [] if code == 2 and "Speicher fehlt" in aus else [f"Exit {code}, Ausgabe {aus!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_null_ablesungen(tmp):
|
||||||
|
baue(tmp, {"wf_n": [opus(1, "2026-09-24T10:00:00Z", "2026-09-24T10:10:00Z")]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "0 Ablesungen, 0 Paare" in aus else [f"Exit {code}, Ausgabe {aus!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_kaputte_zeile(tmp):
|
||||||
|
agenten = [dict(GRUND["wf_a"][0], kaputt=True), GRUND["wf_a"][1], GRUND["wf_a"][2]]
|
||||||
|
baue(tmp, {"wf_k": agenten})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0 or "1 Paare" not in aus:
|
||||||
|
fehler.append("Auswertung trotz kaputter Zeile gescheitert: " + aus[-300:])
|
||||||
|
if "ist kein JSON" not in aus:
|
||||||
|
fehler.append("kaputte Zeile nicht als Hinweis gemeldet")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_ohne_zeitstempel(tmp, position):
|
||||||
|
agenten = [dict(a) for a in GRUND["wf_a"]]
|
||||||
|
agenten[position]["ohne_zeit"] = True
|
||||||
|
baue(tmp, {"wf_z": agenten})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0 or "1 Ablesungen, 0 Paare" not in aus:
|
||||||
|
fehler.append(f"Exit {code}, Ausgabe {aus[-300:]!r}")
|
||||||
|
if "ohne Zeitstempel" not in aus:
|
||||||
|
fehler.append("fehlender Zeitstempel nicht gemeldet")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_fenster_kaputt(tmp, liste, erwartet="| unbekannt | unbekannt |"):
|
||||||
|
baue(tmp, {"wf_f": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, liste=liste), opus(1, "2026-09-24T10:01:00Z", "2026-09-24T10:20:00Z"),
|
||||||
|
ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 6)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and erwartet in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_umlaut_im_anlass(tmp):
|
||||||
|
baue(tmp, {"wf_u": [ablesung(1, "pruefung:vor Prüfung", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "pruefung:nach Prüfung", "2026-09-24T10:30:00Z", 12, 5)]})
|
||||||
|
code, aus, err = fahre(tmp)
|
||||||
|
return [] if code == 0 and "pruefung:vor Pruefung -> pruefung:nach Pruefung" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}, stderr {err[-200:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_seit(tmp):
|
||||||
|
baue(tmp, {"wf_s": [ablesung(1, "alt", "2026-09-20T10:00:00Z", 10, 5), ablesung(2, "a", "2026-09-24T10:00:00Z", 10, 5),
|
||||||
|
ablesung(3, "b", "2026-09-24T11:00:00Z", 12, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp, "--seit", "2026-09-24")
|
||||||
|
return [] if code == 0 and "2 Ablesungen, 1 Paare" in aus and "alt ->" not in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_seit_ungueltig(tmp):
|
||||||
|
baue(tmp, GRUND)
|
||||||
|
code, aus, _ = fahre(tmp, "--seit", "gestern")
|
||||||
|
return [] if code == 2 and "kein Datum" in aus else [f"Exit {code}, Ausgabe {aus!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_voller_modellname(tmp):
|
||||||
|
baue(tmp, {"wf_m": [GRUND["wf_a"][0], opus(1, "2026-09-24T10:00:10Z", "2026-09-24T10:30:10Z", modell="claude-opus-5-5"), GRUND["wf_a"][2]]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "opus 30.0" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_agent_ausserhalb(tmp):
|
||||||
|
# Ein Agent, der vor der ersten Ablesung endet, zaehlt nicht mit; einer, der halb hineinreicht, anteilig.
|
||||||
|
baue(tmp, {"wf_x": [opus(1, "2026-09-24T09:00:00Z", "2026-09-24T09:50:00Z"), GRUND["wf_a"][0],
|
||||||
|
opus(2, "2026-09-24T09:55:00Z", "2026-09-24T10:10:00Z"), GRUND["wf_a"][2]]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| opus 10.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-400:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_absturz_ist_befund(tmp):
|
||||||
|
# Ein Zeitstempel ohne Zeitzone neben einem mit ihr laesst den Vergleich mit TypeError scheitern.
|
||||||
|
# Der Lauf muss das als Hinweis melden und die anderen Laeufe trotzdem auswerten.
|
||||||
|
kaputt = [dict(GRUND["wf_a"][0]), dict(GRUND["wf_a"][1], start="2026-09-24T10:00:10"), dict(GRUND["wf_a"][2])]
|
||||||
|
baue(tmp, {"wf_a": GRUND["wf_a"], "wf_b": kaputt})
|
||||||
|
code, aus, err = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0:
|
||||||
|
fehler.append(f"Exit {code}: {err[-300:]}")
|
||||||
|
if "wf_b: Auswertung abgebrochen (TypeError" not in aus:
|
||||||
|
fehler.append("Absturz nicht als Hinweis gemeldet")
|
||||||
|
if "| wf_a |" not in aus:
|
||||||
|
fehler.append("der gesunde Lauf fehlt")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_fallender_wert(tmp):
|
||||||
|
baue(tmp, {"wf_d": [ablesung(1, "a", "2026-09-24T10:00:00Z", 30, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 5, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| Reset | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_reset_unlesbar(tmp):
|
||||||
|
baue(tmp, {"wf_u": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, reset5="bald"), ablesung(2, "b", "2026-09-24T10:20:00Z", 12, 6)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| unbekannt | 1.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_hoechstes_fenster(tmp):
|
||||||
|
liste = [{"label": "5-hour limit", "percentUsed": 10, "resetsAt": "2026-09-24T18:30:00Z"},
|
||||||
|
{"label": "5-hour limit", "percentUsed": 30, "resetsAt": "2026-09-24T18:30:00Z"},
|
||||||
|
{"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}]
|
||||||
|
baue(tmp, {"wf_h": [ablesung(1, "a", "2026-09-24T10:00:00Z", 0, 0, liste=liste), ablesung(2, "b", "2026-09-24T10:20:00Z", 35, 6)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| 5.0 | 1.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_ablesezeit_ist_ende(tmp):
|
||||||
|
# Der Opus-Agent laeuft ganz innerhalb des ersten Ableseagenten, also vor der Ablesung.
|
||||||
|
baue(tmp, {"wf_t": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, bis="2026-09-24T10:00:10Z"),
|
||||||
|
opus(1, "2026-09-24T10:00:02Z", "2026-09-24T10:00:08Z"),
|
||||||
|
ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| 29.8 | - | 2.0 | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_agent_ueber_das_paar_hinaus(tmp):
|
||||||
|
baue(tmp, {"wf_b": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 5),
|
||||||
|
opus(1, "2026-09-24T10:20:00Z", "2026-09-24T10:50:00Z"), ablesung(3, "c", "2026-09-24T11:00:00Z", 15, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0 or "| a -> b | 30.0 | opus 10.0 |" not in aus:
|
||||||
|
fehler.append("erstes Paar nicht am Ende gekappt: " + aus[-400:])
|
||||||
|
if "| b -> c | 30.0 | opus 20.0 |" not in aus:
|
||||||
|
fehler.append("zweites Paar falsch: " + aus[-400:])
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_ueber_hundert(tmp):
|
||||||
|
baue(tmp, {"wf_g": [ablesung(1, "a", "2026-09-24T10:00:00Z", 98, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 103, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| 5.0 | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
FAELLE = [
|
||||||
|
("Grundlinie: zwei Ablesungen, ein Opus-Agent dazwischen", f_grundlinie),
|
||||||
|
("Reset zwischen zwei Ablesungen", f_reset),
|
||||||
|
("Speicher fehlt: Exit 2", f_speicher_fehlt),
|
||||||
|
("null Ablesungen: Exit 0 mit Zaehlung", f_null_ablesungen),
|
||||||
|
("kaputte Journalzeile: Hinweis, kein Absturz", f_kaputte_zeile),
|
||||||
|
("erste Ablesung ohne Zeitstempel", lambda t: f_ohne_zeitstempel(t, 0)),
|
||||||
|
("letzte Ablesung ohne Zeitstempel", lambda t: f_ohne_zeitstempel(t, 2)),
|
||||||
|
("5-Stunden-Fenster fehlt", lambda t: f_fenster_kaputt(t, [{"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}], "| unbekannt | 1.0 |")),
|
||||||
|
("Umlaut im Anlass auf cp1252", f_umlaut_im_anlass),
|
||||||
|
("Prozent als Text", lambda t: f_fenster_kaputt(t, [{"label": "5-hour limit", "percentUsed": "10", "resetsAt": "2026-09-24T18:30:00Z"}, {"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}])),
|
||||||
|
("Prozent als Wahrheitswert", lambda t: f_fenster_kaputt(t, [{"label": "5-hour limit", "percentUsed": True, "resetsAt": "2026-09-24T18:30:00Z"}, {"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}])),
|
||||||
|
("fenster kein Array", lambda t: f_fenster_kaputt(t, {"a": 1})),
|
||||||
|
("--seit filtert aeltere Ablesungen", f_seit),
|
||||||
|
("--seit ungueltig: Exit 2", f_seit_ungueltig),
|
||||||
|
("voller Modellname zaehlt als opus", f_voller_modellname),
|
||||||
|
("Agent ausserhalb und halb innerhalb des Paars", f_agent_ausserhalb),
|
||||||
|
("Absturz in einem Lauf ist ein Hinweis", f_absturz_ist_befund),
|
||||||
|
("fallender Wert im selben Fenster ist ein Reset", f_fallender_wert),
|
||||||
|
("resetsAt ohne Datum: unbekannt", f_reset_unlesbar),
|
||||||
|
("doppeltes 5-Stunden-Fenster: das hoechste zaehlt", f_hoechstes_fenster),
|
||||||
|
("Ablesezeit ist das Ende des Ableseagenten", f_ablesezeit_ist_ende),
|
||||||
|
("Agent ueber das Paar hinaus wird am Ende gekappt", f_agent_ueber_das_paar_hinaus),
|
||||||
|
("ueber 100 Prozent ist ein Wert, kein Fehler", f_ueber_hundert),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Fehlertexte koennen Zeichen ausserhalb der Konsolenkodierung tragen; der Treiber darf daran nicht sterben.
|
||||||
|
sys.stdout.reconfigure(errors="replace")
|
||||||
|
gescheitert = 0
|
||||||
|
for name, fall in FAELLE:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
try:
|
||||||
|
fehler = fall(Path(tmp))
|
||||||
|
except Exception as e: # ein Absturz im Fall ist selbst ein Befund
|
||||||
|
fehler = [f"Absturz im Fall: {type(e).__name__}: {e}"]
|
||||||
|
if fehler:
|
||||||
|
gescheitert += 1
|
||||||
|
print(f"FEHLER {name}\n " + "\n ".join(fehler))
|
||||||
|
else:
|
||||||
|
print(f"OK {name}")
|
||||||
|
print(f"{len(FAELLE)} Faelle, {gescheitert} gescheitert")
|
||||||
|
return 1 if gescheitert else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/mutanten_reihe.py`
|
||||||
|
|
||||||
|
Die ganze Datei ist neu:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Mutantenlauf gegen verbrauch_reihe.py: je Mutant genau eine Ersetzung in einer Kopie des Ordners, dann faelle_verbrauch_reihe.py.
|
||||||
|
|
||||||
|
Jede Ersetzung muss genau einmal vorkommen, sonst bricht der Lauf mit Exit 2 ab (veralteter Mutant).
|
||||||
|
Aufruf: python mutanten_reihe.py
|
||||||
|
Exit 0, wenn jeder Mutant gefangen wird, 1 bei einem Ueberlebenden, 2 bei einem veralteten Muster.
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HIER = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
MUTANTEN = [
|
||||||
|
("Reset nur bei gleichem Text", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if a[1] != b[1] or b[0] < a[0]:"),
|
||||||
|
("fallender Wert kein Reset", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if abs(rb - ra) > RESET_TOLERANZ:"),
|
||||||
|
("unlesbares resetsAt als gleich", " if ra is None or rb is None or (ra.tzinfo is None) != (rb.tzinfo is None):\n return \"unbekannt\"\n", ""),
|
||||||
|
("bool als Zahl", "isinstance(p, bool) or ", ""),
|
||||||
|
("ueber 100 als Fehler", "not math.isfinite(p) or p < 0", "not math.isfinite(p) or not 0 <= p <= 100"),
|
||||||
|
("erstes statt hoechstes 5-Stunden-Fenster", "if art == \"fuenf\" and (fuenf is None or p > fuenf[0]):", "if art == \"fuenf\" and fuenf is None:"),
|
||||||
|
("Ablesezeit Start statt Ende", "\"zeit\": spanne[1]", "\"zeit\": spanne[0]"),
|
||||||
|
("nur_ascii fehlt", "print(nur_ascii(f\"| {z", "print((f\"| {z"),
|
||||||
|
("Ende des Paars nicht gekappt", "min(g[\"ende\"], b[\"zeit\"])", "g[\"ende\"]"),
|
||||||
|
("Anfang des Paars nicht gekappt", "max(g[\"start\"], a[\"zeit\"])", "g[\"start\"]"),
|
||||||
|
("Absturz nicht gefangen", "except Exception as e: # Ein Absturz", "except ZeroDivisionError as e: # Ein Absturz"),
|
||||||
|
("kaputte Zeile bricht ab", " except ValueError:\n befunde.append(f\"{pfad.parent.name}/{pfad.name}: Zeile", " except KeyError:\n befunde.append(f\"{pfad.parent.name}/{pfad.name}: Zeile"),
|
||||||
|
("seit ignoriert", "ablesungen = [x for x in ablesungen if x[\"zeit\"] >= seit]", "pass"),
|
||||||
|
("Speicher fehlt still", " return 2\n seit = None", " return 0\n seit = None"),
|
||||||
|
("Familie aus vollem Namen fehlt", " if f in modell.lower():", " if f == modell.lower():"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ueberlebt, veraltet = [], []
|
||||||
|
for name, alt, neu in MUTANTEN:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
ziel = Path(tmp) / "w"
|
||||||
|
ziel.mkdir()
|
||||||
|
for datei in ("verbrauch_reihe.py", "faelle_verbrauch_reihe.py"):
|
||||||
|
shutil.copy2(HIER / datei, ziel / datei)
|
||||||
|
pfad = ziel / "verbrauch_reihe.py"
|
||||||
|
text = pfad.read_text(encoding="utf-8")
|
||||||
|
if text.count(alt) != 1:
|
||||||
|
veraltet.append(name)
|
||||||
|
print(f"VERALTET {name} (Muster {text.count(alt)}-mal)")
|
||||||
|
continue
|
||||||
|
pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
|
||||||
|
lauf = subprocess.run([sys.executable, str(ziel / "faelle_verbrauch_reihe.py")], capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||||
|
rot = [z for z in lauf.stdout.splitlines() if z.startswith("FEHLER")]
|
||||||
|
if lauf.returncode == 0:
|
||||||
|
ueberlebt.append(name)
|
||||||
|
print(f"UEBERLEBT {name}")
|
||||||
|
else:
|
||||||
|
print(f"GEFANGEN {name} ({rot[0][7:80] if rot else 'Absturz'})")
|
||||||
|
print(f"{len(MUTANTEN)} Mutanten, {len(ueberlebt)} ueberlebt, {len(veraltet)} veraltet")
|
||||||
|
return 2 if veraltet else 1 if ueberlebt else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.stdout.reconfigure(errors="replace")
|
||||||
|
sys.exit(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Im Vault-Wurzelordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Alle übrigen Zeilen beginnen mit `OK`, die letzte enthält `0 gescheitert`, Exit 0.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .vault-tools/workflow-pruefung/mutanten_reihe.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Die letzte Zeile enthält `0 ueberlebt, 0 veraltet`, Exit 0.
|
||||||
|
|
||||||
|
Danach `git status --porcelain -- .vault-tools/workflow-pruefung/verbrauch_reihe.py .vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py .vault-tools/workflow-pruefung/mutanten_reihe.py` zeigen.
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 08 — Nachbesserung verbrauch.js: über 100 Prozent bremst"
|
||||||
|
status: draft
|
||||||
|
summary: "Delta nach Welle 0: Ein Fenster über 100 Prozent ist ein gültiger Wert und bremst, statt als unbekanntes Fenster auf zwei Plätze zurückzufallen. Befund der Plankritik vom 2026-09-24."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
modell: haiku
|
||||||
|
modell_begruendung: "Abschrift: zwei Aus-wird-Blöcke stehen wörtlich im Plan, Zieldatei und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche, Auslassungspunkte oder das Zeichen ·."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 08 — Nachbesserung verbrauch.js: über 100 Prozent bremst
|
||||||
|
|
||||||
|
Die globalen Vorgaben stehen in `00-uebersicht.md` (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner `D:/Claude`; alle Pfade gelten von dort. Task 01 ist in Welle 0 ausgeführt und bleibt byte-gleich; diese Nachbesserung ändert die Datei nur an zwei Stellen.
|
||||||
|
|
||||||
|
Was danach anders ist:
|
||||||
|
|
||||||
|
- **Über 100 % bremst.** Bisher war ein Fenster über 100 % ein unbekanntes Fenster mit Rückfall auf zwei Plätze, die Bremse lief also nicht monoton: bei 84 % ein Platz, bei 85 bis 100 % keiner, bei 101 % wieder zwei. Jetzt ist nur ein negativer oder nicht endlicher Wert ein Befund.
|
||||||
|
|
||||||
|
## Zieldateien
|
||||||
|
|
||||||
|
- `.claude/workflows/verbrauch.js` (geändert)
|
||||||
|
|
||||||
|
## Verbote
|
||||||
|
|
||||||
|
- Keine andere Datei, auch nicht die übrigen Workflows unter `.claude/workflows/`.
|
||||||
|
- Kein Commit, kein Push, kein Lauf eines echten Workflows.
|
||||||
|
- Kein Umformulieren; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
### `.claude/workflows/verbrauch.js`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Ordnet die Fenster ein. Ein Fenster, das als 5 Stunden oder Woche erkannt wird, aber keine
|
||||||
|
// Prozentzahl von 0 bis 100 trägt, ist ein Befund und wird nicht still übergangen.
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 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.
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.claude/workflows/verbrauch.js`, Stelle 2
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (typeof p !== 'number' || !Number.isFinite(p) || p < 0 || p > 100) return { fehler: `${f.label}: percentUsed ${JSON.stringify(p)} ist keine Zahl von 0 bis 100` }
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (typeof p !== 'number' || !Number.isFinite(p) || p < 0) return { fehler: `${f.label}: percentUsed ${JSON.stringify(p)} ist keine Zahl ab 0` }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Im Vault-Wurzelordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "const t=require('fs').readFileSync(process.argv[1],'utf8').replace(/^export const meta = /m,'const meta = ');new (Object.getPrototypeOf(async function(){}).constructor)('agent','parallel','pipeline','phase','log','workflow','args','budget',t);console.log('PARSE OK')" .claude/workflows/verbrauch.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `PARSE OK`, Exit 0.
|
||||||
|
|
||||||
|
Danach `git status --porcelain -- .claude/workflows/verbrauch.js` zeigen.
|
||||||
+438
@@ -0,0 +1,438 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 09 — Nachbesserung nach der Prüfung: Fälle für die Reset-Toleranz und kleine Lücken"
|
||||||
|
status: draft
|
||||||
|
summary: "Delta nach Welle 1 und 2 aus der Prüfung wf_e0294c82-76c: Regressionsfälle und Mutanten für die Reset-Toleranz, doppelte Linsen abweisen, Status ohne_pruefung, Dateiname mit Pfadgrenze, Kindphase mit Nummer, Fälle für den Nachlesetakt, den Lesehinweis, fehlendes verbrauch.js und das verankerte 5-Stunden-Label."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-09-24
|
||||||
|
updated: 2026-09-24
|
||||||
|
modell: sonnet
|
||||||
|
effort: low
|
||||||
|
modell_begruendung: "Abschrift: alle Aus-wird-Blöcke stehen wörtlich im Plan, Zieldateien und Verbote sind genannt. Sonnet low statt Haiku, weil Blöcke die Zeichen · und ▸ tragen und Haiku in der Messung vom 2026-09-23 typografische Zeichen still ersetzt hat."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 09 — Nachbesserung nach der Prüfung
|
||||||
|
|
||||||
|
Die globalen Vorgaben stehen in `00-uebersicht.md` (Abschnitt „Globale Vorgaben“). Arbeitsverzeichnis ist der Vault-Wurzelordner `D:/Claude`; alle Pfade gelten von dort. Die Tasks 02 bis 07 sind ausgeführt und bleiben byte-gleich; diese Nachbesserung ändert die Dateien nur an den Stellen unten. Jeder Aus-Block steht genau einmal in seiner Datei.
|
||||||
|
|
||||||
|
Was danach anders ist:
|
||||||
|
|
||||||
|
- **Die Reset-Toleranz hat Fälle für ihren Wert:** Verschiebung um 4:59 (dasselbe Fenster), um 5:01 nach vorn und nach hinten (Reset), um fünf Stunden bei steigendem Wert (Reset). Die Mutanten 6 Stunden, 1 Sekunde und „nur nach vorn“ werden gefangen.
|
||||||
|
- **`plankritik.js`** weist doppelte Linsen ab. **`welle-mit-pruefung.js`** meldet `status: ohne_pruefung`, wenn die Prüfung übersprungen wurde oder scheiterte. **`planung.js`** vergleicht den Dateinamen des Planer-Ergebnisses mit Pfadgrenze.
|
||||||
|
- **Die Attrappe** nummeriert wiederholte Kinder wie das Ziel (`▸ name #2`).
|
||||||
|
- **Neue Fälle** für den Nachlesetakt, den Lesehinweis im Planerauftrag, ein fehlendes `verbrauch.js` in `welle-mit-pruefung` und das verankerte 5-Stunden-Label, dazu die Mutanten dafür.
|
||||||
|
|
||||||
|
## Zieldateien
|
||||||
|
|
||||||
|
- `.claude/workflows/plankritik.js` (geändert)
|
||||||
|
- `.claude/workflows/planung.js` (geändert)
|
||||||
|
- `.claude/workflows/welle-mit-pruefung.js` (geändert)
|
||||||
|
- `.vault-tools/workflow-pruefung/attrappe.mjs` (geändert)
|
||||||
|
- `.vault-tools/workflow-pruefung/faelle_workflows.mjs` (geändert)
|
||||||
|
- `.vault-tools/workflow-pruefung/mutanten_workflows.py` (geändert)
|
||||||
|
- `.vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py` (geändert)
|
||||||
|
- `.vault-tools/workflow-pruefung/mutanten_reihe.py` (geändert)
|
||||||
|
|
||||||
|
## Verbote
|
||||||
|
|
||||||
|
- Keine andere Datei.
|
||||||
|
- Kein Commit, kein Push, kein Lauf eines echten Workflows.
|
||||||
|
- Kein Umformulieren; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
### `.claude/workflows/plankritik.js`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (unbekannt.length) throw new Error('plankritik: unbekannte Linse: ' + unbekannt.join(', ') + '; bekannt: ' + Object.keys(LINSEN).join(', '))
|
||||||
|
if (a.effort !== undefined && !EFFORTS.includes(a.effort)) throw new Error('plankritik: effort "' + a.effort + '" ungültig')
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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')
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.claude/workflows/planung.js`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
laufend.delete(nr)
|
||||||
|
const passt = roh && typeof roh.datei === 'string' && roh.datei.replace(/\\/g, '/').endsWith(jeNr.get(nr).datei)
|
||||||
|
const r = passt ? roh : null
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
laufend.delete(nr)
|
||||||
|
const passt = roh && typeof roh.datei === 'string' && ('/' + roh.datei.replace(/\\/g, '/')).endsWith('/' + jeNr.get(nr).datei)
|
||||||
|
const r = passt ? roh : null
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.claude/workflows/welle-mit-pruefung.js`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
return {
|
||||||
|
status: 'fertig',
|
||||||
|
kopie,
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
return {
|
||||||
|
status: pruefung ? 'fertig' : 'ohne_pruefung',
|
||||||
|
kopie,
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/attrappe.mjs`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// "nesting is limited to one level"; die Attrappe setzt einen Doppelpunkt statt des Strichs.
|
||||||
|
// - Im Kind ist phase() wirkungslos, und jeder agent() bekommt die Phase "▸ <meta.name>".
|
||||||
|
// - workflow() mit unbekanntem Namen oder unlesbarem scriptPath wirft.
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// "nesting is limited to one level"; die Attrappe setzt einen Doppelpunkt statt des Strichs.
|
||||||
|
// - Im Kind ist phase() wirkungslos, und jeder agent() bekommt die Phase "▸ <meta.name>", ab dem
|
||||||
|
// zweiten Aufruf desselben Kindes "▸ <meta.name> #<n>" (Bundle 2.1.280 und meta.json eines
|
||||||
|
// echten Laufs mit drei Ablesungen: "▸ verbrauch", "▸ verbrauch #2", "▸ verbrauch #3").
|
||||||
|
// - workflow() mit unbekanntem Namen oder unlesbarem scriptPath wirft.
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/attrappe.mjs`, Stelle 2
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const laufendJePhase = {}
|
||||||
|
let uhr = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const laufendJePhase = {}
|
||||||
|
const kindAufrufe = {}
|
||||||
|
let uhr = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/attrappe.mjs`, Stelle 3
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { name, fn } = lade(datei)
|
||||||
|
const kindPhase = tiefe >= 1 ? '▸ ' + name : null
|
||||||
|
let aktuellePhase = null
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { name, fn } = lade(datei)
|
||||||
|
let kindPhase = null
|
||||||
|
if (tiefe >= 1) {
|
||||||
|
kindAufrufe[name] = (kindAufrufe[name] || 0) + 1
|
||||||
|
kindPhase = '▸ ' + name + (kindAufrufe[name] > 1 ? ' #' + kindAufrufe[name] : '')
|
||||||
|
}
|
||||||
|
let aktuellePhase = null
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Exit 0, wenn alle Fälle bestehen, sonst 1.
|
||||||
|
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Exit 0, wenn alle Fälle bestehen, sonst 1.
|
||||||
|
import { mkdtempSync, writeFileSync, rmSync, cpSync } from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, Stelle 2
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
soll(JSON.stringify(p.phasen) === '["Oben"]', 'phase() im Kind hat gewirkt: ' + JSON.stringify(p.phasen))
|
||||||
|
return fehler
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
soll(JSON.stringify(p.phasen) === '["Oben"]', 'phase() im Kind hat gewirkt: ' + JSON.stringify(p.phasen))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'dasselbe Kind zweimal: ▸ name und ▸ name #2', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const kind = skript('kindzwei', "return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet' })")
|
||||||
|
const { protokoll: p } = await A(skript('elternzwei', `await workflow({ scriptPath: ${JSON.stringify(kind)} }, {}); return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
|
||||||
|
soll(JSON.stringify(p.agents.map(a => a.phase)) === '["▸ kindzwei","▸ kindzwei #2"]', 'Phasen: ' + JSON.stringify(p.agents.map(a => a.phase)))
|
||||||
|
return fehler
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, Stelle 3
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
fall('verbrauch', 'Woche aller Modelle 90 % zählt auch neben einer niedrigen Opus-Woche', async () => {
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
fall('verbrauch', 'ein Label mit "hour" in der Mitte ist kein 5-Stunden-Fenster', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Opus hours', percentUsed: 90, resetsAt: 'z' }]) })
|
||||||
|
soll(e && e.plaetze === 1 && e.fuenf_stunden.prozent === 10, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('verbrauch', 'Woche aller Modelle 90 % zählt auch neben einer niedrigen Opus-Woche', async () => {
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, Stelle 4
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
fall('plankritik', 'Linse ohne Ergebnis steht in ohne_ergebnis', async () => {
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
fall('plankritik', 'doppelte Linse wirft vor dem ersten Agenten', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, linsen: ['arithmetik', 'bezeichner', 'arithmetik'] }, {})
|
||||||
|
soll(f && /doppelt in linsen/.test(f), 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'Linse ohne Ergebnis steht in ohne_ergebnis', async () => {
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, Stelle 5
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { ergebnis: e, protokoll: p } = await WM(WM_ARGS, { executer: OHNE })
|
||||||
|
soll(e.status === 'fertig' && /unvollständig/.test(e.pruefung.uebersprungen), JSON.stringify(e.pruefung))
|
||||||
|
soll(mitLabel(p, 'verbrauch:').length === 0 && mitLabel(p, 'pruefung').length === 0, 'Ablesung oder Prüfung trotz Lücke')
|
||||||
|
return fehler
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { ergebnis: e, protokoll: p } = await WM(WM_ARGS, { executer: OHNE })
|
||||||
|
soll(e.status === 'ohne_pruefung' && /unvollständig/.test(e.pruefung.uebersprungen), JSON.stringify([e.status, e.pruefung]))
|
||||||
|
soll(mitLabel(p, 'verbrauch:').length === 0 && mitLabel(p, 'pruefung').length === 0, 'Ablesung oder Prüfung trotz Lücke')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('welle-mit-pruefung', 'verbrauch.js fehlt: Ablesungen nicht_gelesen, Welle und Prüfung bleiben', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const ohneVerbrauch = mkdtempSync(path.join(os.tmpdir(), 'ohne-verbrauch-'))
|
||||||
|
cpSync(ORDNER, ohneVerbrauch, { recursive: true })
|
||||||
|
rmSync(path.join(ohneVerbrauch, 'verbrauch.js'))
|
||||||
|
const { ergebnis: e, fehler: f } = await fuehreAus(path.join(ohneVerbrauch, 'welle-mit-pruefung.js'), WM_ARGS, antworten({}), { ordner: ohneVerbrauch })
|
||||||
|
rmSync(ohneVerbrauch, { recursive: true, force: true })
|
||||||
|
soll(f === null && e.status === 'fertig' && e.welle && e.pruefung.zaehlung, 'Fehler/Ergebnis: ' + f + ' ' + JSON.stringify(e && e.status))
|
||||||
|
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch))
|
||||||
|
return fehler
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, Stelle 6
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
fall('planung', '5 Stunden bei 60 %: höchstens zwei gleichzeitig', async () => {
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
fall('planung', 'Ergebnis mit ähnlichem Dateinamen gilt als gescheitert', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await PL(PL_ARGS, { planerDatei: (nr, richtig) => (nr === 7 ? 'D:/x/plan/xtask-07-t.md' : richtig) })
|
||||||
|
soll(JSON.stringify(e.gescheitert) === '[7]', JSON.stringify(e.gescheitert))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Nachlesen im Takt von nachlesen_alle, solange Tasks offen sind', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 2, nachlesen_alle: 2 }, { planerDauer: () => 100 })
|
||||||
|
const ab = mitLabel(p, 'verbrauch:').map(x => x.label.slice('verbrauch:planung:'.length))
|
||||||
|
soll(JSON.stringify(ab) === '["Anfang","nach 2 Tasks","nach 4 Tasks","nach 6 Tasks","Ende"]', 'Ablesungen: ' + JSON.stringify(ab))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', '5 Stunden bei 60 %: höchstens zwei gleichzeitig', async () => {
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, Stelle 7
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
soll(!/\{(NR|THEMA|DATEI|MODELL|WELLE|VORAUSSETZUNGEN)\}/.test(t5), 'Platzhalter übrig')
|
||||||
|
soll(/vor: \(keine\)/.test(planerVon(p, 1).prompt), 'Task 1 ohne "(keine)"')
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
soll(!/\{(NR|THEMA|DATEI|MODELL|WELLE|VORAUSSETZUNGEN)\}/.test(t5), 'Platzhalter übrig')
|
||||||
|
soll(/Lies sie, bevor du einen Namen, eine Signatur oder einen Import daraus übernimmst/.test(t5), 'Lesehinweis fehlt')
|
||||||
|
soll(!/Lies sie/.test(planerVon(p, 1).prompt), 'Lesehinweis ohne Voraussetzungen')
|
||||||
|
soll(/vor: \(keine\)/.test(planerVon(p, 1).prompt), 'Task 1 ohne "(keine)"')
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/mutanten_workflows.py`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```python
|
||||||
|
("planung", "Rueckfall ohne Ablesung 3", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```python
|
||||||
|
("planung", "Rueckfall ohne Ablesung 3", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"),
|
||||||
|
("planung", "Datei ohne Pfadgrenze", "('/' + roh.datei.replace(/\\\\/g, '/')).endsWith('/' + jeNr.get(nr).datei)", "roh.datei.replace(/\\\\/g, '/').endsWith(jeNr.get(nr).datei)"),
|
||||||
|
("planung", "Nachlesezaehler nicht zurueckgesetzt", " fertigSeitAblesung = 0\n const v = await ablesen(", " const v = await ablesen("),
|
||||||
|
("planung", "nachlesen_alle ignoriert", "const nachlesenAlle = args.nachlesen_alle || NACHLESEN_ALLE", "const nachlesenAlle = NACHLESEN_ALLE"),
|
||||||
|
("planung", "Lesehinweis fehlt", " Lies sie, bevor du einen Namen, eine Signatur oder einen Import daraus übernimmst.", ""),
|
||||||
|
("plankritik", "doppelte Linse erlaubt", " if (a.linsen && new Set(a.linsen).size !== a.linsen.length) throw", " if (false) throw"),
|
||||||
|
("welle-mit-pruefung", "status fertig ohne Pruefung", "status: pruefung ? 'fertig' : 'ohne_pruefung',", "status: 'fertig',"),
|
||||||
|
("welle-mit-pruefung", "Ablesung ohne try", " try {\n r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })\n } catch (e) {\n r = { status: 'nicht_gelesen', grund: 'verbrauch.js nicht ausführbar: ' + (e && e.message) }\n }", " r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })"),
|
||||||
|
("verbrauch", "5-Stunden-Regex unverankert", "const FUENF_STUNDEN = /^\\s*5[- ]?hour/i", "const FUENF_STUNDEN = /hour/i"),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
def f_fallender_wert(tmp):
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
def f_verschiebung(tmp, reset5, erwartet):
|
||||||
|
# Nur resetsAt verschiebt sich, der Wert steigt; entscheidend ist allein die Toleranz.
|
||||||
|
baue(tmp, {"wf_v": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 12, 5, reset5=reset5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and erwartet in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_fallender_wert(tmp):
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py`, Stelle 2
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```python
|
||||||
|
("Absturz in einem Lauf ist ein Hinweis", f_absturz_ist_befund),
|
||||||
|
("fallender Wert im selben Fenster ist ein Reset", f_fallender_wert),
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```python
|
||||||
|
("Absturz in einem Lauf ist ein Hinweis", f_absturz_ist_befund),
|
||||||
|
("resetsAt 5 Stunden weiter, Wert steigt: Reset", lambda t: f_verschiebung(t, "2026-09-24T23:30:00Z", "| Reset | 0.0 |")),
|
||||||
|
("resetsAt 4:59 verschoben: dasselbe Fenster", lambda t: f_verschiebung(t, "2026-09-24T18:34:59Z", "| 2.0 | 0.0 |")),
|
||||||
|
("resetsAt 5:01 verschoben: Reset", lambda t: f_verschiebung(t, "2026-09-24T18:35:01Z", "| Reset | 0.0 |")),
|
||||||
|
("resetsAt 5:01 frueher: Reset", lambda t: f_verschiebung(t, "2026-09-24T18:24:59Z", "| Reset | 0.0 |")),
|
||||||
|
("fallender Wert im selben Fenster ist ein Reset", f_fallender_wert),
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.vault-tools/workflow-pruefung/mutanten_reihe.py`, Stelle 1
|
||||||
|
|
||||||
|
Aus:
|
||||||
|
|
||||||
|
```python
|
||||||
|
("Reset nur bei gleichem Text", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if a[1] != b[1] or b[0] < a[0]:"),
|
||||||
|
("fallender Wert kein Reset", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if abs(rb - ra) > RESET_TOLERANZ:"),
|
||||||
|
```
|
||||||
|
|
||||||
|
wird:
|
||||||
|
|
||||||
|
```python
|
||||||
|
("Reset nur bei gleichem Text", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if a[1] != b[1] or b[0] < a[0]:"),
|
||||||
|
("Toleranz 6 Stunden", "RESET_TOLERANZ = timedelta(minutes=5)", "RESET_TOLERANZ = timedelta(hours=6)"),
|
||||||
|
("Toleranz 1 Sekunde", "RESET_TOLERANZ = timedelta(minutes=5)", "RESET_TOLERANZ = timedelta(seconds=1)"),
|
||||||
|
("Toleranz nur nach vorn", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if rb - ra > RESET_TOLERANZ or b[0] < a[0]:"),
|
||||||
|
("fallender Wert kein Reset", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if abs(rb - ra) > RESET_TOLERANZ:"),
|
||||||
|
```
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Im Vault-Wurzelordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .vault-tools/workflow-pruefung/faelle_workflows.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Alle übrigen Zeilen beginnen mit `OK`, die letzte enthält `0 gescheitert`, Exit 0.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Die letzte Zeile enthält `0 gescheitert`, Exit 0.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .vault-tools/workflow-pruefung/mutanten_reihe.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Die letzte Zeile enthält `0 ueberlebt, 0 veraltet`, Exit 0.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .vault-tools/workflow-pruefung/mutanten_workflows.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Die letzte Zeile enthält `0 ueberlebt, 0 veraltet`, Exit 0. Der Lauf dauert einige Minuten.
|
||||||
|
|
||||||
|
Danach `git status --porcelain -- .claude/workflows/plankritik.js .claude/workflows/planung.js .claude/workflows/welle-mit-pruefung.js .vault-tools/workflow-pruefung/attrappe.mjs .vault-tools/workflow-pruefung/faelle_workflows.mjs .vault-tools/workflow-pruefung/mutanten_workflows.py .vault-tools/workflow-pruefung/faelle_verbrauch_reihe.py .vault-tools/workflow-pruefung/mutanten_reihe.py` zeigen.
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
"""Faelle fuer verbrauch_reihe.py: baut je Fall einen kuenstlichen Session-Speicher und faehrt das Skript als eigenen Prozess.
|
||||||
|
|
||||||
|
Die Grundlinie laeuft zuerst; scheitert sie, messen die uebrigen Faelle nichts. Der Prozess laeuft
|
||||||
|
ohne PYTHONIOENCODING, damit eine Ausgabe ausserhalb von ASCII auf cp1252 auffaellt.
|
||||||
|
Aufruf: python faelle_verbrauch_reihe.py
|
||||||
|
Exit 0, wenn alle Faelle bestehen, sonst 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SKRIPT = Path(__file__).with_name("verbrauch_reihe.py")
|
||||||
|
|
||||||
|
|
||||||
|
def fenster(fuenf, woche, reset5="2026-09-24T18:30:00Z", resetw="2026-09-28T00:00:00Z"):
|
||||||
|
return [
|
||||||
|
{"label": "5-hour limit", "percentUsed": fuenf, "resetsAt": reset5},
|
||||||
|
{"label": "Weekly · all models", "percentUsed": woche, "resetsAt": resetw},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def baue(speicher, laeufe):
|
||||||
|
"""laeufe: {lauf: [agent]}; agent: {id, label, start, ende, modell, ergebnis, ohne_zeit, kaputt}."""
|
||||||
|
for lauf, agenten in laeufe.items():
|
||||||
|
ordner = speicher / "sitzung-1" / "subagents" / "workflows" / lauf
|
||||||
|
ordner.mkdir(parents=True)
|
||||||
|
with (ordner / "journal.jsonl").open("w", encoding="utf-8") as journal:
|
||||||
|
journal.write(json.dumps({"type": "launched"}) + "\n")
|
||||||
|
for a in agenten:
|
||||||
|
journal.write(json.dumps({"type": "started", "agentId": a["id"], "label": a["label"]}) + "\n")
|
||||||
|
if a.get("kaputt"):
|
||||||
|
journal.write("{kein json\n")
|
||||||
|
if "ergebnis" in a:
|
||||||
|
journal.write(json.dumps({"type": "result", "agentId": a["id"], "result": a["ergebnis"]}) + "\n")
|
||||||
|
for a in agenten:
|
||||||
|
(ordner / f"agent-{a['id']}.meta.json").write_text(json.dumps({"model": a.get("modell", "haiku")}), encoding="utf-8")
|
||||||
|
if a.get("ohne_zeit"):
|
||||||
|
continue
|
||||||
|
with (ordner / f"agent-{a['id']}.jsonl").open("w", encoding="utf-8") as d:
|
||||||
|
d.write(json.dumps({"timestamp": a["start"], "type": "user"}) + "\n")
|
||||||
|
d.write(json.dumps({"timestamp": a["ende"], "type": "assistant"}) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def ablesung(nr, anlass, zeitpunkt, fuenf, woche, **extra):
|
||||||
|
ende = extra.pop("bis", None) or zeitpunkt
|
||||||
|
return {"id": f"v{nr}", "label": "verbrauch:" + anlass, "start": zeitpunkt, "ende": ende, "modell": "haiku",
|
||||||
|
"ergebnis": {"verfuegbar": True, "status": "ok", "plan": "Pro", "fenster": extra.pop("liste", None) or fenster(fuenf, woche, **extra), "fehler": ""}}
|
||||||
|
|
||||||
|
|
||||||
|
# resetsAt schwankt bei echten Ablesungen um Millisekunden und springt ueber Sekunden- und
|
||||||
|
# Datumsgrenzen (gemessen 2026-09-24); die Grundlinie traegt deshalb solche Werte.
|
||||||
|
ZITTER = {"reset5": "2026-09-24T18:29:59.664Z", "resetw": "2026-09-27T23:59:59.664Z"}
|
||||||
|
|
||||||
|
|
||||||
|
def opus(nr, start, ende, modell="opus"):
|
||||||
|
return {"id": f"o{nr}", "label": f"plankritik:linse{nr}", "start": start, "ende": ende, "modell": modell, "ergebnis": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def fahre(speicher, *zusatz):
|
||||||
|
umgebung = {k: v for k, v in os.environ.items() if k not in ("PYTHONIOENCODING", "PYTHONUTF8")}
|
||||||
|
lauf = subprocess.run([sys.executable, str(SKRIPT), "--speicher", str(speicher), *zusatz],
|
||||||
|
capture_output=True, env=umgebung)
|
||||||
|
return lauf.returncode, lauf.stdout.decode("ascii", "replace"), lauf.stderr.decode("utf-8", "replace")
|
||||||
|
|
||||||
|
|
||||||
|
GRUND = {
|
||||||
|
"wf_a": [
|
||||||
|
ablesung(1, "plankritik:vor Linsen", "2026-09-24T10:00:00Z", 10, 5),
|
||||||
|
opus(1, "2026-09-24T10:00:10Z", "2026-09-24T10:30:10Z"),
|
||||||
|
ablesung(2, "plankritik:Ende", "2026-09-24T10:30:10Z", 14, 6, **ZITTER),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def f_grundlinie(tmp):
|
||||||
|
baue(tmp, GRUND)
|
||||||
|
code, aus, err = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0:
|
||||||
|
fehler.append(f"Exit {code}: {err[-300:]}")
|
||||||
|
for erwartet in ["2 Ablesungen, 1 Paare", "| wf_a | plankritik:vor Linsen -> plankritik:Ende | 30.2 | opus 30.0 | 4.0 | 1.0 |",
|
||||||
|
"Naeherung: 8.0 % des 5-Stunden-Fensters je Opus-Agentenstunde (1 Paare, 0.5 Opus-Stunden"]:
|
||||||
|
if erwartet not in aus:
|
||||||
|
fehler.append("fehlt: " + erwartet)
|
||||||
|
if "Hinweise" in aus:
|
||||||
|
fehler.append("unerwartete Hinweise")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_reset(tmp):
|
||||||
|
baue(tmp, {"wf_r": [ablesung(1, "a", "2026-09-24T10:00:00Z", 80, 5), opus(1, "2026-09-24T10:01:00Z", "2026-09-24T10:20:00Z"),
|
||||||
|
ablesung(2, "b", "2026-09-24T10:30:00Z", 3, 6, reset5="2026-09-24T15:30:00Z")]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0 or "| Reset | 1.0 |" not in aus:
|
||||||
|
fehler.append("Reset nicht erkannt: " + aus[-300:])
|
||||||
|
if "Keine auswertbaren Paare mit Opus-Agenten." not in aus:
|
||||||
|
fehler.append("Reset-Paar wurde mitgerechnet")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_speicher_fehlt(tmp):
|
||||||
|
code, aus, _ = fahre(tmp / "gibt-es-nicht")
|
||||||
|
return [] if code == 2 and "Speicher fehlt" in aus else [f"Exit {code}, Ausgabe {aus!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_null_ablesungen(tmp):
|
||||||
|
baue(tmp, {"wf_n": [opus(1, "2026-09-24T10:00:00Z", "2026-09-24T10:10:00Z")]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "0 Ablesungen, 0 Paare" in aus else [f"Exit {code}, Ausgabe {aus!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_kaputte_zeile(tmp):
|
||||||
|
agenten = [dict(GRUND["wf_a"][0], kaputt=True), GRUND["wf_a"][1], GRUND["wf_a"][2]]
|
||||||
|
baue(tmp, {"wf_k": agenten})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0 or "1 Paare" not in aus:
|
||||||
|
fehler.append("Auswertung trotz kaputter Zeile gescheitert: " + aus[-300:])
|
||||||
|
if "ist kein JSON" not in aus:
|
||||||
|
fehler.append("kaputte Zeile nicht als Hinweis gemeldet")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_ohne_zeitstempel(tmp, position):
|
||||||
|
agenten = [dict(a) for a in GRUND["wf_a"]]
|
||||||
|
agenten[position]["ohne_zeit"] = True
|
||||||
|
baue(tmp, {"wf_z": agenten})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0 or "1 Ablesungen, 0 Paare" not in aus:
|
||||||
|
fehler.append(f"Exit {code}, Ausgabe {aus[-300:]!r}")
|
||||||
|
if "ohne Zeitstempel" not in aus:
|
||||||
|
fehler.append("fehlender Zeitstempel nicht gemeldet")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_fenster_kaputt(tmp, liste, erwartet="| unbekannt | unbekannt |"):
|
||||||
|
baue(tmp, {"wf_f": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, liste=liste), opus(1, "2026-09-24T10:01:00Z", "2026-09-24T10:20:00Z"),
|
||||||
|
ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 6)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and erwartet in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_umlaut_im_anlass(tmp):
|
||||||
|
baue(tmp, {"wf_u": [ablesung(1, "pruefung:vor Prüfung", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "pruefung:nach Prüfung", "2026-09-24T10:30:00Z", 12, 5)]})
|
||||||
|
code, aus, err = fahre(tmp)
|
||||||
|
return [] if code == 0 and "pruefung:vor Pruefung -> pruefung:nach Pruefung" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}, stderr {err[-200:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_seit(tmp):
|
||||||
|
baue(tmp, {"wf_s": [ablesung(1, "alt", "2026-09-20T10:00:00Z", 10, 5), ablesung(2, "a", "2026-09-24T10:00:00Z", 10, 5),
|
||||||
|
ablesung(3, "b", "2026-09-24T11:00:00Z", 12, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp, "--seit", "2026-09-24")
|
||||||
|
return [] if code == 0 and "2 Ablesungen, 1 Paare" in aus and "alt ->" not in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_seit_ungueltig(tmp):
|
||||||
|
baue(tmp, GRUND)
|
||||||
|
code, aus, _ = fahre(tmp, "--seit", "gestern")
|
||||||
|
return [] if code == 2 and "kein Datum" in aus else [f"Exit {code}, Ausgabe {aus!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_voller_modellname(tmp):
|
||||||
|
baue(tmp, {"wf_m": [GRUND["wf_a"][0], opus(1, "2026-09-24T10:00:10Z", "2026-09-24T10:30:10Z", modell="claude-opus-5-5"), GRUND["wf_a"][2]]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "opus 30.0" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_agent_ausserhalb(tmp):
|
||||||
|
# Ein Agent, der vor der ersten Ablesung endet, zaehlt nicht mit; einer, der halb hineinreicht, anteilig.
|
||||||
|
baue(tmp, {"wf_x": [opus(1, "2026-09-24T09:00:00Z", "2026-09-24T09:50:00Z"), GRUND["wf_a"][0],
|
||||||
|
opus(2, "2026-09-24T09:55:00Z", "2026-09-24T10:10:00Z"), GRUND["wf_a"][2]]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| opus 10.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-400:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_absturz_ist_befund(tmp):
|
||||||
|
# Ein Zeitstempel ohne Zeitzone neben einem mit ihr laesst den Vergleich mit TypeError scheitern.
|
||||||
|
# Der Lauf muss das als Hinweis melden und die anderen Laeufe trotzdem auswerten.
|
||||||
|
kaputt = [dict(GRUND["wf_a"][0]), dict(GRUND["wf_a"][1], start="2026-09-24T10:00:10"), dict(GRUND["wf_a"][2])]
|
||||||
|
baue(tmp, {"wf_a": GRUND["wf_a"], "wf_b": kaputt})
|
||||||
|
code, aus, err = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0:
|
||||||
|
fehler.append(f"Exit {code}: {err[-300:]}")
|
||||||
|
if "wf_b: Auswertung abgebrochen (TypeError" not in aus:
|
||||||
|
fehler.append("Absturz nicht als Hinweis gemeldet")
|
||||||
|
if "| wf_a |" not in aus:
|
||||||
|
fehler.append("der gesunde Lauf fehlt")
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_verschiebung(tmp, reset5, erwartet):
|
||||||
|
# Nur resetsAt verschiebt sich, der Wert steigt; entscheidend ist allein die Toleranz.
|
||||||
|
baue(tmp, {"wf_v": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 12, 5, reset5=reset5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and erwartet in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_fallender_wert(tmp):
|
||||||
|
baue(tmp, {"wf_d": [ablesung(1, "a", "2026-09-24T10:00:00Z", 30, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 5, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| Reset | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_reset_unlesbar(tmp):
|
||||||
|
baue(tmp, {"wf_u": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, reset5="bald"), ablesung(2, "b", "2026-09-24T10:20:00Z", 12, 6)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| unbekannt | 1.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_hoechstes_fenster(tmp):
|
||||||
|
liste = [{"label": "5-hour limit", "percentUsed": 10, "resetsAt": "2026-09-24T18:30:00Z"},
|
||||||
|
{"label": "5-hour limit", "percentUsed": 30, "resetsAt": "2026-09-24T18:30:00Z"},
|
||||||
|
{"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}]
|
||||||
|
baue(tmp, {"wf_h": [ablesung(1, "a", "2026-09-24T10:00:00Z", 0, 0, liste=liste), ablesung(2, "b", "2026-09-24T10:20:00Z", 35, 6)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| 5.0 | 1.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_ablesezeit_ist_ende(tmp):
|
||||||
|
# Der Opus-Agent laeuft ganz innerhalb des ersten Ableseagenten, also vor der Ablesung.
|
||||||
|
baue(tmp, {"wf_t": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, bis="2026-09-24T10:00:10Z"),
|
||||||
|
opus(1, "2026-09-24T10:00:02Z", "2026-09-24T10:00:08Z"),
|
||||||
|
ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| 29.8 | - | 2.0 | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
def f_agent_ueber_das_paar_hinaus(tmp):
|
||||||
|
baue(tmp, {"wf_b": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 5),
|
||||||
|
opus(1, "2026-09-24T10:20:00Z", "2026-09-24T10:50:00Z"), ablesung(3, "c", "2026-09-24T11:00:00Z", 15, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
fehler = []
|
||||||
|
if code != 0 or "| a -> b | 30.0 | opus 10.0 |" not in aus:
|
||||||
|
fehler.append("erstes Paar nicht am Ende gekappt: " + aus[-400:])
|
||||||
|
if "| b -> c | 30.0 | opus 20.0 |" not in aus:
|
||||||
|
fehler.append("zweites Paar falsch: " + aus[-400:])
|
||||||
|
return fehler
|
||||||
|
|
||||||
|
|
||||||
|
def f_ueber_hundert(tmp):
|
||||||
|
baue(tmp, {"wf_g": [ablesung(1, "a", "2026-09-24T10:00:00Z", 98, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 103, 5)]})
|
||||||
|
code, aus, _ = fahre(tmp)
|
||||||
|
return [] if code == 0 and "| 5.0 | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"]
|
||||||
|
|
||||||
|
|
||||||
|
FAELLE = [
|
||||||
|
("Grundlinie: zwei Ablesungen, ein Opus-Agent dazwischen", f_grundlinie),
|
||||||
|
("Reset zwischen zwei Ablesungen", f_reset),
|
||||||
|
("Speicher fehlt: Exit 2", f_speicher_fehlt),
|
||||||
|
("null Ablesungen: Exit 0 mit Zaehlung", f_null_ablesungen),
|
||||||
|
("kaputte Journalzeile: Hinweis, kein Absturz", f_kaputte_zeile),
|
||||||
|
("erste Ablesung ohne Zeitstempel", lambda t: f_ohne_zeitstempel(t, 0)),
|
||||||
|
("letzte Ablesung ohne Zeitstempel", lambda t: f_ohne_zeitstempel(t, 2)),
|
||||||
|
("5-Stunden-Fenster fehlt", lambda t: f_fenster_kaputt(t, [{"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}], "| unbekannt | 1.0 |")),
|
||||||
|
("Umlaut im Anlass auf cp1252", f_umlaut_im_anlass),
|
||||||
|
("Prozent als Text", lambda t: f_fenster_kaputt(t, [{"label": "5-hour limit", "percentUsed": "10", "resetsAt": "2026-09-24T18:30:00Z"}, {"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}])),
|
||||||
|
("Prozent als Wahrheitswert", lambda t: f_fenster_kaputt(t, [{"label": "5-hour limit", "percentUsed": True, "resetsAt": "2026-09-24T18:30:00Z"}, {"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}])),
|
||||||
|
("fenster kein Array", lambda t: f_fenster_kaputt(t, {"a": 1})),
|
||||||
|
("--seit filtert aeltere Ablesungen", f_seit),
|
||||||
|
("--seit ungueltig: Exit 2", f_seit_ungueltig),
|
||||||
|
("voller Modellname zaehlt als opus", f_voller_modellname),
|
||||||
|
("Agent ausserhalb und halb innerhalb des Paars", f_agent_ausserhalb),
|
||||||
|
("Absturz in einem Lauf ist ein Hinweis", f_absturz_ist_befund),
|
||||||
|
("resetsAt 5 Stunden weiter, Wert steigt: Reset", lambda t: f_verschiebung(t, "2026-09-24T23:30:00Z", "| Reset | 0.0 |")),
|
||||||
|
("resetsAt 4:59 verschoben: dasselbe Fenster", lambda t: f_verschiebung(t, "2026-09-24T18:34:59Z", "| 2.0 | 0.0 |")),
|
||||||
|
("resetsAt 5:01 verschoben: Reset", lambda t: f_verschiebung(t, "2026-09-24T18:35:01Z", "| Reset | 0.0 |")),
|
||||||
|
("resetsAt 5:01 frueher: Reset", lambda t: f_verschiebung(t, "2026-09-24T18:24:59Z", "| Reset | 0.0 |")),
|
||||||
|
("fallender Wert im selben Fenster ist ein Reset", f_fallender_wert),
|
||||||
|
("resetsAt ohne Datum: unbekannt", f_reset_unlesbar),
|
||||||
|
("doppeltes 5-Stunden-Fenster: das hoechste zaehlt", f_hoechstes_fenster),
|
||||||
|
("Ablesezeit ist das Ende des Ableseagenten", f_ablesezeit_ist_ende),
|
||||||
|
("Agent ueber das Paar hinaus wird am Ende gekappt", f_agent_ueber_das_paar_hinaus),
|
||||||
|
("ueber 100 Prozent ist ein Wert, kein Fehler", f_ueber_hundert),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Fehlertexte koennen Zeichen ausserhalb der Konsolenkodierung tragen; der Treiber darf daran nicht sterben.
|
||||||
|
sys.stdout.reconfigure(errors="replace")
|
||||||
|
gescheitert = 0
|
||||||
|
for name, fall in FAELLE:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
try:
|
||||||
|
fehler = fall(Path(tmp))
|
||||||
|
except Exception as e: # ein Absturz im Fall ist selbst ein Befund
|
||||||
|
fehler = [f"Absturz im Fall: {type(e).__name__}: {e}"]
|
||||||
|
if fehler:
|
||||||
|
gescheitert += 1
|
||||||
|
print(f"FEHLER {name}\n " + "\n ".join(fehler))
|
||||||
|
else:
|
||||||
|
print(f"OK {name}")
|
||||||
|
print(f"{len(FAELLE)} Faelle, {gescheitert} gescheitert")
|
||||||
|
return 1 if gescheitert else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,848 @@
|
|||||||
|
// Fälle für die Workflow-Skripte unter .claude/workflows/, gefahren mit attrappe.mjs.
|
||||||
|
// Aufruf: node faelle_workflows.mjs [<ordner mit den Workflows>]
|
||||||
|
// Ohne Argument gilt ../../.claude/workflows relativ zu dieser Datei. Je Workflow läuft zuerst
|
||||||
|
// die Grundlinie; scheitert sie, messen die übrigen Fälle dieses Workflows nichts.
|
||||||
|
// Exit 0, wenn alle Fälle bestehen, sonst 1.
|
||||||
|
import { mkdtempSync, writeFileSync, rmSync, cpSync } from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { fuehreAus } from './attrappe.mjs'
|
||||||
|
|
||||||
|
const hier = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const ORDNER = path.resolve(process.argv[2] || path.join(hier, '..', '..', '.claude', 'workflows'))
|
||||||
|
const WF = name => path.join(ORDNER, name + '.js')
|
||||||
|
|
||||||
|
const ok = (wert, dauer = 5) => ({ wert, dauer })
|
||||||
|
const fenster = (fuenf, woche, extra = []) => [
|
||||||
|
{ label: '5-hour limit', percentUsed: fuenf, resetsAt: '2026-09-24T18:30:00.000Z' },
|
||||||
|
{ label: 'Weekly · all models', percentUsed: woche, resetsAt: '2026-09-28T00:00:00.000Z' },
|
||||||
|
...extra,
|
||||||
|
]
|
||||||
|
const ablesung = (fuenf, woche, extra) => ({ verfuegbar: true, status: 'ok', plan: 'Pro', fenster: fenster(fuenf, woche, extra), fehler: '' })
|
||||||
|
const OHNE = 'ohne'
|
||||||
|
|
||||||
|
// Antworten für alle Agenten, die die Workflows starten. Unbekannte Labels werfen, damit ein
|
||||||
|
// falsches Label im Protokoll auffällt.
|
||||||
|
function antworten(k = {}) {
|
||||||
|
let n = 0
|
||||||
|
return async a => {
|
||||||
|
const label = a.label || ''
|
||||||
|
if (label.startsWith('verbrauch:')) {
|
||||||
|
const x = (k.ablesungen || [])[n++]
|
||||||
|
if (x === OHNE) return undefined
|
||||||
|
return ok(x === undefined ? ablesung(10, 10) : x)
|
||||||
|
}
|
||||||
|
if (label === 'werkzeug:planwerkzeuge') return ok({ laeufe: [{ befehl: 'planwerkzeuge', exit: 0, ausgabe: 'keine Befunde' }] })
|
||||||
|
if (label === 'werkzeug:kopie') return ok({ laeufe: [{ befehl: 'kopie', exit: 0, ausgabe: "KOPIE=C:/tmp/k/mat\n{'neu': 2}" }] })
|
||||||
|
if (label === 'werkzeug:vergleich') return ok({ laeufe: [{ befehl: 'vergleich', exit: 0, ausgabe: 'VERGLEICH-OK\nEXIT suite: 0' }] })
|
||||||
|
if (label.startsWith('plankritik:')) {
|
||||||
|
const linse = label.slice('plankritik:'.length)
|
||||||
|
if (k.linse) { const x = k.linse(linse); if (x === OHNE) return undefined; if (x !== undefined) return ok(x, k.dauer || 30) }
|
||||||
|
const basis = { arithmetik: 10, bezeichner: 20, abdeckung: 30 }[linse] || 40
|
||||||
|
return ok({ befunde: [0, 1].map(j => ({ schweregrad: 'wichtig', datei: 'task-01.md', zeile: basis + j, beschreibung: linse + ' ' + j, reproduktion: 'x' })), selbst_ausgefuehrt: [], nicht_pruefbar: [] }, k.dauer || 30)
|
||||||
|
}
|
||||||
|
if (label.startsWith('widerlegung:')) return ok({ urteil: 'bestaetigt', schweregrad: 'wichtig', bedingung: '', begruendung: 'nachgestellt', beleg: 'befehl' }, k.dauer || 30)
|
||||||
|
if (label.startsWith('pruefung')) {
|
||||||
|
if (k.pruefer === OHNE) return undefined
|
||||||
|
return ok({ selbst_ausgefuehrt: [], eigener_lauf: [], befunde: [{ schweregrad: 'klein', datei: 'a.py', zeile: 1, beschreibung: 'x', reproduktion: 'y' }], plan_vs_umsetzung: [], geprueft_und_in_ordnung: [], nicht_pruefbar: [] }, 20)
|
||||||
|
}
|
||||||
|
if (label.startsWith('executer:')) {
|
||||||
|
if (k.executer === OHNE) return undefined
|
||||||
|
return ok({ erledigt: ['alles'], geaenderte_dateien: [], verifikation: [], abweichungen: ['keine'], annahmen: [], beobachtet: [] }, 10)
|
||||||
|
}
|
||||||
|
if (label.startsWith('planer:task-')) {
|
||||||
|
const nr = Number(label.slice('planer:task-'.length))
|
||||||
|
const x = k.planer ? k.planer(nr) : undefined
|
||||||
|
if (x === OHNE) return undefined
|
||||||
|
const ziel = /Schreib genau eine Datei: (\S+\.md)/.exec(a.prompt)
|
||||||
|
const datei = k.planerDatei ? k.planerDatei(nr, ziel && ziel[1]) : ziel && ziel[1]
|
||||||
|
return ok({ datei, zieldateien: [], testdateien: [], abweichungen_von_uebersicht: [], offene_punkte: [] }, k.planerDauer ? k.planerDauer(nr) : 20 + nr * 3)
|
||||||
|
}
|
||||||
|
throw new Error('unerwarteter Agent ' + label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruefe() {
|
||||||
|
const fehler = []
|
||||||
|
return { fehler, soll: (bedingung, text) => { if (!bedingung) fehler.push(text) } }
|
||||||
|
}
|
||||||
|
const mitLabel = (p, anfang) => p.agents.filter(x => (x.label || '').startsWith(anfang))
|
||||||
|
const spitze = (p, phase, modell) => p.spitzeJePhase[phase + '|' + modell] || 0
|
||||||
|
// Wie viele andere Agenten mit diesem Labelanfang liefen, als a startete.
|
||||||
|
const laufendBeiStart = (p, a, anfang) => mitLabel(p, anfang).filter(b => b !== a && b.start < a.start && (b.ende === null || b.ende > a.start)).length
|
||||||
|
function ohneVerstoss(p, soll) { soll(p.verstoesse.length === 0, 'Verstöße: ' + p.verstoesse.join('; ')) }
|
||||||
|
|
||||||
|
const faelle = []
|
||||||
|
const fall = (workflow, name, lauf) => faelle.push({ workflow, name, lauf })
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- attrappe
|
||||||
|
// Die Attrappe muss so streng sein wie das Ziel, sonst misst jeder grüne Fall nur sie selbst.
|
||||||
|
const TMP = mkdtempSync(path.join(os.tmpdir(), 'attrappe-'))
|
||||||
|
function skript(name, rumpf) {
|
||||||
|
const datei = path.join(TMP, name + '.js')
|
||||||
|
writeFileSync(datei, `export const meta = { name: '${name}', description: 'Selbsttest' }\n${rumpf}\n`, 'utf8')
|
||||||
|
return datei
|
||||||
|
}
|
||||||
|
const A = (datei, k) => fuehreAus(datei, {}, antworten(k), { ordner: TMP })
|
||||||
|
|
||||||
|
for (const [i, [name, rumpf, muster]] of [
|
||||||
|
['Date.now() wirft', 'return Date.now()', /Date\.now\(\)/],
|
||||||
|
['new Date() ohne Argument wirft', 'return new Date()', /new Date\(\)/],
|
||||||
|
['Math.random() wirft', 'return Math.random()', /Math\.random\(\)/],
|
||||||
|
['unbekannter Workflow wirft', "return await workflow('gibt-es-nicht', {})", /nicht lesbar/],
|
||||||
|
['Schema ohne properties wirft', "return await agent('x', { label: 'verbrauch:t', model: 'haiku', schema: { type: 'object' } })", /Schema braucht/],
|
||||||
|
].entries()) {
|
||||||
|
fall('attrappe', name, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f } = await A(skript('wirft-' + i, rumpf))
|
||||||
|
soll(f && muster.test(f), 'Fehler: ' + f)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fall('attrappe', 'Grundlinie: Math.min und new Date(0) gehen, Globals sind da', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f } = await A(skript('grund', "phase('P'); log('x'); return [Math.min(1, 2), new Date(0).getTime(), typeof parallel, typeof pipeline, args !== undefined]"))
|
||||||
|
soll(f === null && JSON.stringify(e) === '[1,0,"function","function",true]', 'Fehler/Ergebnis: ' + f + ' ' + JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'workflow() im Kind wirft', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const enkel = skript('enkel', 'return 1')
|
||||||
|
const kind = skript('kind', `return await workflow({ scriptPath: ${JSON.stringify(enkel)} }, {})`)
|
||||||
|
const { fehler: f } = await A(skript('eltern', `return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
|
||||||
|
soll(f && /nesting is limited to one level/.test(f), 'Fehler: ' + f)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'im Kind tragen Agenten die Phase ▸ <name>, phase() wirkt dort nicht', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const kind = skript('kindphase', "phase('Eigen'); return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', phase: 'Eigen' })")
|
||||||
|
const { protokoll: p } = await A(skript('elternphase', `phase('Oben'); return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
|
||||||
|
soll(p.agents.length === 1 && p.agents[0].phase === '▸ kindphase', 'Phase: ' + (p.agents[0] && p.agents[0].phase))
|
||||||
|
soll(JSON.stringify(p.phasen) === '["Oben"]', 'phase() im Kind hat gewirkt: ' + JSON.stringify(p.phasen))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'dasselbe Kind zweimal: ▸ name und ▸ name #2', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const kind = skript('kindzwei', "return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet' })")
|
||||||
|
const { protokoll: p } = await A(skript('elternzwei', `await workflow({ scriptPath: ${JSON.stringify(kind)} }, {}); return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
|
||||||
|
soll(JSON.stringify(p.agents.map(a => a.phase)) === '["▸ kindzwei","▸ kindzwei #2"]', 'Phasen: ' + JSON.stringify(p.agents.map(a => a.phase)))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'agent() ohne model ist ein Verstoß', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { protokoll: p } = await A(skript('ohne-modell', "return await agent('x', { label: 'verbrauch:t' })"))
|
||||||
|
soll(p.verstoesse.some(v => /ohne model/.test(v)), 'Verstöße: ' + JSON.stringify(p.verstoesse))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'Ergebnis ohne required-Feld wird null und ist ein Verstoß', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const schema = "{ type: 'object', properties: { a: { type: 'string' }, b: { type: 'string' } }, required: ['a', 'b'] }"
|
||||||
|
const { ergebnis: e, protokoll: p } = await A(skript('required', `return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', schema: ${schema} })`))
|
||||||
|
soll(e === null && p.verstoesse.some(v => /Ergebnis ohne/.test(v)), 'Ergebnis/Verstöße: ' + JSON.stringify([e, p.verstoesse]))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'Spitze zählt gleichzeitige Agenten je Phase und Modell', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const rumpf = "phase('P'); await parallel([1, 2, 3].map(() => () => agent('x', { label: 'widerlegung:t', model: 'opus', phase: 'P' }))); await agent('x', { label: 'widerlegung:t', model: 'opus', phase: 'P' }); return 1"
|
||||||
|
const { protokoll: p } = await A(skript('spitze', rumpf))
|
||||||
|
soll(spitze(p, 'P', 'opus') === 3, 'Spitze ' + spitze(p, 'P', 'opus'))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- verbrauch
|
||||||
|
const V =(a, k) => fuehreAus(WF('verbrauch'), a, antworten(k), { ordner: ORDNER })
|
||||||
|
|
||||||
|
fall('verbrauch', 'Grundlinie: Agent liest 10/10, wunsch 3 ergibt 3 Plätze', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await V({ anlass: 'test', wunsch: 3 }, {})
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(e && e.status === 'ok' && e.plaetze === 3 && e.gebremst === false && e.quelle === 'agent', 'Ergebnis: ' + JSON.stringify(e))
|
||||||
|
soll(p.agents.length === 1 && p.agents[0].label === 'verbrauch:test' && p.agents[0].modell === 'haiku', 'Agent: ' + JSON.stringify(p.agents.map(x => [x.label, x.modell])))
|
||||||
|
ohneVerstoss(p, soll)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('verbrauch', 'probe ersetzt den Agenten', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await V({ anlass: 't', wunsch: 2, probe: ablesung(10, 10) })
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet trotz probe')
|
||||||
|
soll(e && e.quelle === 'probe' && e.plaetze === 2, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [fuenf, woche, wunsch, plaetze] of [
|
||||||
|
[49.9, 10, 4, 4], [50, 10, 4, 2], [69.9, 10, 4, 2], [70, 10, 4, 1], [84.9, 10, 4, 1], [85, 10, 4, 0], [100, 10, 4, 0], [0, 0, 4, 4],
|
||||||
|
[10, 69.9, 4, 4], [10, 70, 4, 2], [10, 84.9, 4, 2], [10, 85, 4, 1], [10, 94.9, 4, 1], [10, 95, 4, 0], [10, 100, 4, 0],
|
||||||
|
[101, 10, 4, 0], [10, 150, 4, 0],
|
||||||
|
]) {
|
||||||
|
fall('verbrauch', `Stufe 5 h ${fuenf} %, Woche ${woche} % ergibt ${plaetze}`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch, probe: ablesung(fuenf, woche) })
|
||||||
|
soll(e && e.plaetze === plaetze, `plaetze ${e && e.plaetze}, erwartet ${plaetze}`)
|
||||||
|
soll(e && e.gebremst === (plaetze === 0), 'gebremst ' + (e && e.gebremst))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fall('verbrauch', 'wunsch 0 bei 95 % misst nur und bremst nicht', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 0, probe: ablesung(95, 10) })
|
||||||
|
soll(e && e.plaetze === 0 && e.gebremst === false && e.status === 'ok', JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('verbrauch', 'wunsch 7 wird auf die harte Obergrenze 4 gesenkt', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 7, probe: ablesung(10, 10) })
|
||||||
|
soll(e && e.plaetze === 4 && /harte Obergrenze/.test(e.grund), JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const wunsch of [-1, 1.5, '3', null, undefined]) {
|
||||||
|
fall('verbrauch', `wunsch ${JSON.stringify(wunsch)} wirft vor dem Agenten`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await V({ anlass: 't', wunsch })
|
||||||
|
soll(f && /wunsch muss/.test(f), 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const anlass of [undefined, '', ' ', 3]) {
|
||||||
|
fall('verbrauch', `anlass ${JSON.stringify(anlass)} wirft`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f } = await V({ anlass, wunsch: 1 })
|
||||||
|
soll(f && /anlass fehlt/.test(f), 'Fehler: ' + f)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const probe of [null, [], 'x']) {
|
||||||
|
fall('verbrauch', `probe ${JSON.stringify(probe)} wirft`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f } = await V({ anlass: 't', wunsch: 1, probe })
|
||||||
|
soll(f && /probe muss/.test(f), 'Fehler: ' + f)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fall('verbrauch', 'Agent ohne Ergebnis: nicht_gelesen, zwei Plätze, bei wunsch 1 einer', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const a = await V({ anlass: 't', wunsch: 3 }, { ablesungen: [OHNE] })
|
||||||
|
soll(a.ergebnis && a.ergebnis.status === 'nicht_gelesen' && a.ergebnis.plaetze === 2 && a.ergebnis.gebremst === false, JSON.stringify(a.ergebnis))
|
||||||
|
const b = await V({ anlass: 't', wunsch: 1 }, { ablesungen: [OHNE] })
|
||||||
|
soll(b.ergebnis && b.ergebnis.plaetze === 1, JSON.stringify(b.ergebnis))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [name, probe] of [
|
||||||
|
['status unavailable', { verfuegbar: true, status: 'unavailable', plan: '', fenster: [], fehler: '' }],
|
||||||
|
['verfuegbar false', { verfuegbar: false, status: 'fehler', plan: '', fenster: [], fehler: 'Werkzeug fehlt' }],
|
||||||
|
['verfuegbar als Text', { verfuegbar: 'true', status: 'ok', plan: 'Pro', fenster: fenster(10, 10), fehler: '' }],
|
||||||
|
]) {
|
||||||
|
fall('verbrauch', `${name}: nicht_gelesen`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 3, probe })
|
||||||
|
soll(e && e.status === 'nicht_gelesen' && e.plaetze === 2, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok5 = { label: '5-hour limit', percentUsed: 10, resetsAt: 'x' }
|
||||||
|
const okW = { label: 'Weekly · all models', percentUsed: 10, resetsAt: 'y' }
|
||||||
|
for (const [name, liste] of [
|
||||||
|
['fenster leer', []],
|
||||||
|
['fenster kein Array', { a: 1 }],
|
||||||
|
['nur Woche', [okW]],
|
||||||
|
['nur 5 Stunden', [ok5]],
|
||||||
|
['Eintrag null an erster Stelle', [null, ok5, okW]],
|
||||||
|
['Eintrag null an letzter Stelle', [ok5, okW, null]],
|
||||||
|
['label fehlt', [ok5, { percentUsed: 1, resetsAt: 'z' }, okW]],
|
||||||
|
['percentUsed null vorn', [{ ...ok5, percentUsed: null }, okW]],
|
||||||
|
['percentUsed null hinten', [ok5, { ...okW, percentUsed: null }]],
|
||||||
|
['percentUsed als Text', [{ ...ok5, percentUsed: '12' }, okW]],
|
||||||
|
['percentUsed NaN', [{ ...ok5, percentUsed: NaN }, okW]],
|
||||||
|
['percentUsed -1', [ok5, { ...okW, percentUsed: -1 }]],
|
||||||
|
['percentUsed Infinity', [{ ...ok5, percentUsed: Infinity }, okW]],
|
||||||
|
['Opus-Woche ohne Zahl', [ok5, okW, { label: 'Weekly · Opus', percentUsed: null, resetsAt: 'z' }]],
|
||||||
|
]) {
|
||||||
|
fall('verbrauch', `${name}: unbekannte_fenster, alte Obergrenze`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: liste, fehler: '' } })
|
||||||
|
soll(e && e.status === 'unbekannte_fenster' && e.plaetze === 2 && e.gebremst === false, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, liste] of [
|
||||||
|
['doppeltes 5-Stunden-Fenster, hohes zuerst', [{ ...ok5, percentUsed: 90 }, ok5, okW]],
|
||||||
|
['doppeltes 5-Stunden-Fenster, hohes zuletzt', [ok5, okW, { ...ok5, percentUsed: 90 }]],
|
||||||
|
]) {
|
||||||
|
fall('verbrauch', `${name}: das höhere zählt`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: liste, fehler: '' } })
|
||||||
|
soll(e && e.plaetze === 0 && e.gebremst === true && e.fuenf_stunden.prozent === 90, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fall('verbrauch', 'Opus-Woche 90 % zählt wie die Woche', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Opus', percentUsed: 90, resetsAt: 'z' }]) })
|
||||||
|
soll(e && e.plaetze === 1 && e.woche_opus && e.woche_opus.prozent === 90, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('verbrauch', 'ein Label mit "hour" in der Mitte ist kein 5-Stunden-Fenster', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Opus hours', percentUsed: 90, resetsAt: 'z' }]) })
|
||||||
|
soll(e && e.plaetze === 1 && e.fuenf_stunden.prozent === 10, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('verbrauch', 'Woche aller Modelle 90 % zählt auch neben einer niedrigen Opus-Woche', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 90, [{ label: 'Weekly · Opus', percentUsed: 10, resetsAt: 'z' }]) })
|
||||||
|
soll(e && e.plaetze === 1, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [name, fremd] of [['Fable 99 %', 99], ['Fable ohne Zahl', null], ['Fable als Text', 'viel']]) {
|
||||||
|
fall('verbrauch', `fremdes Fenster (${name}) wird übergangen`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Fable', percentUsed: fremd, resetsAt: 'z' }]) })
|
||||||
|
soll(e && e.status === 'ok' && e.plaetze === 4, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [a, b] of [['5-Hour Limit', 'Weekly · All Models'], ['5 hour limit', 'weekly all models'], ['5hour', 'WEEKLY - ALL MODELS']]) {
|
||||||
|
fall('verbrauch', `Schreibweise "${a}" / "${b}" wird erkannt`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: [{ label: a, percentUsed: 60, resetsAt: 'x' }, { label: b, percentUsed: 10, resetsAt: 'y' }], fehler: '' } })
|
||||||
|
soll(e && e.status === 'ok' && e.plaetze === 2, JSON.stringify(e))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- plankritik
|
||||||
|
const PK_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', kopie: 'C:/tmp/k/mat' }
|
||||||
|
const PK = (a, k, o = {}) => fuehreAus(WF('plankritik'), a, antworten(k), { ordner: ORDNER, ...o })
|
||||||
|
|
||||||
|
fall('plankritik', 'Grundlinie: drei Linsen gleichzeitig, Widerlegung zu zweit, drei Ablesungen', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, {})
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
|
||||||
|
soll(spitze(p, 'Plankritik', 'opus') === 3, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
|
||||||
|
soll(spitze(p, 'Widerlegung', 'opus') === 2, 'Widerlegung gleichzeitig: ' + spitze(p, 'Widerlegung', 'opus'))
|
||||||
|
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
|
||||||
|
soll(JSON.stringify(ab) === JSON.stringify(['verbrauch:plankritik:vor Linsen', 'verbrauch:plankritik:vor Widerlegung', 'verbrauch:plankritik:Ende']), 'Ablesungen: ' + JSON.stringify(ab))
|
||||||
|
soll(mitLabel(p, 'verbrauch:').every(x => x.modell === 'haiku'), 'Ablesung nicht auf haiku')
|
||||||
|
soll(e && e.verbrauch.length === 3 && e.plaetze.linsen === 3 && e.plaetze.widerlegung === 2, 'verbrauch/plaetze: ' + JSON.stringify(e && e.plaetze))
|
||||||
|
soll(e && e.zaehlung.bestaetigt === 6 && e.zaehlung.nicht_geprueft === 0, 'zaehlung ' + JSON.stringify(e && e.zaehlung))
|
||||||
|
for (const k of ['zaehlung', 'kopie', 'temp_ordner', 'befunde', 'selbst_ausgefuehrt', 'nicht_pruefbar', 'vorbereitung', 'ohne_ergebnis']) soll(e && k in e, 'Feld fehlt: ' + k)
|
||||||
|
ohneVerstoss(p, soll)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', '5 Stunden bei 60 %: Linsen zu zweit', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(60, 10)] })
|
||||||
|
soll(spitze(p, 'Plankritik', 'opus') === 2 && e.plaetze.linsen === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
|
||||||
|
soll(e.status === 'fertig', 'status ' + e.status)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'gebremst vor den Linsen: keine Linse, keine Widerlegung', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(90, 10)] })
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(e && e.status === 'gebremst' && e.gebremst_vor === 'Linsen', JSON.stringify(e && [e.status, e.gebremst_vor]))
|
||||||
|
soll(mitLabel(p, 'plankritik:').length === 0 && mitLabel(p, 'widerlegung:').length === 0, 'Opus gestartet trotz Bremse')
|
||||||
|
soll(e && e.verbrauch.length === 1 && e.ohne_ergebnis.length === 3, 'verbrauch/ohne_ergebnis ' + JSON.stringify(e && [e.verbrauch.length, e.ohne_ergebnis]))
|
||||||
|
soll(e && e.zaehlung && e.zaehlung.bestaetigt === 0 && e.zaehlung.nicht_geprueft === 0, 'zaehlung ' + JSON.stringify(e && e.zaehlung))
|
||||||
|
soll(e && /neu starten/.test(e.naechster_schritt), 'naechster_schritt ' + (e && e.naechster_schritt))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'gebremst vor der Widerlegung: nur die Befunde über der Schwelle heißen gebremst', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const zeile = { arithmetik: 11, bezeichner: 12, abdeckung: 13 }
|
||||||
|
const mitKlein = linse => ({ befunde: [
|
||||||
|
{ schweregrad: 'wichtig', datei: 'task-01.md', zeile: zeile[linse], beschreibung: 'w', reproduktion: 'x' },
|
||||||
|
{ schweregrad: 'klein', datei: 'task-02.md', zeile: zeile[linse], beschreibung: 'k', reproduktion: 'x' },
|
||||||
|
], selbst_ausgefuehrt: [], nicht_pruefbar: [] })
|
||||||
|
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(10, 10), ablesung(90, 10)], linse: mitKlein })
|
||||||
|
soll(mitLabel(p, 'plankritik:').length === 3, 'Linsen: ' + mitLabel(p, 'plankritik:').length)
|
||||||
|
soll(mitLabel(p, 'widerlegung:').length === 0, 'Widerlegung trotz Bremse')
|
||||||
|
soll(e.status === 'gebremst' && e.gebremst_vor === 'Widerlegung', JSON.stringify([e.status, e.gebremst_vor]))
|
||||||
|
const urteile = e.befunde.map(b => b.schweregrad + ':' + b.urteil).sort()
|
||||||
|
soll(urteile.filter(u => u === 'wichtig:nicht geprüft (gebremst)').length === 3 && urteile.filter(u => u === 'klein:nicht geprüft').length === 3, 'Urteile: ' + JSON.stringify(urteile))
|
||||||
|
soll(e.zaehlung.nicht_geprueft === 6 && e.zaehlung.bestaetigt === 0, JSON.stringify(e.zaehlung))
|
||||||
|
soll(mitLabel(p, 'verbrauch:').length === 3, 'Ablesung am Ende fehlt')
|
||||||
|
soll(/nachholen/.test(e.naechster_schritt), 'naechster_schritt ' + e.naechster_schritt)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'vor der Widerlegung 75 %: Widerlegung einzeln', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(10, 10), ablesung(75, 10)] })
|
||||||
|
soll(spitze(p, 'Widerlegung', 'opus') === 1 && e.plaetze.widerlegung === 1, 'Widerlegung gleichzeitig: ' + spitze(p, 'Widerlegung', 'opus'))
|
||||||
|
soll(mitLabel(p, 'widerlegung:').length === 6 && e.status === 'fertig', JSON.stringify([mitLabel(p, 'widerlegung:').length, e.status]))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'Ablesung ohne Ergebnis: Linsen zu zweit (alte Obergrenze)', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [OHNE] })
|
||||||
|
soll(spitze(p, 'Plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
|
||||||
|
soll(e.status === 'fertig' && e.verbrauch[0].status === 'nicht_gelesen', JSON.stringify(e.verbrauch[0]))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'als Kind: verbrauch.js wirft, Rückfall auf zwei', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, {}, { tiefe: 1 })
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(spitze(p, '▸ plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, '▸ plankritik', 'opus'))
|
||||||
|
soll(mitLabel(p, 'verbrauch:').length === 0, 'Ableseagent trotz Kind')
|
||||||
|
soll(e && e.verbrauch.every(v => v.status === 'nicht_gelesen' && /nicht ausführbar/.test(v.grund)), JSON.stringify(e && e.verbrauch))
|
||||||
|
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'Aufträge nennen den Testort', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const mit = await PK(PK_ARGS, {})
|
||||||
|
const satz = 'Tests fährst du nur in der Kopie C:/tmp/k/mat, nie im echten Projektbaum'
|
||||||
|
for (const a of [...mitLabel(mit.protokoll, 'plankritik:'), ...mitLabel(mit.protokoll, 'widerlegung:')]) {
|
||||||
|
soll(a.prompt.includes(satz) && /eigenen Kopie dieser Kopie/.test(a.prompt), a.label + ' ohne Testort')
|
||||||
|
}
|
||||||
|
const ohne = await PK({ projekt: 'D:/x', planordner: 'D:/x/plan' }, {})
|
||||||
|
for (const a of mitLabel(ohne.protokoll, 'plankritik:')) {
|
||||||
|
soll(/Tests fährst du in dieser Plankritik nicht/.test(a.prompt), a.label + ': ohne Kopie kein Testverbot')
|
||||||
|
}
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'plaetze 1 begrenzt die Linsen auf eine', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { protokoll: p } = await PK({ ...PK_ARGS, plaetze: 1 }, {})
|
||||||
|
soll(spitze(p, 'Plankritik', 'opus') === 1, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const plaetze of [0, 5, 2.5, '3']) {
|
||||||
|
fall('plankritik', `plaetze ${JSON.stringify(plaetze)} wirft vor dem ersten Agenten`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, plaetze }, {})
|
||||||
|
soll(f && /plaetze muss/.test(f), 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fall('plankritik', 'doppelte Linse wirft vor dem ersten Agenten', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, linsen: ['arithmetik', 'bezeichner', 'arithmetik'] }, {})
|
||||||
|
soll(f && /doppelt in linsen/.test(f), 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'Linse ohne Ergebnis steht in ohne_ergebnis', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await PK(PK_ARGS, { linse: l => (l === 'arithmetik' ? OHNE : undefined) })
|
||||||
|
soll(e.status === 'fertig' && JSON.stringify(e.ohne_ergebnis) === '["arithmetik"]', JSON.stringify(e.ohne_ergebnis))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'verifizieren false: keine Ablesung vor der Widerlegung', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PK({ ...PK_ARGS, verifizieren: false }, {})
|
||||||
|
soll(mitLabel(p, 'widerlegung:').length === 0, 'Widerlegung trotz verifizieren false')
|
||||||
|
soll(mitLabel(p, 'verbrauch:').length === 2, 'Ablesungen: ' + mitLabel(p, 'verbrauch:').length)
|
||||||
|
soll(e.status === 'fertig' && e.zaehlung.nicht_geprueft === 6, JSON.stringify(e.zaehlung))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- pruefung
|
||||||
|
const PR_ARGS = { projekt: 'D:/x', plan: 'D:/x/plan' }
|
||||||
|
const PR = (a, k, o = {}) => fuehreAus(WF('pruefung'), a, antworten(k), { ordner: ORDNER, ...o })
|
||||||
|
|
||||||
|
fall('pruefung', 'Grundlinie: Ablesung vor und nach dem pruefer', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await PR(PR_ARGS, {})
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
const folge = p.agents.slice().sort((x, y) => x.start - y.start).map(x => x.label)
|
||||||
|
soll(JSON.stringify(folge) === JSON.stringify(['verbrauch:pruefung:vor Prüfung', 'pruefung', 'verbrauch:pruefung:nach Prüfung']), 'Folge: ' + JSON.stringify(folge))
|
||||||
|
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'ok' && v.plaetze === 0 && v.gebremst === false), JSON.stringify(e && e.verbrauch))
|
||||||
|
soll(e && e.effort === '(Frontmatter)', 'effort ' + (e && e.effort))
|
||||||
|
soll(mitLabel(p, 'pruefung')[0].modell === 'opus', 'pruefer nicht auf opus')
|
||||||
|
ohneVerstoss(p, soll)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('pruefung', 'kritisch hebt auf xhigh', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PR({ ...PR_ARGS, kritisch: true }, {})
|
||||||
|
soll(e.effort === 'xhigh' && mitLabel(p, 'pruefung:kritisch')[0].effort === 'xhigh', 'effort ' + e.effort)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('pruefung', 'verbrauch false: keine Ablesung', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PR({ ...PR_ARGS, verbrauch: false }, {})
|
||||||
|
soll(mitLabel(p, 'verbrauch:').length === 0 && e.verbrauch.length === 0, 'Ablesung trotz verbrauch false')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('pruefung', 'als Kind ohne verbrauch false: Ablesung scheitert, Prüfung läuft', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await PR(PR_ARGS, {}, { tiefe: 1 })
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(mitLabel(p, 'pruefung').length === 1, 'pruefer nicht gelaufen')
|
||||||
|
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('pruefung', 'pruefer ohne Bericht: Fehler, beide Ablesungen gelaufen', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await PR(PR_ARGS, { pruefer: OHNE })
|
||||||
|
soll(f === 'pruefung: der pruefer hat keinen Bericht geliefert', 'Fehler: ' + f)
|
||||||
|
soll(mitLabel(p, 'verbrauch:').length === 2, 'Ablesungen: ' + mitLabel(p, 'verbrauch:').length)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const verbrauch of ['nein', 0, null]) {
|
||||||
|
fall('pruefung', `verbrauch ${JSON.stringify(verbrauch)} wirft`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await PR({ ...PR_ARGS, verbrauch }, {})
|
||||||
|
soll(f && /verbrauch muss/.test(f), 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- welle-mit-pruefung
|
||||||
|
const WM_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', tasks: [{ datei: 'task-01.md', modell: 'haiku' }], kopie: 'C:/tmp/k/mat', suite: 'pytest -q' }
|
||||||
|
const WM = (a, k) => fuehreAus(WF('welle-mit-pruefung'), a, antworten(k), { ordner: ORDNER })
|
||||||
|
|
||||||
|
fall('welle-mit-pruefung', 'Grundlinie: Welle, Vergleich, Ablesung um die Prüfung, Kind liest nicht selbst', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_ARGS, {})
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
|
||||||
|
soll(mitLabel(p, 'executer:').length === 1 && mitLabel(p, 'werkzeug:vergleich').length === 1 && mitLabel(p, 'pruefung').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
|
||||||
|
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
|
||||||
|
soll(JSON.stringify(ab) === JSON.stringify(['verbrauch:welle-mit-pruefung:vor Prüfung', 'verbrauch:welle-mit-pruefung:nach Prüfung']), 'Ablesungen: ' + JSON.stringify(ab))
|
||||||
|
const kind = p.workflows.find(w => w.ref === 'pruefung')
|
||||||
|
soll(kind && kind.args.verbrauch === false, 'Kind pruefung ohne verbrauch false')
|
||||||
|
soll(e.pruefung.verbrauch.length === 0 && mitLabel(p, 'verbrauch:pruefung:').length === 0, 'Kind hat selbst abgelesen')
|
||||||
|
soll(e.verbrauch.length === 2, 'verbrauch ' + e.verbrauch.length)
|
||||||
|
ohneVerstoss(p, soll)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('welle-mit-pruefung', 'bis vergleich: keine Prüfung, pruefung_args mit Rohbelegen', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await WM({ ...WM_ARGS, bis: 'vergleich' }, {})
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(e && e.status === 'bis_vergleich', 'status ' + (e && e.status))
|
||||||
|
soll(mitLabel(p, 'pruefung').length === 0 && mitLabel(p, 'verbrauch:').length === 0, 'Prüfung oder Ablesung trotz bis')
|
||||||
|
const pa = e && e.pruefung_args
|
||||||
|
soll(pa && /VERGLEICH-OK/.test(pa.schwerpunkt) && pa.plan === 'D:/x/plan' && JSON.stringify(pa.tasks) === '["task-01.md"]', 'pruefung_args ' + JSON.stringify(pa))
|
||||||
|
soll(pa && !('verbrauch' in pa), 'pruefung_args trägt verbrauch')
|
||||||
|
soll(e && /nur lesend/.test(e.naechster_schritt), 'naechster_schritt ohne Lesevorgabe')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('welle-mit-pruefung', 'bis vergleich bei unvollständiger Welle: keine pruefung_args', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await WM({ ...WM_ARGS, bis: 'vergleich' }, { executer: OHNE })
|
||||||
|
soll(e && e.status === 'bis_vergleich' && e.pruefung_args === null && /unvollständig/.test(e.pruefung_grund), JSON.stringify(e && [e.status, e.pruefung_args, e.pruefung_grund]))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('welle-mit-pruefung', 'unvollständige Welle: Prüfung übersprungen, keine Ablesung', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await WM(WM_ARGS, { executer: OHNE })
|
||||||
|
soll(e.status === 'ohne_pruefung' && /unvollständig/.test(e.pruefung.uebersprungen), JSON.stringify([e.status, e.pruefung]))
|
||||||
|
soll(mitLabel(p, 'verbrauch:').length === 0 && mitLabel(p, 'pruefung').length === 0, 'Ablesung oder Prüfung trotz Lücke')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('welle-mit-pruefung', 'verbrauch.js fehlt: Ablesungen nicht_gelesen, Welle und Prüfung bleiben', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const ohneVerbrauch = mkdtempSync(path.join(os.tmpdir(), 'ohne-verbrauch-'))
|
||||||
|
cpSync(ORDNER, ohneVerbrauch, { recursive: true })
|
||||||
|
rmSync(path.join(ohneVerbrauch, 'verbrauch.js'))
|
||||||
|
const { ergebnis: e, fehler: f } = await fuehreAus(path.join(ohneVerbrauch, 'welle-mit-pruefung.js'), WM_ARGS, antworten({}), { ordner: ohneVerbrauch })
|
||||||
|
rmSync(ohneVerbrauch, { recursive: true, force: true })
|
||||||
|
soll(f === null && e.status === 'fertig' && e.welle && e.pruefung.zaehlung, 'Fehler/Ergebnis: ' + f + ' ' + JSON.stringify(e && e.status))
|
||||||
|
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const bis of ['alles', '', 1]) {
|
||||||
|
fall('welle-mit-pruefung', `bis ${JSON.stringify(bis)} wirft`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await WM({ ...WM_ARGS, bis }, {})
|
||||||
|
soll(f && /bis kennt nur/.test(f), 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- planung
|
||||||
|
const T = (nr, voraussetzungen, welle) => ({ nr, thema: 'Thema ' + nr, datei: `task-${String(nr).padStart(2, '0')}-t.md`, modell: 'haiku', voraussetzungen, welle })
|
||||||
|
const DAG = [T(1, [], 1), T(2, [1], 2), T(3, [1], 2), T(4, [1], 2), T(5, [2, 3], 3), T(6, [4], 3), T(7, [], 1)]
|
||||||
|
const PL_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', gemeinsam: 'GEMEINSAM', taskauftrag: 'Task {NR} {THEMA} in {DATEI}, Modell {MODELL}, Welle {WELLE}, vor: {VORAUSSETZUNGEN}', tasks: DAG }
|
||||||
|
const PL = (a, k, o = {}) => fuehreAus(WF('planung'), a, antworten(k), { ordner: ORDNER, ...o })
|
||||||
|
const planerVon = (p, nr) => p.agents.find(x => x.label === `planer:task-${String(nr).padStart(2, '0')}`)
|
||||||
|
|
||||||
|
function voraussetzungenEingehalten(p, tasks, soll) {
|
||||||
|
for (const t of tasks) {
|
||||||
|
const a = planerVon(p, t.nr)
|
||||||
|
if (!a) continue
|
||||||
|
for (const n of t.voraussetzungen) {
|
||||||
|
const v = planerVon(p, n)
|
||||||
|
soll(v && v.ende !== null && v.ende < a.start, `Task ${t.nr} startete vor dem Ende von Task ${n}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fall('planung', 'Grundlinie: Start nach den Voraussetzungen, ohne Wunsch zwei gleichzeitig', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await PL(PL_ARGS, {})
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(e && e.status === 'fertig' && e.geschrieben.length === 7, 'status ' + (e && e.status))
|
||||||
|
voraussetzungenEingehalten(p, DAG, soll)
|
||||||
|
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
|
||||||
|
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
|
||||||
|
soll(ab[0] === 'verbrauch:planung:Anfang' && ab[ab.length - 1] === 'verbrauch:planung:Ende' && ab.includes('verbrauch:planung:nach 4 Tasks'), 'Ablesungen: ' + JSON.stringify(ab))
|
||||||
|
soll(e && e.plaetze_verlauf[0] === 2, 'plaetze_verlauf ' + JSON.stringify(e && e.plaetze_verlauf))
|
||||||
|
soll(p.agents.filter(x => x.label.startsWith('planer:')).every(x => x.modell === 'opus' && x.agentType === 'planer'), 'planer nicht auf opus/planer')
|
||||||
|
ohneVerstoss(p, soll)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Wunsch vier: drei bis vier gleichzeitig, Voraussetzungen halten', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, {})
|
||||||
|
const s = spitze(p, 'Taskdateien', 'opus')
|
||||||
|
soll(s >= 3 && s <= 4, 'gleichzeitig: ' + s)
|
||||||
|
soll(e.status === 'fertig' && e.plaetze_verlauf[0] === 4, JSON.stringify([e.status, e.plaetze_verlauf]))
|
||||||
|
voraussetzungenEingehalten(p, DAG, soll)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
const FREI = [1, 2, 3, 4, 5, 6, 7, 8].map(n => T(n, [], 1))
|
||||||
|
|
||||||
|
fall('planung', 'Nachlesen senkt die Plätze: danach startet keiner neben zwei anderen', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 4, nachlesen_alle: 2 }, { ablesungen: [ablesung(10, 10), ablesung(60, 10), ablesung(60, 10), ablesung(60, 10), ablesung(60, 10)] })
|
||||||
|
const zweite = mitLabel(p, 'verbrauch:')[1]
|
||||||
|
const danach = mitLabel(p, 'planer:').filter(a => a.start > zweite.start)
|
||||||
|
soll(danach.length > 0, 'nach der zweiten Ablesung startete kein Planer')
|
||||||
|
soll(danach.every(a => laufendBeiStart(p, a, 'planer:') <= 1), 'Start neben zwei oder mehr: ' + JSON.stringify(danach.map(a => [a.label, laufendBeiStart(p, a, 'planer:')])))
|
||||||
|
soll(e.status === 'fertig' && JSON.stringify(e.plaetze_verlauf.slice(0, 2)) === '[4,2]', JSON.stringify([e.status, e.plaetze_verlauf]))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Nachlesen hebt die Plätze: danach laufen mehr als zwei', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 4, nachlesen_alle: 2 }, { ablesungen: [ablesung(60, 10), ablesung(10, 10), ablesung(10, 10), ablesung(10, 10)] })
|
||||||
|
const zweite = mitLabel(p, 'verbrauch:')[1]
|
||||||
|
const danach = mitLabel(p, 'planer:').filter(a => a.start > zweite.start)
|
||||||
|
soll(danach.some(a => laufendBeiStart(p, a, 'planer:') >= 2), 'nach dem Heben nie mehr als zwei gleichzeitig')
|
||||||
|
soll(e.status === 'fertig' && JSON.stringify(e.plaetze_verlauf.slice(0, 2)) === '[2,4]', JSON.stringify([e.status, e.plaetze_verlauf]))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'gebremst schon am Anfang: kein Planer, Status gebremst', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { ablesungen: [ablesung(90, 10)] })
|
||||||
|
soll(mitLabel(p, 'planer:').length === 0, 'Planer trotz Bremse')
|
||||||
|
soll(e.status === 'gebremst' && e.offen.length === 7, JSON.stringify([e.status, e.offen]))
|
||||||
|
soll(/nicht mit resumeFromRunId/.test(e.naechster_schritt), 'naechster_schritt ' + e.naechster_schritt)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Startreihenfolge folgt Welle und Nummer, nicht der Liste', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: [T(3, [], 2), T(2, [], 1), T(1, [], 1)], plaetze: 1 }, {})
|
||||||
|
const folge = mitLabel(p, 'planer:').sort((x, y) => x.start - y.start).map(a => a.label)
|
||||||
|
soll(JSON.stringify(folge) === '["planer:task-01","planer:task-02","planer:task-03"]', 'Folge ' + JSON.stringify(folge))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'kein Nachlesen, wenn kein Task mehr offen ist', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI.slice(0, 3), plaetze: 4, nachlesen_alle: 1 }, {})
|
||||||
|
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
|
||||||
|
soll(JSON.stringify(ab) === '["verbrauch:planung:Anfang","verbrauch:planung:Ende"]', 'Ablesungen: ' + JSON.stringify(ab))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Ergebnis mit fremder Datei gilt als gescheitert', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { planerDatei: (nr, richtig) => (nr === 2 ? 'D:/x/plan/task-99-t.md' : richtig) })
|
||||||
|
soll(e.gescheitert.includes(2) && e.blockiert.includes(5) && !planerVon(p, 5), JSON.stringify([e.gescheitert, e.blockiert]))
|
||||||
|
soll(p.logs.some(l => /statt task-02-t\.md/.test(l)), 'kein Hinweis auf die fremde Datei')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Ergebnis mit ähnlichem Dateinamen gilt als gescheitert', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await PL(PL_ARGS, { planerDatei: (nr, richtig) => (nr === 7 ? 'D:/x/plan/xtask-07-t.md' : richtig) })
|
||||||
|
soll(JSON.stringify(e.gescheitert) === '[7]', JSON.stringify(e.gescheitert))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Nachlesen im Takt von nachlesen_alle, solange Tasks offen sind', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 2, nachlesen_alle: 2 }, { planerDauer: () => 100 })
|
||||||
|
const ab = mitLabel(p, 'verbrauch:').map(x => x.label.slice('verbrauch:planung:'.length))
|
||||||
|
soll(JSON.stringify(ab) === '["Anfang","nach 2 Tasks","nach 4 Tasks","nach 6 Tasks","Ende"]', 'Ablesungen: ' + JSON.stringify(ab))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', '5 Stunden bei 60 %: höchstens zwei gleichzeitig', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, { ablesungen: [ablesung(60, 10), ablesung(60, 10)] })
|
||||||
|
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
|
||||||
|
soll(e.status === 'fertig', 'status ' + e.status)
|
||||||
|
voraussetzungenEingehalten(p, DAG, soll)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Bremse beim Nachlesen: kein Start mehr, laufende enden', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, nachlesen_alle: 2 }, { ablesungen: [ablesung(10, 10), ablesung(90, 10)] })
|
||||||
|
soll(e.status === 'gebremst' && e.offen.length > 0, JSON.stringify([e.status, e.offen]))
|
||||||
|
const zweite = mitLabel(p, 'verbrauch:')[1]
|
||||||
|
soll(zweite && p.agents.filter(x => x.label.startsWith('planer:') && x.start > zweite.start).length === 0, 'Planer nach der Bremse gestartet')
|
||||||
|
soll(p.agents.filter(x => x.label.startsWith('planer:')).every(x => x.ende !== null), 'laufender Planer nicht beendet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'erste Voraussetzung scheitert: abhängige blockiert, unabhängige laufen', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { planer: nr => (nr === 1 ? OHNE : undefined) })
|
||||||
|
soll(e.status === 'unvollstaendig', 'status ' + e.status)
|
||||||
|
soll(JSON.stringify(e.gescheitert) === '[1]' && JSON.stringify(e.blockiert) === '[2,3,4,5,6]', JSON.stringify([e.gescheitert, e.blockiert]))
|
||||||
|
soll(e.geschrieben.map(g => g.nr).join() === '7', 'geschrieben ' + e.geschrieben.map(g => g.nr))
|
||||||
|
soll([2, 3, 4, 5, 6].every(n => !planerVon(p, n)), 'blockierter Task gestartet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'letzte Tasks scheitern: nichts blockiert', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e } = await PL(PL_ARGS, { planer: nr => (nr === 6 || nr === 7 ? OHNE : undefined) })
|
||||||
|
soll(e.status === 'unvollstaendig' && JSON.stringify(e.gescheitert.slice().sort()) === '[6,7]' && e.blockiert.length === 0, JSON.stringify([e.status, e.gescheitert, e.blockiert]))
|
||||||
|
soll(e.geschrieben.length === 5, 'geschrieben ' + e.geschrieben.length)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'erledigt: Task 1 startet nicht, die abhängigen schon', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, erledigt: [1] }, {})
|
||||||
|
soll(!planerVon(p, 1), 'Task 1 gestartet')
|
||||||
|
soll(e.status === 'fertig' && e.geschrieben.length === 6, JSON.stringify([e.status, e.geschrieben.length]))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'drei Planer ohne Ergebnis: Abbruch, kein weiterer Start', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const frei = [1, 2, 3, 4, 5, 6].map(n => T(n, [], 1))
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: frei, plaetze: 2 }, { planer: nr => (nr <= 3 ? OHNE : undefined), planerDauer: nr => (nr <= 3 ? 5 : 60) })
|
||||||
|
soll(e.status === 'abgebrochen', 'status ' + e.status)
|
||||||
|
soll(!planerVon(p, 5) && !planerVon(p, 6) && JSON.stringify(e.offen) === '[5,6]', 'offen ' + JSON.stringify(e.offen))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [name, aendern, text] of [
|
||||||
|
['Voraussetzung ins Leere', a => ({ ...a, tasks: [T(1, [], 1), T(2, [99], 2)] }), /Voraussetzung 99 gibt es nicht/],
|
||||||
|
['Zyklus', a => ({ ...a, tasks: [T(1, [2], 1), T(2, [1], 2)] }), /Zyklus/],
|
||||||
|
['Selbstbezug', a => ({ ...a, tasks: [T(1, [1], 1)] }), /Zyklus/],
|
||||||
|
['doppelte Nummer', a => ({ ...a, tasks: [T(1, [], 1), T(1, [], 1)] }), /doppelt in tasks/],
|
||||||
|
['doppelte Datei', a => ({ ...a, tasks: [T(1, [], 1), { ...T(2, [], 1), datei: T(1, [], 1).datei }] }), /bei zwei Tasks/],
|
||||||
|
['doppelte Voraussetzung', a => ({ ...a, tasks: [T(1, [], 1), T(2, [1, 1], 2)] }), /Voraussetzung steht doppelt/],
|
||||||
|
['modell unbekannt', a => ({ ...a, tasks: [{ ...T(1, [], 1), modell: 'gpt' }] }), /modell "gpt"/],
|
||||||
|
['erledigt ins Leere', a => ({ ...a, erledigt: [99] }), /erledigt muss/],
|
||||||
|
['plaetze 5', a => ({ ...a, plaetze: 5 }), /plaetze muss/],
|
||||||
|
['nachlesen_alle 0', a => ({ ...a, nachlesen_alle: 0 }), /nachlesen_alle muss/],
|
||||||
|
['tasks leer', a => ({ ...a, tasks: [] }), /nicht leere Liste/],
|
||||||
|
['nr als Text', a => ({ ...a, tasks: [{ ...T(1, [], 1), nr: '1' }] }), /nr muss/],
|
||||||
|
['voraussetzungen fehlen', a => ({ ...a, tasks: [{ ...T(1, [], 1), voraussetzungen: undefined }] }), /voraussetzungen muss/],
|
||||||
|
['gemeinsam fehlt', a => ({ ...a, gemeinsam: '' }), /args fehlen: gemeinsam/],
|
||||||
|
]) {
|
||||||
|
fall('planung', `${name} wirft vor dem ersten Agenten`, async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await PL(aendern(PL_ARGS), {})
|
||||||
|
soll(f && text.test(f), 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fall('planung', 'Auftrag nennt die Voraussetzungen und ersetzt alle Platzhalter', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { protokoll: p } = await PL(PL_ARGS, {})
|
||||||
|
const t5 = planerVon(p, 5).prompt
|
||||||
|
soll(t5.includes('GEMEINSAM') && t5.includes('task-02-t.md') && t5.includes('task-03-t.md') && t5.includes('D:/x/plan/task-05-t.md'), 'Auftrag Task 5 unvollständig')
|
||||||
|
soll(!/\{(NR|THEMA|DATEI|MODELL|WELLE|VORAUSSETZUNGEN)\}/.test(t5), 'Platzhalter übrig')
|
||||||
|
soll(/Lies sie, bevor du einen Namen, eine Signatur oder einen Import daraus übernimmst/.test(t5), 'Lesehinweis fehlt')
|
||||||
|
soll(!/Lies sie/.test(planerVon(p, 1).prompt), 'Lesehinweis ohne Voraussetzungen')
|
||||||
|
soll(/vor: \(keine\)/.test(planerVon(p, 1).prompt), 'Task 1 ohne "(keine)"')
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'alles erledigt: kein Agent, keine Ablesung', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, erledigt: DAG.map(t => t.nr) }, {})
|
||||||
|
soll(p.agents.length === 0 && e.status === 'fertig' && e.verbrauch.length === 0, JSON.stringify([p.agents.length, e.status]))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'Ablesung ohne Ergebnis: höchstens zwei gleichzeitig', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, { ablesungen: [OHNE, OHNE] })
|
||||||
|
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('planung', 'als Kind: Rückfall auf zwei, Planung läuft durch', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, {}, { tiefe: 1 })
|
||||||
|
soll(f === null && e.status === 'fertig', 'Fehler/status: ' + f + ' ' + (e && e.status))
|
||||||
|
soll(spitze(p, '▸ planung', 'opus') === 2, 'gleichzeitig: ' + spitze(p, '▸ planung', 'opus'))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Lauf
|
||||||
|
let gescheitert = 0
|
||||||
|
const grundlinieGescheitert = new Set()
|
||||||
|
for (const f of faelle) {
|
||||||
|
let fehler
|
||||||
|
try { fehler = await f.lauf() } catch (e) { fehler = ['Absturz im Fall: ' + (e && e.stack || e)] }
|
||||||
|
if (f.name.startsWith('Grundlinie') && fehler.length) grundlinieGescheitert.add(f.workflow)
|
||||||
|
if (fehler.length) {
|
||||||
|
gescheitert++
|
||||||
|
console.log(`FEHLER ${f.workflow}: ${f.name}\n ${fehler.join('\n ')}`)
|
||||||
|
} else console.log(`OK ${f.workflow}: ${f.name}`)
|
||||||
|
}
|
||||||
|
for (const w of grundlinieGescheitert) console.log(`HINWEIS Grundlinie von ${w} gescheitert: Die übrigen Fälle dieses Workflows messen nichts.`)
|
||||||
|
rmSync(TMP, { recursive: true, force: true })
|
||||||
|
console.log(`${faelle.length} Fälle, ${gescheitert} gescheitert (Ordner ${ORDNER})`)
|
||||||
|
process.exit(gescheitert ? 1 : 0)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""Mutantenlauf gegen verbrauch_reihe.py: je Mutant genau eine Ersetzung in einer Kopie des Ordners, dann faelle_verbrauch_reihe.py.
|
||||||
|
|
||||||
|
Jede Ersetzung muss genau einmal vorkommen, sonst bricht der Lauf mit Exit 2 ab (veralteter Mutant).
|
||||||
|
Aufruf: python mutanten_reihe.py
|
||||||
|
Exit 0, wenn jeder Mutant gefangen wird, 1 bei einem Ueberlebenden, 2 bei einem veralteten Muster.
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HIER = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
MUTANTEN = [
|
||||||
|
("Reset nur bei gleichem Text", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if a[1] != b[1] or b[0] < a[0]:"),
|
||||||
|
("Toleranz 6 Stunden", "RESET_TOLERANZ = timedelta(minutes=5)", "RESET_TOLERANZ = timedelta(hours=6)"),
|
||||||
|
("Toleranz 1 Sekunde", "RESET_TOLERANZ = timedelta(minutes=5)", "RESET_TOLERANZ = timedelta(seconds=1)"),
|
||||||
|
("Toleranz nur nach vorn", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if rb - ra > RESET_TOLERANZ or b[0] < a[0]:"),
|
||||||
|
("fallender Wert kein Reset", "if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:", "if abs(rb - ra) > RESET_TOLERANZ:"),
|
||||||
|
("unlesbares resetsAt als gleich", " if ra is None or rb is None or (ra.tzinfo is None) != (rb.tzinfo is None):\n return \"unbekannt\"\n", ""),
|
||||||
|
("bool als Zahl", "isinstance(p, bool) or ", ""),
|
||||||
|
("ueber 100 als Fehler", "not math.isfinite(p) or p < 0", "not math.isfinite(p) or not 0 <= p <= 100"),
|
||||||
|
("erstes statt hoechstes 5-Stunden-Fenster", "if art == \"fuenf\" and (fuenf is None or p > fuenf[0]):", "if art == \"fuenf\" and fuenf is None:"),
|
||||||
|
("Ablesezeit Start statt Ende", "\"zeit\": spanne[1]", "\"zeit\": spanne[0]"),
|
||||||
|
("nur_ascii fehlt", "print(nur_ascii(f\"| {z", "print((f\"| {z"),
|
||||||
|
("Ende des Paars nicht gekappt", "min(g[\"ende\"], b[\"zeit\"])", "g[\"ende\"]"),
|
||||||
|
("Anfang des Paars nicht gekappt", "max(g[\"start\"], a[\"zeit\"])", "g[\"start\"]"),
|
||||||
|
("Absturz nicht gefangen", "except Exception as e: # Ein Absturz", "except ZeroDivisionError as e: # Ein Absturz"),
|
||||||
|
("kaputte Zeile bricht ab", " except ValueError:\n befunde.append(f\"{pfad.parent.name}/{pfad.name}: Zeile", " except KeyError:\n befunde.append(f\"{pfad.parent.name}/{pfad.name}: Zeile"),
|
||||||
|
("seit ignoriert", "ablesungen = [x for x in ablesungen if x[\"zeit\"] >= seit]", "pass"),
|
||||||
|
("Speicher fehlt still", " return 2\n seit = None", " return 0\n seit = None"),
|
||||||
|
("Familie aus vollem Namen fehlt", " if f in modell.lower():", " if f == modell.lower():"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ueberlebt, veraltet = [], []
|
||||||
|
for name, alt, neu in MUTANTEN:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
ziel = Path(tmp) / "w"
|
||||||
|
ziel.mkdir()
|
||||||
|
for datei in ("verbrauch_reihe.py", "faelle_verbrauch_reihe.py"):
|
||||||
|
shutil.copy2(HIER / datei, ziel / datei)
|
||||||
|
pfad = ziel / "verbrauch_reihe.py"
|
||||||
|
text = pfad.read_text(encoding="utf-8")
|
||||||
|
if text.count(alt) != 1:
|
||||||
|
veraltet.append(name)
|
||||||
|
print(f"VERALTET {name} (Muster {text.count(alt)}-mal)")
|
||||||
|
continue
|
||||||
|
pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
|
||||||
|
lauf = subprocess.run([sys.executable, str(ziel / "faelle_verbrauch_reihe.py")], capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||||
|
rot = [z for z in lauf.stdout.splitlines() if z.startswith("FEHLER")]
|
||||||
|
if lauf.returncode == 0:
|
||||||
|
ueberlebt.append(name)
|
||||||
|
print(f"UEBERLEBT {name}")
|
||||||
|
else:
|
||||||
|
print(f"GEFANGEN {name} ({rot[0][7:80] if rot else 'Absturz'})")
|
||||||
|
print(f"{len(MUTANTEN)} Mutanten, {len(ueberlebt)} ueberlebt, {len(veraltet)} veraltet")
|
||||||
|
return 2 if veraltet else 1 if ueberlebt else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.stdout.reconfigure(errors="replace")
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""Mutantenlauf gegen die Workflows unter .claude/workflows/: je Mutant genau eine Ersetzung, dann faelle_workflows.mjs.
|
||||||
|
|
||||||
|
Jede Ersetzung muss genau einmal vorkommen, sonst bricht der Lauf mit Exit 2 ab: Ein Muster, das
|
||||||
|
nicht mehr passt, ist ein veralteter Mutant und kein ueberlebter. Ein ueberlebter Mutant ist ein
|
||||||
|
Befund, ausser er steht in WIRKUNGSLOS mit Begruendung.
|
||||||
|
Aufruf: python mutanten_workflows.py [<ordner mit den workflows>]
|
||||||
|
Exit 0, wenn jeder wirksame Mutant gefangen wird, 1 bei einem Ueberlebenden, 2 bei einem veralteten Muster.
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HIER = Path(__file__).resolve().parent
|
||||||
|
FAELLE = HIER / "faelle_workflows.mjs"
|
||||||
|
|
||||||
|
# Mutanten, die bei den heutigen Stufen kein Verhalten aendern, mit Grund.
|
||||||
|
WIRKUNGSLOS = {
|
||||||
|
"harte Obergrenze fehlt": "keine Stufe gibt mehr als vier Plaetze; wer die Stufen anhebt, braucht einen Fall",
|
||||||
|
}
|
||||||
|
|
||||||
|
MUTANTEN = [
|
||||||
|
("verbrauch", "Stufe 50 -> 60", "[[50, 4], [70, 2], [85, 1]]", "[[60, 4], [70, 2], [85, 1]]"),
|
||||||
|
("verbrauch", "Wochenstufe 85 -> 80", "[[70, 4], [85, 2], [95, 1]]", "[[70, 4], [80, 2], [95, 1]]"),
|
||||||
|
("verbrauch", "ueber 100 wieder Fehler", "|| p < 0) return", "|| p < 0 || p > 100) return"),
|
||||||
|
("verbrauch", "doppeltes Fenster: erstes gewinnt", "if (!e[art] || e[art].prozent < p) e[art] = neu", "if (!e[art]) e[art] = neu"),
|
||||||
|
("verbrauch", "Opus-Woche ignoriert", "Math.max(e.woche.prozent, e.opus ? e.opus.prozent : 0)", "e.woche.prozent"),
|
||||||
|
("verbrauch", "Opus-Woche ersetzt die Woche", "Math.max(e.woche.prozent, e.opus ? e.opus.prozent : 0)", "(e.opus ? e.opus.prozent : e.woche.prozent)"),
|
||||||
|
("verbrauch", "harte Obergrenze fehlt", "Math.min(wunsch, grenzeFuenf, grenzeWoche, HARTE_OBERGRENZE)", "Math.min(wunsch, grenzeFuenf, grenzeWoche)"),
|
||||||
|
("verbrauch", "gebremst auch bei wunsch 0", "const gebremst = wunsch > 0 && plaetze === 0", "const gebremst = plaetze === 0"),
|
||||||
|
("verbrauch", "Status ok nicht verlangt", " || ablesung.status !== 'ok'", ""),
|
||||||
|
("verbrauch", "Rueckfall 3 statt 2", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"),
|
||||||
|
("verbrauch", "wunsch-Pruefung fehlt", "if (!Number.isInteger(a.wunsch) || a.wunsch < 0) throw", "if (false) throw"),
|
||||||
|
("verbrauch", "fremdes Fenster mit Zahl geprueft", "if (!art) { e.andere.push(f.label); continue }\n const p = f.percentUsed", "const p = f.percentUsed\n if (!art) { if (typeof p !== 'number') return { fehler: 'x' }; e.andere.push(f.label); continue }"),
|
||||||
|
("plankritik", "Bremse vor Linsen fehlt", "if (vorLinsen.gebremst) {", "if (false) {"),
|
||||||
|
("plankritik", "Bremse vor Widerlegung fehlt", "if (vorWiderlegung.gebremst) {", "if (false) {"),
|
||||||
|
("plankritik", "Widerlegung bis vier", "const WIDERLEGUNG_HOECHSTENS = 2", "const WIDERLEGUNG_HOECHSTENS = 4"),
|
||||||
|
("plankritik", "Widerlegung ignoriert die Ablesung", "} else plaetzeWiderlegung = vorWiderlegung.plaetze", "} else plaetzeWiderlegung = WIDERLEGUNG_HOECHSTENS"),
|
||||||
|
("plankritik", "Linsen fest zwei", "await hoechstens(vorLinsen.plaetze, gewaehlt,", "await hoechstens(2, gewaehlt,"),
|
||||||
|
("plankritik", "Rueckfall ohne Ablesung 3", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"),
|
||||||
|
("plankritik", "Testort fehlt in der Widerlegung", "Du änderst keine Datei im Projekt und keine im Plan. ${testort(ctx)}\n- ", "Du änderst keine Datei im Projekt und keine im Plan.\n- "),
|
||||||
|
("plankritik", "Testort fehlt bei den Linsen", "Executer-Bericht, dessen Testlauf du wiederholen könntest. ${testort(ctx)}", "Executer-Bericht, dessen Testlauf du wiederholen könntest. ${linse === 'arithmetik' ? testort(ctx) : ''}"),
|
||||||
|
("plankritik", "keine Ablesung am Ende", "await ablesen('Ende', 0)\n", "\n"),
|
||||||
|
("plankritik", "gebremste Befunde ohne Hinweis", "'nicht geprüft (gebremst)'", "'nicht geprüft'"),
|
||||||
|
("plankritik", "gebremst auch unter der Schwelle", "gebremstVor && zuPruefen.includes(g) ?", "gebremstVor ?"),
|
||||||
|
("plankritik", "naechster_schritt uebergeht die Bremse", "naechster_schritt: gebremstVor ? ", "naechster_schritt: false ? "),
|
||||||
|
("plankritik", "zaehlung null bei Bremse vor Linsen", "zaehlung: { roh: 0, zusammengefasst: 0, bestaetigt: 0, offen: 0, widerlegt: 0, nicht_geprueft: 0 },", "zaehlung: null,"),
|
||||||
|
("pruefung", "verbrauch false wird uebergangen", "if (args.verbrauch === false) return\n", "\n"),
|
||||||
|
("pruefung", "Ablesung nach der Pruefung fehlt", "await ablesen('nach Prüfung')\n", "\n"),
|
||||||
|
("welle-mit-pruefung", "Kind liest selbst", "{ ...pruefungArgs, verbrauch: false }", "{ ...pruefungArgs }"),
|
||||||
|
("welle-mit-pruefung", "bis vergleich wirkt nicht", "if (args.bis === 'vergleich') {", "if (false) {"),
|
||||||
|
("welle-mit-pruefung", "Ablesung vor der Pruefung fehlt", " await ablesen('vor Prüfung')\n", "\n"),
|
||||||
|
("planung", "Voraussetzung laufend genuegt", "['fertig', 'erledigt'].includes(status.get(n)))) starte(t)", "['fertig', 'erledigt', 'laeuft'].includes(status.get(n)))) starte(t)"),
|
||||||
|
("planung", "keine Blockade", "status.set(t.nr, 'blockiert')\n geaendert = true", "geaendert = false"),
|
||||||
|
("planung", "kein Abbruch nach drei", "gescheitert.length >= ABBRUCH_NACH && !abgebrochen", "false"),
|
||||||
|
("planung", "Bremse beim Nachlesen fehlt", "if (v.gebremst) {\n gebremst = true\n log('Planung gebremst", "if (false) {\n gebremst = true\n log('Planung gebremst"),
|
||||||
|
("planung", "Nachlesen uebernimmt die Plaetze nicht", "log('Planung gebremst: kein weiterer Start, die laufenden Planer enden')\n } else plaetze = v.plaetze", "log('Planung gebremst: kein weiterer Start, die laufenden Planer enden')\n }"),
|
||||||
|
("planung", "Bremse am Anfang nicht gemeldet", " if (v.gebremst) gebremst = true\n else plaetze = v.plaetze", " plaetze = v.plaetze"),
|
||||||
|
("planung", "Plaetze nicht begrenzt", "if (laufend.size >= plaetze) break", "if (laufend.size >= 99) break"),
|
||||||
|
("planung", "Reihenfolge unsortiert", "const reihenfolge = [...args.tasks].sort((x, y) => ((x.welle || 0) - (y.welle || 0)) || (x.nr - y.nr))", "const reihenfolge = [...args.tasks]"),
|
||||||
|
("planung", "Nachlesen ohne offene Tasks", "fertigSeitAblesung >= nachlesenAlle && nochOffen()", "fertigSeitAblesung >= nachlesenAlle"),
|
||||||
|
("planung", "fremde Datei zaehlt als fertig", "const r = passt ? roh : null", "const r = roh"),
|
||||||
|
("planung", "Vorgabe vier statt zwei", "const VORGABE_PLAETZE = 2", "const VORGABE_PLAETZE = 4"),
|
||||||
|
("planung", "Zyklenpruefung fehlt", "if (geschafft < a.tasks.length) throw", "if (false) throw"),
|
||||||
|
("planung", "erledigt ignoriert", "(args.erledigt || []).includes(t.nr) ? 'erledigt' : 'offen'", "'offen'"),
|
||||||
|
("planung", "Rueckfall ohne Ablesung 3", "const OHNE_ABLESUNG = 2", "const OHNE_ABLESUNG = 3"),
|
||||||
|
("planung", "Datei ohne Pfadgrenze", "('/' + roh.datei.replace(/\\\\/g, '/')).endsWith('/' + jeNr.get(nr).datei)", "roh.datei.replace(/\\\\/g, '/').endsWith(jeNr.get(nr).datei)"),
|
||||||
|
("planung", "Nachlesezaehler nicht zurueckgesetzt", " fertigSeitAblesung = 0\n const v = await ablesen(", " const v = await ablesen("),
|
||||||
|
("planung", "nachlesen_alle ignoriert", "const nachlesenAlle = args.nachlesen_alle || NACHLESEN_ALLE", "const nachlesenAlle = NACHLESEN_ALLE"),
|
||||||
|
("planung", "Lesehinweis fehlt", " Lies sie, bevor du einen Namen, eine Signatur oder einen Import daraus übernimmst.", ""),
|
||||||
|
("plankritik", "doppelte Linse erlaubt", " if (a.linsen && new Set(a.linsen).size !== a.linsen.length) throw", " if (false) throw"),
|
||||||
|
("welle-mit-pruefung", "status fertig ohne Pruefung", "status: pruefung ? 'fertig' : 'ohne_pruefung',", "status: 'fertig',"),
|
||||||
|
("welle-mit-pruefung", "Ablesung ohne try", " try {\n r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })\n } catch (e) {\n r = { status: 'nicht_gelesen', grund: 'verbrauch.js nicht ausführbar: ' + (e && e.message) }\n }", " r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })"),
|
||||||
|
("verbrauch", "5-Stunden-Regex unverankert", "const FUENF_STUNDEN = /^\\s*5[- ]?hour/i", "const FUENF_STUNDEN = /hour/i"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ordner = Path(sys.argv[1]) if len(sys.argv) > 1 else HIER.parent.parent / ".claude" / "workflows"
|
||||||
|
ueberlebt, veraltet = [], []
|
||||||
|
for datei, name, alt, neu in MUTANTEN:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
ziel = Path(tmp) / "wf"
|
||||||
|
shutil.copytree(ordner, ziel)
|
||||||
|
pfad = ziel / (datei + ".js")
|
||||||
|
text = pfad.read_text(encoding="utf-8")
|
||||||
|
if text.count(alt) != 1:
|
||||||
|
veraltet.append(name)
|
||||||
|
print(f"VERALTET {datei}: {name} (Muster {text.count(alt)}-mal)")
|
||||||
|
continue
|
||||||
|
pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
|
||||||
|
lauf = subprocess.run(["node", str(FAELLE), str(ziel)], capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||||
|
rot = [z for z in lauf.stdout.splitlines() if z.startswith("FEHLER")]
|
||||||
|
if lauf.returncode == 0:
|
||||||
|
grund = WIRKUNGSLOS.get(name)
|
||||||
|
if grund:
|
||||||
|
print(f"WIRKUNGSLOS {datei}: {name} ({grund})")
|
||||||
|
else:
|
||||||
|
ueberlebt.append(name)
|
||||||
|
print(f"UEBERLEBT {datei}: {name}")
|
||||||
|
else:
|
||||||
|
print(f"GEFANGEN {datei}: {name} ({len(rot)} rot, z. B. {rot[0][7:90] if rot else 'Absturz'})")
|
||||||
|
print(f"{len(MUTANTEN)} Mutanten, {len(ueberlebt)} ueberlebt, {len(veraltet)} veraltet")
|
||||||
|
return 2 if veraltet else 1 if ueberlebt else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.stdout.reconfigure(errors="replace")
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
"""Wertet die Verbrauchsablesungen der Workflows aus: wie viel Prozent des 5-Stunden-Fensters und der Woche zwischen zwei Ablesungen verbraucht wurden und welche Agenten dazwischen liefen.
|
||||||
|
|
||||||
|
Koordinator-Werkzeug (entstanden 2026-09-24). Es misst und beurteilt nichts; die Stufen in
|
||||||
|
.claude/workflows/verbrauch.js stellt der Mensch nach dieser Messreihe ein.
|
||||||
|
Quelle der Daten ist der Session-Speicher von Claude Code. Je Workflow-Lauf liegen unter
|
||||||
|
<speicher>/<sitzung>/subagents/workflows/<lauf>/ die Datei journal.jsonl (Ereignisse "started"
|
||||||
|
mit agentId und label, "result" mit agentId und result), je Agent agent-<id>.jsonl (Zeilen mit
|
||||||
|
timestamp) und agent-<id>.meta.json (model). Ablesungen sind Agenten, deren label mit
|
||||||
|
"verbrauch:" beginnt; ihr result traegt fenster mit label, percentUsed und resetsAt.
|
||||||
|
Die Fenster werden wie in verbrauch.js erkannt: "5-hour ..." und "Weekly ... all models".
|
||||||
|
Was das Skript nicht sieht: den Verbrauch anderer Sitzungen und der Hauptsitzung im selben
|
||||||
|
Zeitraum (er steckt in der Differenz mit drin), Ablesungen ueber probe (sie starten keinen
|
||||||
|
Agenten) und Laeufe, deren Protokolle geloescht sind. Die Kennzahl je Opus-Agentenstunde ist eine
|
||||||
|
Naeherung, kein Nachweis.
|
||||||
|
Aufruf: python verbrauch_reihe.py [--speicher <pfad>] [--seit JJJJ-MM-TT]
|
||||||
|
Exit 0 mit Auswertung (auch bei null Ablesungen), 2 wenn der Speicher fehlt oder --seit kein
|
||||||
|
Datum ist. Die Ausgabe ist reines ASCII, damit sie auch auf einer cp1252-Konsole durchlaeuft.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STANDARD_SPEICHER = Path.home() / ".claude" / "projects" / "D--Claude"
|
||||||
|
RESET_TOLERANZ = timedelta(minutes=5)
|
||||||
|
FUENF = re.compile(r"^\s*5[- ]?hour", re.I)
|
||||||
|
WOCHE = re.compile(r"^\s*weekly\b.*\ball models\b", re.I)
|
||||||
|
FAMILIEN = ("opus", "sonnet", "haiku", "fable")
|
||||||
|
|
||||||
|
|
||||||
|
def zeit(text):
|
||||||
|
return datetime.fromisoformat(text.replace("Z", "+00:00"))
|
||||||
|
|
||||||
|
|
||||||
|
def lies_jsonl(pfad, befunde):
|
||||||
|
"""Liest eine JSONL-Datei zeilenweise; kaputte Zeilen sind ein Befund, kein Absturz."""
|
||||||
|
zeilen = []
|
||||||
|
try:
|
||||||
|
with pfad.open(encoding="utf-8") as datei:
|
||||||
|
for nr, zeile in enumerate(datei, 1):
|
||||||
|
if not zeile.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
zeilen.append(json.loads(zeile))
|
||||||
|
except ValueError:
|
||||||
|
befunde.append(f"{pfad.parent.name}/{pfad.name}: Zeile {nr} ist kein JSON, uebergangen")
|
||||||
|
except OSError as e:
|
||||||
|
befunde.append(f"{pfad.parent.name}/{pfad.name}: nicht lesbar ({type(e).__name__})")
|
||||||
|
return zeilen
|
||||||
|
|
||||||
|
|
||||||
|
def einordnen(fenster):
|
||||||
|
"""(prozent, resetsAt) fuer 5 Stunden und Woche; None, wo das Fenster fehlt oder kaputt ist."""
|
||||||
|
fuenf = woche = None
|
||||||
|
if not isinstance(fenster, list):
|
||||||
|
return None, None
|
||||||
|
for f in fenster:
|
||||||
|
if not isinstance(f, dict) or not isinstance(f.get("label"), str):
|
||||||
|
return None, None
|
||||||
|
art = "fuenf" if FUENF.match(f["label"]) else "woche" if WOCHE.match(f["label"]) else None
|
||||||
|
if art is None:
|
||||||
|
continue
|
||||||
|
p = f.get("percentUsed")
|
||||||
|
if isinstance(p, bool) or not isinstance(p, (int, float)) or not math.isfinite(p) or p < 0:
|
||||||
|
return None, None
|
||||||
|
eintrag = (float(p), f.get("resetsAt"))
|
||||||
|
if art == "fuenf" and (fuenf is None or p > fuenf[0]):
|
||||||
|
fuenf = eintrag
|
||||||
|
elif art == "woche" and (woche is None or p > woche[0]):
|
||||||
|
woche = eintrag
|
||||||
|
return fuenf, woche
|
||||||
|
|
||||||
|
|
||||||
|
def zeitspanne(ordner, agent_id, befunde):
|
||||||
|
stempel = []
|
||||||
|
for o in lies_jsonl(ordner / f"agent-{agent_id}.jsonl", befunde):
|
||||||
|
t = o.get("timestamp") if isinstance(o, dict) else None
|
||||||
|
if isinstance(t, str):
|
||||||
|
try:
|
||||||
|
stempel.append(zeit(t))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return (min(stempel), max(stempel)) if stempel else None
|
||||||
|
|
||||||
|
|
||||||
|
def familie(ordner, agent_id):
|
||||||
|
try:
|
||||||
|
modell = json.loads((ordner / f"agent-{agent_id}.meta.json").read_text(encoding="utf-8")).get("model")
|
||||||
|
except (OSError, ValueError, AttributeError):
|
||||||
|
return "?"
|
||||||
|
if not isinstance(modell, str):
|
||||||
|
return "?"
|
||||||
|
for f in FAMILIEN:
|
||||||
|
if f in modell.lower():
|
||||||
|
return f
|
||||||
|
return modell
|
||||||
|
|
||||||
|
|
||||||
|
def lauf_auswerten(ordner, befunde):
|
||||||
|
labels, ergebnisse = {}, {}
|
||||||
|
for o in lies_jsonl(ordner / "journal.jsonl", befunde):
|
||||||
|
if not isinstance(o, dict) or not isinstance(o.get("agentId"), str):
|
||||||
|
continue
|
||||||
|
if o.get("type") == "started":
|
||||||
|
labels[o["agentId"]] = o.get("label") if isinstance(o.get("label"), str) else ""
|
||||||
|
elif o.get("type") == "result":
|
||||||
|
ergebnisse[o["agentId"]] = o.get("result")
|
||||||
|
ablesungen, agenten = [], []
|
||||||
|
for agent_id, label in labels.items():
|
||||||
|
spanne = zeitspanne(ordner, agent_id, befunde)
|
||||||
|
if label.startswith("verbrauch:"):
|
||||||
|
if spanne is None:
|
||||||
|
befunde.append(f"{ordner.name}: Ablesung {label} ohne Zeitstempel, uebergangen")
|
||||||
|
continue
|
||||||
|
ergebnis = ergebnisse.get(agent_id)
|
||||||
|
fuenf, woche = einordnen(ergebnis.get("fenster") if isinstance(ergebnis, dict) else None)
|
||||||
|
# Ablesezeit ist das Ende des Ableseagenten: get_usage laeuft kurz vor seinem Schluss.
|
||||||
|
ablesungen.append({"anlass": label[len("verbrauch:"):], "zeit": spanne[1], "fuenf": fuenf, "woche": woche})
|
||||||
|
elif spanne is not None:
|
||||||
|
agenten.append({"familie": familie(ordner, agent_id), "start": spanne[0], "ende": spanne[1]})
|
||||||
|
ablesungen.sort(key=lambda a: a["zeit"])
|
||||||
|
return ablesungen, agenten
|
||||||
|
|
||||||
|
|
||||||
|
def zeitpunkt(text):
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return zeit(text)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def differenz(a, b):
|
||||||
|
"""Prozentpunkte von a nach b im selben Fenster.
|
||||||
|
|
||||||
|
get_usage rechnet resetsAt bei jeder Ablesung neu aus; der Wert schwankt um Millisekunden und
|
||||||
|
springt dabei auch ueber eine Sekunden- oder Datumsgrenze (gemessen 2026-09-24: 18:29:59.664Z,
|
||||||
|
18:30:00.267Z, 18:30:00.459Z fuer dasselbe Fenster). Als Reset gilt deshalb erst eine
|
||||||
|
Verschiebung um mehr als RESET_TOLERANZ oder ein fallender Wert.
|
||||||
|
"""
|
||||||
|
if a is None or b is None:
|
||||||
|
return "unbekannt"
|
||||||
|
ra, rb = zeitpunkt(a[1]), zeitpunkt(b[1])
|
||||||
|
if ra is None or rb is None or (ra.tzinfo is None) != (rb.tzinfo is None):
|
||||||
|
return "unbekannt"
|
||||||
|
if abs(rb - ra) > RESET_TOLERANZ or b[0] < a[0]:
|
||||||
|
return "Reset"
|
||||||
|
return round(b[0] - a[0], 1)
|
||||||
|
|
||||||
|
|
||||||
|
def paare(ordner, ablesungen, agenten):
|
||||||
|
for a, b in zip(ablesungen, ablesungen[1:]):
|
||||||
|
minuten = {}
|
||||||
|
for g in agenten:
|
||||||
|
anteil = (min(g["ende"], b["zeit"]) - max(g["start"], a["zeit"])).total_seconds()
|
||||||
|
if anteil > 0:
|
||||||
|
minuten[g["familie"]] = minuten.get(g["familie"], 0.0) + anteil / 60
|
||||||
|
yield {
|
||||||
|
"lauf": ordner.name,
|
||||||
|
"von": a["anlass"],
|
||||||
|
"bis": b["anlass"],
|
||||||
|
"minuten": round((b["zeit"] - a["zeit"]).total_seconds() / 60, 1),
|
||||||
|
"agenten": {k: round(v, 1) for k, v in sorted(minuten.items())},
|
||||||
|
"d5": differenz(a["fuenf"], b["fuenf"]),
|
||||||
|
"dw": differenz(a["woche"], b["woche"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "Ä": "Ae", "Ö": "Oe", "Ü": "Ue", "ß": "ss"})
|
||||||
|
|
||||||
|
|
||||||
|
def nur_ascii(text):
|
||||||
|
return str(text).translate(UMLAUTE).encode("ascii", "replace").decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
p = argparse.ArgumentParser(description="Messreihe der Verbrauchsablesungen aus den Workflow-Protokollen")
|
||||||
|
p.add_argument("--speicher", type=Path, default=STANDARD_SPEICHER)
|
||||||
|
p.add_argument("--seit", default=None, help="nur Ablesungen ab diesem Tag (JJJJ-MM-TT, UTC)")
|
||||||
|
a = p.parse_args(argv)
|
||||||
|
if not a.speicher.is_dir():
|
||||||
|
print(nur_ascii(f"Speicher fehlt: {a.speicher}"))
|
||||||
|
return 2
|
||||||
|
seit = None
|
||||||
|
if a.seit:
|
||||||
|
try:
|
||||||
|
seit = datetime.fromisoformat(a.seit).replace(tzinfo=timezone.utc)
|
||||||
|
except ValueError:
|
||||||
|
print(nur_ascii(f"--seit {a.seit!r} ist kein Datum JJJJ-MM-TT"))
|
||||||
|
return 2
|
||||||
|
befunde, zeilen, anzahl = [], [], 0
|
||||||
|
for ordner in sorted(a.speicher.glob("*/subagents/workflows/wf_*")):
|
||||||
|
if not ordner.is_dir():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ablesungen, agenten = lauf_auswerten(ordner, befunde)
|
||||||
|
if seit is not None:
|
||||||
|
ablesungen = [x for x in ablesungen if x["zeit"] >= seit]
|
||||||
|
anzahl += len(ablesungen)
|
||||||
|
zeilen.extend(paare(ordner, ablesungen, agenten))
|
||||||
|
except Exception as e: # Ein Absturz ist ein Befund (pruefwerkzeuge.md, Abschnitt 4).
|
||||||
|
befunde.append(f"{ordner.name}: Auswertung abgebrochen ({type(e).__name__}: {e})")
|
||||||
|
print(f"{anzahl} Ablesungen, {len(zeilen)} Paare")
|
||||||
|
if zeilen:
|
||||||
|
print("| Lauf | von -> bis | Minuten | Agentenminuten | d 5h % | d Woche % |")
|
||||||
|
print("|---|---|---|---|---|---|")
|
||||||
|
for z in zeilen:
|
||||||
|
agenten = ", ".join(f"{k} {v}" for k, v in z["agenten"].items()) or "-"
|
||||||
|
print(nur_ascii(f"| {z['lauf']} | {z['von']} -> {z['bis']} | {z['minuten']} | {agenten} | {z['d5']} | {z['dw']} |"))
|
||||||
|
werte = [z for z in zeilen if isinstance(z["d5"], float) and z["agenten"].get("opus", 0) > 0]
|
||||||
|
if werte:
|
||||||
|
prozent = sum(z["d5"] for z in werte)
|
||||||
|
stunden = sum(z["agenten"]["opus"] for z in werte) / 60
|
||||||
|
print(f"Naeherung: {prozent / stunden:.1f} % des 5-Stunden-Fensters je Opus-Agentenstunde "
|
||||||
|
f"({len(werte)} Paare, {stunden:.1f} Opus-Stunden; andere Sitzungen und Nicht-Opus-Agenten stecken in der Differenz)")
|
||||||
|
else:
|
||||||
|
print("Keine auswertbaren Paare mit Opus-Agenten.")
|
||||||
|
if befunde:
|
||||||
|
print("Hinweise:")
|
||||||
|
for b in befunde:
|
||||||
|
print(nur_ascii("- " + b))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
type: resource
|
type: resource
|
||||||
status: reference
|
status: pending-review
|
||||||
title: Codeworkflow
|
title: Codeworkflow
|
||||||
summary: Arbeitsweise für Code-Projekte — Standardschleife, Planaufteilung je Task, Subagenten und ihre Modellstufen und Effort, Gegenprüfung, Lauf mit echten Daten, Aufgabenmanagement.
|
summary: Arbeitsweise für Code-Projekte — Standardschleife, Planaufteilung je Task, Subagenten und ihre Modellstufen und Effort, Gegenprüfung, Lauf mit echten Daten, Aufgabenmanagement.
|
||||||
tags: [regelwerk, codeworkflow, code-projekte]
|
tags: [regelwerk, codeworkflow, code-projekte]
|
||||||
created: 2026-08-03
|
created: 2026-08-03
|
||||||
updated: 2026-09-23
|
updated: 2026-09-24
|
||||||
---
|
---
|
||||||
|
|
||||||
# Codeworkflow
|
# Codeworkflow
|
||||||
@@ -69,6 +69,8 @@ Das Feld `effort:` im Task-Frontmatter ist freiwillig. Fehlt es, gilt das Frontm
|
|||||||
- Die zentralen Artefakte selbst lesen (SKILL.md, README, die Hauptmodule) statt sie aus dem Bericht zu glauben.
|
- Die zentralen Artefakte selbst lesen (SKILL.md, README, die Hauptmodule) statt sie aus dem Bericht zu glauben.
|
||||||
- **Einen eigenen Lauf mit echten Daten fahren, mit eigenem Prüfcode.** Nicht die Tests des Agenten. Siehe „Lauf mit echten Daten" unten.
|
- **Einen eigenen Lauf mit echten Daten fahren, mit eigenem Prüfcode.** Nicht die Tests des Agenten. Siehe „Lauf mit echten Daten" unten.
|
||||||
|
|
||||||
|
**Die eigene Gegenprüfung läuft, während der `pruefer` arbeitet, nicht danach.** `welle-mit-pruefung` mit `bis: "vergleich"` endet vor der Prüfung und gibt `pruefung_args` mit den Rohbelegen zurück; damit startest du `pruefung` im Hintergrund und prüfst in derselben Zeit selbst. Solange er läuft, schreibst du nichts in Projektbaum oder Planordner, fährst Rot- und Mutantentests nur auf einer Kopie und brichst ihn nicht ab, auch nicht bei einem eigenen Blocker. Ein abgebrochener Prüfer hinterlässt eine Runde ohne volle Abdeckung, und jede weitere Korrekturrunde bringt ihre eigene Fehlerschicht. Plankorrektur und Commit kommen nach seinem Bericht. Der Gewinn ist höchstens die Lücke nach der Prüfung: im Hub am 2026-09-24 drei bis elf Minuten je Prüfung, gerechnet aus sechs Prüfungen, nicht gemessen.
|
||||||
|
|
||||||
**Mechanische Prüfungen brauchen einen Regressionsnachweis.** Ein Prüfskript gilt erst als Gate, wenn du es mit absichtlich kaputter Eingabe hast anschlagen sehen — Exit-Code und Meldung selbst beobachtet, nicht aus einem Bericht übernommen. Ohne diesen Nachweis ist ein grüner Lauf kein Ergebnis, sondern eine unbelegte Zusicherung.
|
**Mechanische Prüfungen brauchen einen Regressionsnachweis.** Ein Prüfskript gilt erst als Gate, wenn du es mit absichtlich kaputter Eingabe hast anschlagen sehen — Exit-Code und Meldung selbst beobachtet, nicht aus einem Bericht übernommen. Ohne diesen Nachweis ist ein grüner Lauf kein Ergebnis, sondern eine unbelegte Zusicherung.
|
||||||
|
|
||||||
Für jede Prüfung, die eine **offene Menge** einschränkt (verbotene Aktionen, verbotene Wörter, unerlaubte Muster), gilt zusätzlich: **Positivliste statt Negativliste.** Was nicht ausdrücklich erlaubt ist, ist ein Befund. Eine handgepflegte Negativliste verliert prinzipiell gegen eine offene Menge — 2026-08-18 im Projekt „Logging" fünfmal hintereinander belegt: jede Fassung des Rollenprüfskripts schloss die genannte Lücke und riss eine neue auf. Der Kopfkommentar eines solchen Skripts benennt deshalb seinen Geltungsbereich: was es prüft, was es nicht sieht, und dass es ein Fangnetz ist und kein Nachweis. Wo der belastbare Nachweis erst in der Zielumgebung möglich ist, gehört er dorthin und wird bis dahin als offen geführt.
|
Für jede Prüfung, die eine **offene Menge** einschränkt (verbotene Aktionen, verbotene Wörter, unerlaubte Muster), gilt zusätzlich: **Positivliste statt Negativliste.** Was nicht ausdrücklich erlaubt ist, ist ein Befund. Eine handgepflegte Negativliste verliert prinzipiell gegen eine offene Menge — 2026-08-18 im Projekt „Logging" fünfmal hintereinander belegt: jede Fassung des Rollenprüfskripts schloss die genannte Lücke und riss eine neue auf. Der Kopfkommentar eines solchen Skripts benennt deshalb seinen Geltungsbereich: was es prüft, was es nicht sieht, und dass es ein Fangnetz ist und kein Nachweis. Wo der belastbare Nachweis erst in der Zielumgebung möglich ist, gehört er dorthin und wird bis dahin als offen geführt.
|
||||||
@@ -81,6 +83,8 @@ Für jede Prüfung, die eine **offene Menge** einschränkt (verbotene Aktionen,
|
|||||||
|
|
||||||
Faustregel: Lässt sich die Nachbesserung in einem Auftrag beschreiben, der für sich steht — geänderte Planstelle, Fehlerbild, erwartete Fehlermeldung —, dann ein frischer Subagent. Fortsetzen nur, wenn der Verlauf selbst gebraucht wird, etwa bei einer Fehlersuche über mehrere Runden.
|
Faustregel: Lässt sich die Nachbesserung in einem Auftrag beschreiben, der für sich steht — geänderte Planstelle, Fehlerbild, erwartete Fehlermeldung —, dann ein frischer Subagent. Fortsetzen nur, wenn der Verlauf selbst gebraucht wird, etwa bei einer Fehlersuche über mehrere Runden.
|
||||||
|
|
||||||
|
**Nachbesserung nach einer ausgeführten Welle als Delta, nicht als Neufassung.** Der `planer` schreibt dann neue Taskdateien mit `Aus:`/`wird:`-Blöcken und für ältere Tasks eine Folgenliste. Die Regel für ihn steht in `.claude/agents/planer.md`, weil er das Regelwerk nicht lädt. Dein Teil: Die Plankritik liest nur die neuen Tasks und die Folgenliste (`materialisieren.tasks`). Nach dem Anwenden läuft `plan_bloecke.py nach` auf einer frisch gebauten Kopie. Die Folgenliste setzt ein Executer ein, danach prüfst du die Zeilenenden mit `git ls-files --eol` oder `file`, nicht mit `grep`. Und die `spec.json` des Rot-Nachweises bekommt die neuen Tasks. Gemessen im Hub am 2026-09-24: rund 20 Minuten je Runde schneller als die Neufassung, drei gegen drei Runden, nicht kontrolliert. Am 2026-09-23 waren beim Neuschreiben 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Für die Korrektur nach der Plankritik vor der ersten Ausführung ist das Delta nicht belegt.
|
||||||
|
|
||||||
**Die Nachbesserung folgt derselben Reihenfolge wie der Task selbst:** erst die neuen Tests, Fehlschlag im Wortlaut beobachten lassen, dann die Implementierung. Wer beides gleichzeitig schreiben lässt, bekommt einen grünen Lauf ohne Beleg, dass die neuen Tests je etwas gemessen haben.
|
**Die Nachbesserung folgt derselben Reihenfolge wie der Task selbst:** erst die neuen Tests, Fehlschlag im Wortlaut beobachten lassen, dann die Implementierung. Wer beides gleichzeitig schreiben lässt, bekommt einen grünen Lauf ohne Beleg, dass die neuen Tests je etwas gemessen haben.
|
||||||
|
|
||||||
Dabei die **Ertragskurve mitschreiben**: was hat Runde 1 gefunden, was Runde 2, was Runde 3. Fällt der Ertrag auf Umformulierungen, ist Schluss — und der Prüfer wird im Abschlussauftrag ausdrücklich gefragt, ob eine weitere Runde noch Substanz hat. Er hat darauf brauchbar geantwortet und einmal von sich aus empfohlen, ohne weitere Runde zu committen. **Jede Nachbesserung ist selbst neuer, ungeprüfter Code** und bekommt in derselben Runde ihren eigenen Regressionsfall.
|
Dabei die **Ertragskurve mitschreiben**: was hat Runde 1 gefunden, was Runde 2, was Runde 3. Fällt der Ertrag auf Umformulierungen, ist Schluss — und der Prüfer wird im Abschlussauftrag ausdrücklich gefragt, ob eine weitere Runde noch Substanz hat. Er hat darauf brauchbar geantwortet und einmal von sich aus empfohlen, ohne weitere Runde zu committen. **Jede Nachbesserung ist selbst neuer, ungeprüfter Code** und bekommt in derselben Runde ihren eigenen Regressionsfall.
|
||||||
@@ -122,6 +126,7 @@ Was echte Daten aufdecken und Fixtures nicht: verdrehte Vorzeichen, Skalierungsp
|
|||||||
- **Testdoubles, die nur zurückspiegeln.** Ein Fake, der beim Lesen zurückgibt, was er beim Schreiben bekam, bestätigt die Annahmen des Produktivcodes statt die echte Schnittstelle. Bei asymmetrischen APIs (Schreiben nimmt IDs, Lesen liefert Objekte) muss der Fake die Asymmetrie nachbilden. Im Zweifel in der API-Dokumentation nachschlagen statt Symmetrie anzunehmen.
|
- **Testdoubles, die nur zurückspiegeln.** Ein Fake, der beim Lesen zurückgibt, was er beim Schreiben bekam, bestätigt die Annahmen des Produktivcodes statt die echte Schnittstelle. Bei asymmetrischen APIs (Schreiben nimmt IDs, Lesen liefert Objekte) muss der Fake die Asymmetrie nachbilden. Im Zweifel in der API-Dokumentation nachschlagen statt Symmetrie anzunehmen.
|
||||||
- **Schalterpaare.** Wo ein Schalter Schreibzugriffe freigibt und ein anderer sie unterdrückt, ist die Kombination der Testfall — nicht jeder für sich. Für jeden Trockenlauf-Modus einen Test führen, der über das Testdouble *jeden* schreibenden Aufruf zählt und auf null prüft, statt nur das erwartete Endergebnis zu betrachten.
|
- **Schalterpaare.** Wo ein Schalter Schreibzugriffe freigibt und ein anderer sie unterdrückt, ist die Kombination der Testfall — nicht jeder für sich. Für jeden Trockenlauf-Modus einen Test führen, der über das Testdouble *jeden* schreibenden Aufruf zählt und auf null prüft, statt nur das erwartete Endergebnis zu betrachten.
|
||||||
- **Der Test misst den Inhalt, nicht den Rahmen.** (2026-08-07, „Paketierung Intune") Die Layouttests prüften, ob der Text ins Fenster passt — grün. Ob das *Fenster* auf den *Bildschirm* passt, prüfte niemand: auf 1366×768 bei 125 % Skalierung lag die Fußzeile mit beiden Schaltflächen unter dem Bildschirmrand, der Benutzer hätte nichts mehr anklicken können. Wo etwas in einen Container gesetzt wird, immer beide Ebenen prüfen: passt der Inhalt hinein, und passt der Container dorthin, wo er landet. Die äußere Ebene gehört als reine Funktion mit den Maßen als Parameter getestet — der Entwicklungsrechner ist nie das kleinste Zielgerät.
|
- **Der Test misst den Inhalt, nicht den Rahmen.** (2026-08-07, „Paketierung Intune") Die Layouttests prüften, ob der Text ins Fenster passt — grün. Ob das *Fenster* auf den *Bildschirm* passt, prüfte niemand: auf 1366×768 bei 125 % Skalierung lag die Fußzeile mit beiden Schaltflächen unter dem Bildschirmrand, der Benutzer hätte nichts mehr anklicken können. Wo etwas in einen Container gesetzt wird, immer beide Ebenen prüfen: passt der Inhalt hinein, und passt der Container dorthin, wo er landet. Die äußere Ebene gehört als reine Funktion mit den Maßen als Parameter getestet — der Entwicklungsrechner ist nie das kleinste Zielgerät.
|
||||||
|
- **Ein Thread im Test, viele im Betrieb.** (2026-09-24, „Nexhorizons Hub") FastAPI führt synchrone Abhängigkeiten und Endpunkte im Threadpool aus, und alle Anfragen teilten sich eine SQLite-Verbindung. 402 Tests, drei Plankritik-Runden und zwei Prüfungen waren grün, weil jeder Test einen Thread hat; erst ein Lastlauf gegen die echte Mittelschicht zeigte ein falsches Cookie mit Admin-Sitzung und eine gebrochene Audit-Kette. Hält ein Zustand eine Verbindung, einen Zähler oder einen Cache, steht im Plan, wer ihn aus welchem Thread benutzt, und der Lauf mit echten Daten fährt parallele Klienten.
|
||||||
|
|
||||||
**Screenshots zur Fehlersuche.** Vor `CopyFromScreen` immer `SetProcessDPIAware()` aufrufen und das Fenster gezielt über `GetWindowRect` abfotografieren. Ohne das liefert `SystemInformation.VirtualScreen` bei aktiver Skalierung die *logische* Größe, während *physische* Pixel kopiert werden — das Ergebnis ist ein Ausschnitt der linken oberen Ecke, kein verkleinertes Gesamtbild. Sieht ein Fenster auf einem Screenshot abgeschnitten aus: zuerst das Aufnahmeverfahren prüfen, dann den Code. Sonst wird am Produktivcode ein Fehler „behoben", den es nicht gibt.
|
**Screenshots zur Fehlersuche.** Vor `CopyFromScreen` immer `SetProcessDPIAware()` aufrufen und das Fenster gezielt über `GetWindowRect` abfotografieren. Ohne das liefert `SystemInformation.VirtualScreen` bei aktiver Skalierung die *logische* Größe, während *physische* Pixel kopiert werden — das Ergebnis ist ein Ausschnitt der linken oberen Ecke, kein verkleinertes Gesamtbild. Sieht ein Fenster auf einem Screenshot abgeschnitten aus: zuerst das Aufnahmeverfahren prüfen, dann den Code. Sonst wird am Produktivcode ein Fehler „behoben", den es nicht gibt.
|
||||||
|
|
||||||
@@ -231,14 +236,16 @@ Warum nicht überall die billigste Stufe: Beim Prüfer ist Abdeckung der Zweck.
|
|||||||
|
|
||||||
Was die Messung nicht belegt: Die Prüfstände waren klein, ein Modul mit 73 Zeilen. Bei Plänen und Reviews über viele Dateien kann der Abstand zwischen den Stufen anders aussehen. Die Recherche-Aufgabe (acht Fragen, Antworten verteilt auf fünf Regelwerk-Dateien) war zu leicht: Haiku und Sonnet beantworteten sie in jeder Stufe 8 von 8. Belegt ist dort nur, dass Nachschlagen keinen hohen Effort braucht.
|
Was die Messung nicht belegt: Die Prüfstände waren klein, ein Modul mit 73 Zeilen. Bei Plänen und Reviews über viele Dateien kann der Abstand zwischen den Stufen anders aussehen. Die Recherche-Aufgabe (acht Fragen, Antworten verteilt auf fünf Regelwerk-Dateien) war zu leicht: Haiku und Sonnet beantworteten sie in jeder Stufe 8 von 8. Belegt ist dort nur, dass Nachschlagen keinen hohen Effort braucht.
|
||||||
|
|
||||||
**In Workflows** bekommt jeder `agent()`-Aufruf `model`. Den Effort liefert mit `agentType` (`planer`, `pruefer`, `executer`, `recherche`) dessen Frontmatter; ein `effort`-Parameter daneben schlägt das Frontmatter und steht nur für eine bewusste Abweichung. Ohne `agentType` erbt der Agent die Sitzung, dort wird der Effort immer mitgegeben. Plankritik, Executer-Welle und Prüfung liegen als gespeicherte Workflows unter `.claude/workflows/` (`plankritik`, `executer-welle`, `pruefung`); sie setzen genau das um und halten Opus bei höchstens zwei gleichzeitig. Was sie darüber hinaus tun:
|
**In Workflows** bekommt jeder `agent()`-Aufruf `model`. Den Effort liefert mit `agentType` (`planer`, `pruefer`, `executer`, `recherche`) dessen Frontmatter; ein `effort`-Parameter daneben schlägt das Frontmatter und steht nur für eine bewusste Abweichung. Ohne `agentType` erbt der Agent die Sitzung, dort wird der Effort immer mitgegeben. Gespeicherte Workflows unter `.claude/workflows/`: `plankritik`, `executer-welle`, `pruefung`, `welle-mit-pruefung`, `planung` für die Erstplanung und `verbrauch` für die Ablesung des Planverbrauchs. Sie setzen genau das um; wie viele Opus-Agenten sie gleichzeitig starten, regelt „Verbrauch und Opus-Plätze" unten. Was sie darüber hinaus tun:
|
||||||
|
|
||||||
- `plankritik` baut auf Wunsch die Kopie mit angewendetem Plan selbst (`materialisieren` mit Ausschlüssen) und lässt einen billigen Hilfsagenten die Planwerkzeuge fahren; die Linsen bekommen die Rohausgabe. Danach fasst sie doppelte Befunde zusammen und lässt jeden wichtigen oder blockierenden von einem zweiten `pruefer` zu widerlegen versuchen: „bestätigt" nur mit eigener Reproduktion, „widerlegt" nur mit Gegenbeleg, sonst „offen". Das filtert Fehlalarme, bevor du den Plan korrigierst, und kostet je Befund einen Opus-Lauf.
|
- `plankritik` baut auf Wunsch die Kopie mit angewendetem Plan selbst (`materialisieren` mit Ausschlüssen) und lässt einen billigen Hilfsagenten die Planwerkzeuge fahren; die Linsen bekommen die Rohausgabe. Danach fasst sie doppelte Befunde zusammen und lässt jeden wichtigen oder blockierenden von einem zweiten `pruefer` zu widerlegen versuchen: „bestätigt" nur mit eigener Reproduktion, „widerlegt" nur mit Gegenbeleg, sonst „offen". Das filtert Fehlalarme, bevor du den Plan korrigierst, und kostet je Befund einen Opus-Lauf.
|
||||||
- `welle-mit-pruefung` fasst Schritt 3 und 4 zusammen: Kopie aus dem Stand **vor** der Welle, `executer-welle`, Byte-Vergleich und Suite durch den Hilfsagenten, dann `pruefung` mit diesen Rohbelegen. Ist die Welle unvollständig, prüft er nicht und sagt das.
|
- `welle-mit-pruefung` fasst Schritt 3 und 4 zusammen: Kopie aus dem Stand **vor** der Welle, `executer-welle`, Byte-Vergleich und Suite durch den Hilfsagenten, dann `pruefung` mit diesen Rohbelegen. Ist die Welle unvollständig, prüft er nicht und sagt das.
|
||||||
- Eine selbst gebaute Kopie verlangt eine nicht leere Liste von Ausschlüssen (`ohne`), gedacht für `.git` und Geheimnisdateien; fehlt sie oder ist sie leer, bricht der Workflow vor dem ersten Agenten ab. Ob die Liste die Geheimnisse wirklich trifft, sieht er nicht — das bleibt deine Sorgfalt. Die Kopie bleibt im Temp liegen, der Rückgabewert nennt den Ordner (`temp_ordner`).
|
- Eine selbst gebaute Kopie verlangt eine nicht leere Liste von Ausschlüssen (`ohne`), gedacht für `.git` und Geheimnisdateien; fehlt sie oder ist sie leer, bricht der Workflow vor dem ersten Agenten ab. Ob die Liste die Geheimnisse wirklich trifft, sieht er nicht — das bleibt deine Sorgfalt. Die Kopie bleibt im Temp liegen, der Rückgabewert nennt den Ordner (`temp_ordner`).
|
||||||
- Scheitert in `welle-mit-pruefung` ein verschachtelter Schritt, kommen Welle, Kopie und Vergleich trotzdem zurück, der Fehler steht unter `fehler`.
|
- Scheitert in `welle-mit-pruefung` ein verschachtelter Schritt, kommen Welle, Kopie und Vergleich trotzdem zurück, der Fehler steht unter `fehler`. Mit `bis: "vergleich"` endet er vor der Prüfung (Schritt 4).
|
||||||
|
- `plankritik` fährt ihre Linsen mit so vielen Plätzen, wie `verbrauch` erlaubt, die Widerlegung mit höchstens zwei. Die Aufträge verbieten Tests im echten Projektbaum; Mutanten entstehen in einer eigenen Kopie der Kopie.
|
||||||
|
- `planung` startet je Task einen `planer`, sobald die Planer seiner Voraussetzungen fertig sind, statt paarweise nach Wellen. Das spart Wartezeit ohne einen Opus-Agenten mehr: gerechnet 19 Minuten bei der Hub-Planung mit 38 Tasks. Außerdem liest jeder Planer die Taskdateien seiner Voraussetzungen, bevor er einen Namen daraus übernimmt. Gebremst oder abgebrochen kommen die offenen Tasks zurück; nachgeholt wird in einem neuen Lauf mit `erledigt`, nicht über Resume. Die Plankritik läuft danach als eigener Workflow.
|
||||||
|
|
||||||
Deine eigene Gegenprüfung aus Schritt 4 ersetzt keiner davon; sie wird nur billiger, weil die Rohbelege schon da sind. Recherche und Sammeln `medium` — gemessen reichte fürs Nachschlagen sogar `low`, `medium` ist die Reserve für Recherche mit Synthese oder Webquellen, die der Prüfstand nicht hatte. Reine Abschrift `low` (gleich treu wie `high`, halbe Laufzeit), Urteil und Plan `high`. Höchstens zwei Opus-Agenten gleichzeitig: Am 2026-09-20 riss das Wochenkontingent bei neun parallelen Opus-Agenten, und alles stand zwei Stunden. Wo das richtige Ergebnis feststeht, wertet ein Muster im Skript aus statt eines Richter-Agenten.
|
Deine eigene Gegenprüfung aus Schritt 4 ersetzt keiner davon; sie wird nur billiger, weil die Rohbelege schon da sind. Recherche und Sammeln `medium` — gemessen reichte fürs Nachschlagen sogar `low`, `medium` ist die Reserve für Recherche mit Synthese oder Webquellen, die der Prüfstand nicht hatte. Reine Abschrift `low` (gleich treu wie `high`, halbe Laufzeit), Urteil und Plan `high`. Wie viele Opus-Agenten gleichzeitig laufen, regelt „Verbrauch und Opus-Plätze" unten. Wo das richtige Ergebnis feststeht, wertet ein Muster im Skript aus statt eines Richter-Agenten.
|
||||||
|
|
||||||
**Bei Sonnet-Agenten treibt das Lesen die Kosten, nicht der Effort.** In den sechs Sonnet-Recherche-Läufen machte das Cache-Schreiben der gelesenen Dateien je Lauf ein Drittel bis gut 70 Prozent der Kosten aus, im Mittel gut die Hälfte; teurer oder billiger nach Effort sortierten sich die Läufe nicht. Gezielt lesen — `Grep` mit Zeilennummern, `Read` mit `offset` und `limit` — spart dort mehr als eine niedrigere Stufe. Bei Opus-Agenten dominiert die Ausgabe einschließlich Thinking, und genau die steuert der Effort.
|
**Bei Sonnet-Agenten treibt das Lesen die Kosten, nicht der Effort.** In den sechs Sonnet-Recherche-Läufen machte das Cache-Schreiben der gelesenen Dateien je Lauf ein Drittel bis gut 70 Prozent der Kosten aus, im Mittel gut die Hälfte; teurer oder billiger nach Effort sortierten sich die Läufe nicht. Gezielt lesen — `Grep` mit Zeilennummern, `Read` mit `offset` und `limit` — spart dort mehr als eine niedrigere Stufe. Bei Opus-Agenten dominiert die Ausgabe einschließlich Thinking, und genau die steuert der Effort.
|
||||||
|
|
||||||
@@ -246,8 +253,27 @@ Deine eigene Gegenprüfung aus Schritt 4 ersetzt keiner davon; sie wird nur bill
|
|||||||
|
|
||||||
**Die Escape-Folge für U+2028 hat jedes Modell verloren.** Alle acht Abschrift-Läufe, Haiku wie Sonnet in jeder Stufe, schrieben sie als echtes Zeichen in die Datei. Alle acht Berichte nannten die Datei trotzdem „byte-genau" übernommen, die Tests blieben grün. Das liegt an der Schreibschicht, nicht am Modell. Der Byte-Vergleich nach jeder Welle bleibt Pflicht, egal mit welchen Parametern.
|
**Die Escape-Folge für U+2028 hat jedes Modell verloren.** Alle acht Abschrift-Läufe, Haiku wie Sonnet in jeder Stufe, schrieben sie als echtes Zeichen in die Datei. Alle acht Berichte nannten die Datei trotzdem „byte-genau" übernommen, die Tests blieben grün. Das liegt an der Schreibschicht, nicht am Modell. Der Byte-Vergleich nach jeder Welle bleibt Pflicht, egal mit welchen Parametern.
|
||||||
|
|
||||||
|
### Verbrauch und Opus-Plätze
|
||||||
|
|
||||||
|
**Wie viele Opus-Agenten gleichzeitig laufen, richtet sich nach dem Verbrauch, nicht nach einer festen Zahl** (Pascals Freigabe vom 2026-09-24). Vor jeder Opus-Staffel liest der Workflow `verbrauch` über `get_usage` das 5-Stunden-Fenster und die Woche ab. Er senkt die gewünschten Plätze bei steigendem Verbrauch und gibt bei hohem keinen mehr frei; mehr als vier gibt es nie. Ohne Platz startet die Phase nicht, und der Workflow kommt mit `status: gebremst` zurück. Ein gebremster Lauf wird nach dem Zurücksetzen des Fensters neu gestartet, nicht über Resume, sonst käme die Ablesung aus dem Zwischenspeicher. Die Stufen stehen nur in `.claude/workflows/verbrauch.js`.
|
||||||
|
|
||||||
|
Warum keine feste Zwei mehr: Der Vorfall vom 2026-09-20 war die Wochengrenze aller Modelle (`seven_day`), und alle 18 Opus-Agenten dieser Nacht liefen mit effort `max`. Häufiger reißt das 5-Stunden-Fenster, 16 von 19 typisierten Abbrüchen seit dem 22.08. Eine feste Menge Agenten verbraucht parallel dieselben Tokens wie nacheinander, nur schneller. Die Zwei hat also nur die Rate gebremst, und sie galt je Workflow, nicht über zwei gleichzeitige Sitzungen hinweg. Die Ablesung ist kontoweit.
|
||||||
|
|
||||||
|
Wo das gilt:
|
||||||
|
|
||||||
|
- **`plankritik`:** Die Linsen laufen nach Verbrauch. Die dritte Linse wartete vorher im Mittel 14,5 Minuten auf einen Platz (fünf echte Pläne). Die Widerlegung bleibt bei höchstens zwei.
|
||||||
|
- **`planung`:** Vorgegeben sind zwei Plätze, nach Verbrauch gesenkt und nach je vier fertigen Planern neu gelesen.
|
||||||
|
- **Ad-hoc-Workflows mit vielen Opus-Agenten** (Reviews, Erstplanungen): Vor jeder Staffel `workflow({ scriptPath: "D:/Claude/.claude/workflows/verbrauch.js" }, { anlass, wunsch })` aufrufen und dessen `plaetze` nehmen, bis zur Eichung mit `wunsch` zwei. Ein Kind eines Workflows kann das nicht, dort gilt zwei.
|
||||||
|
- **Opus-Agenten über das Agent-Werkzeug:** Vorher selbst `get_usage` lesen; bei hohem Verbrauch weniger starten, höchstens zwei. Die Stufen stehen in `verbrauch.js`.
|
||||||
|
- **`pruefung` und `welle-mit-pruefung`** lesen nur ab, für die Messreihe. Sie bremsen nicht.
|
||||||
|
- **Ohne `get_usage`**, also außerhalb der Desktop-App, gilt zwei.
|
||||||
|
|
||||||
|
**Stufe 3 kommt erst nach der Eichung.** Drei oder vier Opus-Plätze für Reviews und Erstplanungen (`planung` mit `plaetze`, Ad-hoc-Fan-outs mit höherem `wunsch`) gibt es erst, wenn die Stufen in `verbrauch.js` an der Messreihe nachgestellt sind; bis dahin laufen drei gleichzeitig nur in den Linsen der Plankritik. **Die Stufen sind vorläufig** und stehen nur in `verbrauch.js`. Ein erster Messpunkt vom 2026-09-24, Plan laut App „Pro": Ein Workflow mit 21 Opus-Agenten auf `high` hob zusammen mit dem übrigen Verbrauch der Sitzung das 5-Stunden-Fenster von 7 auf 43 % und die Woche von 2 auf 12 %. Das sind grob 1,5 bis 2 % des Fensters je Opus-Lauf. Nach etwa einer Woche wertet `.vault-tools/workflow-pruefung/verbrauch_reihe.py` die Ablesungen aus, danach werden die Stufen nachgestellt. Werkzeuge und Belege: `.vault-tools/workflow-pruefung/LIESMICH.md`. Ein neuer oder geänderter Workflow unter `.claude/workflows/` wird dort mit `faelle_workflows.mjs` gegen die Attrappe gefahren, bevor er ins Verzeichnis kommt.
|
||||||
|
|
||||||
**Bei parallelem Einsatz zusätzlich:**
|
**Bei parallelem Einsatz zusätzlich:**
|
||||||
|
|
||||||
|
- **Tests nur in der Kopie.** Laufen mehrere Prüfer gleichzeitig, fährt keiner Tests im echten Projektbaum; Mutanten entstehen in einer eigenen Kopie der Kopie. Im echten Baum fährt die Suite immer nur einer.
|
||||||
|
|
||||||
- **Disjunkte Zieldateien.** Jeder Subagent bekommt die exakten Pfade genannt, die er anlegen darf, und die Anweisung, nichts anderes anzufassen. Gemeinsam benötigte Dateien (Paket-`__init__.py`, `.gitignore`) legt der Koordinator vorher an.
|
- **Disjunkte Zieldateien.** Jeder Subagent bekommt die exakten Pfade genannt, die er anlegen darf, und die Anweisung, nichts anderes anzufassen. Gemeinsam benötigte Dateien (Paket-`__init__.py`, `.gitignore`) legt der Koordinator vorher an.
|
||||||
- **Subagenten committen nicht.** Mehrere gleichzeitige `git commit` im selben Repository kollidieren am Index-Lock. Der Koordinator committet nach dem Rücklauf, je Task einzeln.
|
- **Subagenten committen nicht.** Mehrere gleichzeitige `git commit` im selben Repository kollidieren am Index-Lock. Der Koordinator committet nach dem Rücklauf, je Task einzeln.
|
||||||
- **`git status` in der Taskdatei nennt die eigenen Zieldateien.** „Genau diese Dateien im Baum und sonst nichts" scheitert in einer parallelen Welle an den Dateien der Nachbarn. In die Taskdatei gehört `git status --porcelain -- <Zieldateien>`; die Mengenprüfung über die ganze Welle macht der Koordinator nach dem Rücklauf.
|
- **`git status` in der Taskdatei nennt die eigenen Zieldateien.** „Genau diese Dateien im Baum und sonst nichts" scheitert in einer parallelen Welle an den Dateien der Nachbarn. In die Taskdatei gehört `git status --porcelain -- <Zieldateien>`; die Mengenprüfung über die ganze Welle macht der Koordinator nach dem Rücklauf.
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ Rollen sind fest auf Modelle verteilt. **Beim Starten eines Subagenten immer das
|
|||||||
|
|
||||||
Die Aliasse `opus` und `sonnet` lösen in Claude Code 2.1.280 auf `claude-opus-5-5` bzw. `claude-sonnet-5` auf (Modellkatalog im Programm, nachgelesen 2026-09-23). Der eingebaute Agent `Explore` lädt diese Datei nicht und kennt die Vault-Regeln deshalb nicht — nur für reine Dateisuche; `general-purpose` erbt den Sitzungs-Effort und ist durch `recherche` ersetzt. Wann Haiku statt Sonnet, die Belege dazu und die Zuordnung der Superpowers-Skills: [[AI/Regelwerk/codeworkflow|codeworkflow.md]].
|
Die Aliasse `opus` und `sonnet` lösen in Claude Code 2.1.280 auf `claude-opus-5-5` bzw. `claude-sonnet-5` auf (Modellkatalog im Programm, nachgelesen 2026-09-23). Der eingebaute Agent `Explore` lädt diese Datei nicht und kennt die Vault-Regeln deshalb nicht — nur für reine Dateisuche; `general-purpose` erbt den Sitzungs-Effort und ist durch `recherche` ersetzt. Wann Haiku statt Sonnet, die Belege dazu und die Zuordnung der Superpowers-Skills: [[AI/Regelwerk/codeworkflow|codeworkflow.md]].
|
||||||
|
|
||||||
**Ein Subagent ohne eigenen Effort erbt den der Hauptsitzung.** Läuft sie auf `xhigh` oder `max`, kostet ein Sonnet-Agent laut Katalog das 2,4- bzw. 5,6-Fache von `high`. Deshalb trägt jede Agentendatei `effort:`. In Workflows bekommt jeder `agent()`-Aufruf `model`; den Effort liefert mit `agentType` dessen Frontmatter, ohne `agentType` wird er mitgegeben, und ein `effort`-Parameter neben `agentType` steht nur für eine bewusste Abweichung. Höchstens zwei Opus-Agenten gleichzeitig. Für Plankritik, Executer-Welle und Prüfung gibt es gespeicherte Workflows unter `.claude/workflows/`, in denen das eingebaut ist, dazu `welle-mit-pruefung` für Schritt 3 und 4 in einem Aufruf. Eine geänderte Agentendatei oder ein neuer gespeicherter Workflow wirkt in der laufenden Sitzung nicht sofort, sondern erst nach einer Weile (am 2026-09-23 knapp eine Stunde) oder in der nächsten Sitzung. Ob ein Effort greift, zeigt nur das Feld `effort` im Transkript; ein noch unbekannter Workflow geht so lange über `scriptPath`. Messung und Begründung je Rolle: [[AI/Regelwerk/codeworkflow|codeworkflow.md]], „Effort je Rolle".
|
**Ein Subagent ohne eigenen Effort erbt den der Hauptsitzung.** Läuft sie auf `xhigh` oder `max`, kostet ein Sonnet-Agent laut Katalog das 2,4- bzw. 5,6-Fache von `high`. Deshalb trägt jede Agentendatei `effort:`. In Workflows bekommt jeder `agent()`-Aufruf `model`; den Effort liefert mit `agentType` dessen Frontmatter, ohne `agentType` wird er mitgegeben, und ein `effort`-Parameter neben `agentType` steht nur für eine bewusste Abweichung. **Wie viele Opus-Agenten gleichzeitig laufen, entscheidet der Verbrauch:** Vor jeder Opus-Staffel liest der Workflow `verbrauch` das 5-Stunden-Fenster und die Woche ab und senkt die Plätze bei hohem Verbrauch bis auf keinen. Bis zur Eichung der Stufen laufen drei Opus-Agenten gleichzeitig nur in den Linsen der Plankritik, sonst höchstens zwei; danach bis vier für Reviews und Erstplanung. Ohne Ablesung, also außerhalb der Desktop-App oder als Kind eines Workflows, gilt die Zwei; wer Opus-Agenten über das Agent-Werkzeug startet, liest vorher selbst `get_usage`. Für Plankritik, Executer-Welle, Prüfung und Erstplanung gibt es gespeicherte Workflows unter `.claude/workflows/`, in denen das eingebaut ist (`plankritik`, `executer-welle`, `pruefung`, `planung`), dazu `welle-mit-pruefung` für Schritt 3 und 4 in einem Aufruf. Eine geänderte Agentendatei oder ein neuer gespeicherter Workflow wirkt in der laufenden Sitzung nicht sofort, sondern erst nach einer Weile (am 2026-09-23 knapp eine Stunde, am 2026-09-24 gut 20 Minuten) oder in der nächsten Sitzung. Ob ein Effort greift, zeigt nur das Feld `effort` im Transkript; ein noch unbekannter Workflow geht so lange über `scriptPath`. Messung und Begründung je Rolle: [[AI/Regelwerk/codeworkflow|codeworkflow.md]], „Effort je Rolle"; Stufen und Belege zum Verbrauch dort unter „Verbrauch und Opus-Plätze".
|
||||||
|
|
||||||
## Regelwerk (ausgelagert, nur bei Bedarf laden)
|
## Regelwerk (ausgelagert, nur bei Bedarf laden)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user