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
67d674cd0b
commit
ea278dc24f
46 files changed
+8403
-80
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user