--- 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.