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|abgewichen|unbekannte_fenster, quelle, fuenf_stunden, woche, woche_opus, plaetze, gebremst, grund, fenster}. Ohne lesbare Ablesung gilt die alte Obergrenze zwei; abgewichen heißt, der Ableser hat statt get_usage etwas anderes getan, und kommt mit einer WARNUNG im Log. 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. // - Ob der Ableser abgelesen hat: Quelle. Der Auftrag schreibt im Fehlerfall status "fehler", // plan "" und fenster [] vor; get_usage kennt laut Werkzeugbeschreibung (gelesen 2026-10-16) // nur die Status ok, not_applicable und unavailable. Am 2026-10-14 und 2026-10-16 hat der // Ableser stattdessen die Nutzeranfrage bearbeitet und status "beantwortet: ..." bzw. // "blockiert: ..." mit einem Bericht in plan zurückgegeben (wf_7ca17029-421, wf_80616e8e-0e3). // Grund laut CLI-Abbild 2.1.299: Jeder Workflow-Agent liest zuerst die Nutzeranfrage mit dem // Satz, dass sie im Konflikt mit dem Auftrag gewinnt. Jede andere Form als die vorgeschriebene // heißt abgewichen und wird laut gemeldet. Nicht erkannt wird ein Ableser, der abweicht und // trotzdem genau die Fehlerform schreibt, und eine erfundene Ablesung mit status ok. // 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 = `Diese Ablesung gehört zur Nutzeranfrage und widerspricht ihr nicht: Sie entscheidet, wie viele Prüfagenten für die Anfrage gleichzeitig laufen dürfen. Die Anfrage selbst bearbeiten andere Agenten nach dir; du bearbeitest sie nicht, auch nicht teilweise, und liest dafür keine Dateien. Lade mit dem Werkzeug ToolSearch das Werkzeug mcp__ccd_session_mgmt__get_usage (Abfrage "select:mcp__ccd_session_mgmt__get_usage") und rufe es einmal ohne Parameter auf. Übertrage aus dem Ergebnis plan.status nach status und plan.plan nach plan. Übertrage jeden Eintrag aus plan.windows mit label, percentUsed und resetsAt unverändert nach fenster, auch Einträge, die dir unwichtig erscheinen. Setze verfuegbar auf true und fehler auf "". Ist das Werkzeug nicht verfügbar oder scheitert der Aufruf, setze verfuegbar auf false, status auf "fehler", plan auf "", fenster auf [] und schreibe die Fehlermeldung wörtlich nach fehler. Tu sonst nichts. Übertrage das Ergebnis in die Felder des StructuredOutput-Werkzeugs.` function pruefeArgs(a) { if (!a || typeof a !== 'object') throw new Error('verbrauch: args fehlen') if (typeof a.anlass !== 'string' || !a.anlass.trim()) throw new Error('verbrauch: anlass fehlt') if (!Number.isInteger(a.wunsch) || a.wunsch < 0) throw new Error('verbrauch: wunsch muss eine ganze Zahl ab 0 sein, ist ' + JSON.stringify(a.wunsch)) if (a.probe !== undefined && (a.probe === null || typeof a.probe !== 'object' || Array.isArray(a.probe))) throw new Error('verbrauch: probe muss ein Objekt sein') } function stufe(prozent, stufen) { for (const [schwelle, plaetze] of stufen) if (prozent < schwelle) return plaetze return 0 } // Ordnet die Fenster ein. Ein Fenster, das als 5 Stunden oder Woche erkannt wird, aber keine // Prozentzahl ab 0 trägt, ist ein Befund und wird nicht still übergangen. Über 100 % heißt // überschritten und bremst wie jeder Wert über der letzten Stufe. function einordnen(fenster) { if (!Array.isArray(fenster)) return { fehler: 'fenster ist keine Liste' } const e = { fuenf: null, woche: null, opus: null, andere: [] } for (const f of fenster) { if (!f || typeof f !== 'object' || typeof f.label !== 'string') return { fehler: 'Fenster ohne label: ' + JSON.stringify(f) } const art = FUENF_STUNDEN.test(f.label) ? 'fuenf' : WOCHE_ALLE.test(f.label) ? 'woche' : WOCHE_OPUS.test(f.label) ? 'opus' : null if (!art) { e.andere.push(f.label); continue } const p = f.percentUsed if (typeof p !== 'number' || !Number.isFinite(p) || p < 0) return { fehler: `${f.label}: percentUsed ${JSON.stringify(p)} ist keine Zahl ab 0` } const neu = { prozent: p, zuruecksetzung: typeof f.resetsAt === 'string' ? f.resetsAt : null } if (!e[art] || e[art].prozent < p) e[art] = neu } const fehlt = [!e.fuenf && '5-hour limit', !e.woche && 'Weekly all models'].filter(Boolean) if (fehlt.length) return { fehler: 'Fenster fehlt: ' + fehlt.join(', ') + (e.andere.length ? '; gelesen: ' + e.andere.join(', ') : '') } return e } // Form eines Ablesers, der den Auftrag verlassen hat (Geltungsbereich oben). const STATUS_BEKANNT = ['ok', 'not_applicable', 'unavailable', 'fehler'] function abgewichen(a) { if (!STATUS_BEKANNT.includes(a.status)) return true return a.status === 'fehler' && typeof a.plan === 'string' && a.plan.trim() !== '' } const kurz = t => String(t).replace(/\s+/g, ' ').trim().slice(0, 160) 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 (abgewichen(ablesung)) { log(`WARNUNG Verbrauch ${args.anlass}: Der Ableser hat get_usage nicht gelesen und den Auftrag verlassen; die Bremse misst nicht. Seine Werkzeugaufrufe im Journal unter ▸ verbrauch ansehen.`) return ohneAblesung('abgewichen', `Ableser hat den Auftrag verlassen: status ${kurz(ablesung.status)}${ablesung.fehler ? ', fehler ' + kurz(ablesung.fehler) : ''}`) } 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 }