- .claude/agents/planer.md - .claude/settings.json - .claude/workflows/executer-welle.js - .claude/workflows/plankritik.js - .claude/workflows/planung.js - .claude/workflows/quorum.js - .claude/workflows/verbrauch.js - .claude/workflows/welle-mit-pruefung.js - .vault-tools/shell-wache/ENTWURF.md - .vault-tools/shell-wache/docs/code/shell_wache.md - .vault-tools/shell-wache/docs/plans/2026-10-30-shell-wache/00-uebersicht.md - .vault-tools/shell-wache/docs/plans/2026-10-30-shell-wache/task-01-shell-wache.md - … und 34 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
216 lines
12 KiB
JavaScript
216 lines
12 KiB
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, effort?, zusatz?}. effort überschreibt den Effort aller Planer bewusst, etwa für die Messung medium gegen high; ohne effort gilt das Frontmatter von planer.md. plaetze ist der Wunsch, verbrauch.js senkt ihn; ohne plaetze gilt zwei; drei oder vier sind seit der Eichung der Stufen am 2026-10-30 frei. 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; args.effort überschreibt ihn nur bewusst (Messplan A2). 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 über plaetze hat Pascal nach der Eichung am 2026-10-30 freigegeben.
|
|
const MODELLE = ['haiku', 'sonnet', 'opus']
|
|
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
|
|
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.effort !== undefined && !EFFORTS.includes(a.effort)) throw new Error('planung: effort "' + a.effort + '" ungültig')
|
|
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' }
|
|
if (args.effort) opts.effort = args.effort
|
|
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,
|
|
effort: args.effort || '(Frontmatter)',
|
|
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.',
|
|
}
|