Erste Fassung: Claude-Werkzeuge aus dem Vault
.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
fa1c26b407
137 files changed
+43926
No files matched your search
@@ -0,0 +1,658 @@
|
||||
export const meta = {
|
||||
name: 'quorum',
|
||||
description: 'Quorum am Meilenstein: Opus, Sonnet und Codex (Ersatz Haiku) prüfen unabhängig denselben Unterschied seit dem letzten Quorum; was zwei melden, gilt, Einzelbefunde prüft Sonnet nach',
|
||||
whenToUse: 'Vor dem Einspielen, vor Merge und Push einer neuen Minor- oder Major-Version und vor der Übergabe eines Kundendokuments (AI/Regelwerk/codeworkflow.md, "Quorum am Meilenstein"). Args: {anlass: "einspielen"|"version"|"kundendokument", projekt?, basis?, ordner?, dateien?, uebersicht?, kontext?: [pfade], suite?, suite_datei?, codex?: false, codex_runden?: 1..6}. Einspielen und version brauchen projekt (Wurzel eines Git-Repos, sauberer Arbeitsbaum), das erste Quorum dort zusätzlich basis. suite nur für eine Suite, die in neun Minuten durchläuft und keine unversionierten Dateien schreibt; sonst die Suite selbst fahren und suite_datei nennen. Kundendokument braucht ordner (im Vault unter D:/Claude/), dateien (Pfade relativ zu ordner) und uebersicht. Rückgabe mit status fertig, leer, gebremst oder unvollstaendig, urteil frei, nicht_frei oder kein_urteil, dazu bericht_pfad, bericht_md, paket und ablegen_befehl: bericht_md als bericht.md im Ordner paket speichern und ablegen_befehl fahren, der den Stand aus paket.json einsetzt. Einzelheiten im Kommentar unter meta.',
|
||||
phases: [
|
||||
{ title: 'Paket', detail: 'Hilfsagent baut das Prüfpaket mit quorum_werkzeug.py' },
|
||||
{ title: 'Stimmen', detail: 'Opus und Sonnet als quorum-stimme, Codex über Hilfsagenten, Ersatz Haiku' },
|
||||
{ title: 'Zusammenführen', detail: 'ein Sonnet-Agent gruppiert, das Skript zählt die Stimmen' },
|
||||
{ title: 'Nachprüfen', detail: 'ein Sonnet-Agent prüft Einzelbefunde der Schwere blockierend und wichtig' },
|
||||
],
|
||||
}
|
||||
|
||||
// Diese Datei trägt LF (Erinnerung gespeicherter-workflow-per-name-scheitert). Regel und
|
||||
// Auslöser stehen in AI/Regelwerk/codeworkflow.md, "Quorum am Meilenstein"; hier steht nur, wie
|
||||
// der Ablauf gebaut ist. Plan: .vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/.
|
||||
//
|
||||
// Paket: Ein Workflow-Skript hat weder Dateisystem noch Shell. Das Prüfpaket baut deshalb ein
|
||||
// Hilfsagent (sonnet, low), der genau einen Befehl fährt: quorum_werkzeug.py paket. Das Skript
|
||||
// entscheidet, ob der Unterschied leer ist; der Hilfsagent überträgt nur die Ausgabe.
|
||||
// Die Zeile QUORUM_PAKET trägt nur Felder fester Größe. Dateiliste und Stand-Liste bleiben in
|
||||
// paket.json; den Stand setzt quorum_werkzeug.py ablegen beim Ablegen des Berichts aus paket.json
|
||||
// ein, der Bericht trägt bis dahin die Zeile "stand: PAKET". So schreibt kein Agent den Stand ab.
|
||||
// Stimmen: Opus und Sonnet laufen als agentType quorum-stimme, Effort aus deren Frontmatter.
|
||||
// Vor der Opus-Stimme liest verbrauch.js mit wunsch 1 ab; gebremst heißt, keine Stimme startet.
|
||||
// Codex über das Plugin codex 1.0.6, nur lesend; cc heißt scripts/codex-companion.mjs:
|
||||
// - task ohne --write fährt read-only (cc 491), liest den Auftrag aus --prompt-file (cc 643-645)
|
||||
// und braucht kein Git (cc 461-495). review und adversarial-review brauchen Git (cc 360),
|
||||
// adversarial-review hat ein festes Ausgabeschema ohne Feld für den Lesebeleg (cc 415).
|
||||
// - Im Hintergrund gestartet (cc 788-805) wartet je ein Hilfsagent mit status <job> --wait
|
||||
// höchstens WARTEN_MS (cc 318-334, 891-897), weil das Bash-Werkzeug nach zehn Minuten endet.
|
||||
// result <job> --json liefert Job und Ergebnis (cc 910-926); quorum_werkzeug.py codex zählt
|
||||
// "usage limit" im Log des Jobs und zieht die Antwort heraus.
|
||||
// - Gültig ist die Codex-Stimme nur mit gelesenem Status vor dem Lauf, Job completed, gelesenem
|
||||
// Log ohne "usage limit" und einem Lesebeleg mit der Lesemarke aus paket.md. Sonst Haiku.
|
||||
// - Die Codex-Hilfsagenten laufen auf sonnet mit low: feste Befehle, Ausgaben übertragen. Haiku
|
||||
// nicht, weil Befundtexte typografische Zeichen tragen können, die Haiku still ersetzt.
|
||||
// Zusammenführen: Ein Sonnet-Agent ohne agentType gruppiert nur belegte Befunde; das Skript teilt
|
||||
// jede Gruppe nach Datei und Zeilen in zusammenhängende Teilgruppen (Funktion teilgruppen) und
|
||||
// zählt die Stimmen. Mehrheit heißt zwei verschiedene Stimmen, die Schwere der Gruppe ist die
|
||||
// höchste ihrer Mitglieder. quorum-stimme passt hier nicht, weil dessen Auftragsarten den Auftrag
|
||||
// verfälschen; dass der Agent keine Datei liest, sagt nur sein Auftrag, ein Fall sichert den Satz.
|
||||
// Nachprüfen: quorum-stimme auf sonnet für Gruppen ohne Mehrheit mit Schwere blockierend oder
|
||||
// wichtig. Ein blockierender Einzelbefund ohne Urteil sperrt wie ein bestätigter.
|
||||
// Urteil: frei heißt kein bestätigter und kein ungeprüfter blockierender Befund mit Beleg; ein
|
||||
// blockierender Befund ohne Beleg sperrt nicht (Plan, Offener Punkt 8), der Grund nennt ihn. Mit weniger als
|
||||
// zwei abgegebenen Stimmen gibt es kein Urteil und keinen Bericht, die Basis bleibt die alte.
|
||||
// Laufzeit: Date.now() und new Date() gibt es im Laufzeitsystem nicht. Die Codex-Laufzeit kommt
|
||||
// aus dem Job, die der Claude-Stimmen steht im Bericht als nicht gemessen.
|
||||
// Bericht: Freitext aus Stimmen, Zusammenführen und Nachprüfung geht nur durch klartext() in
|
||||
// bericht_md. Ein zitierter Wikilink, eine Einbettung oder ein Tag würde im Vault sonst lebendig;
|
||||
// Hub-Zeile und Frontmatter baut der Workflow selbst, sie bleiben echte Links bzw. Felder.
|
||||
const ANLAESSE = ['einspielen', 'version', 'kundendokument']
|
||||
const RANG = { blockierend: 0, wichtig: 1, hinweis: 2 }
|
||||
const KUERZEL = { opus: 'O', sonnet: 'S', codex: 'C', haiku: 'H' }
|
||||
const NAMEN = { opus: 'Opus', sonnet: 'Sonnet', codex: 'Codex', haiku: 'Haiku' }
|
||||
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
|
||||
const WERKZEUG = 'D:/Claude/.vault-tools/quorum/quorum_werkzeug.py'
|
||||
const VAULT = 'D:/Claude/'
|
||||
const COMPANION = '"$HOME/.claude/plugins/cache/openai-codex/codex/1.0.6/scripts/codex-companion.mjs"'
|
||||
const CODEX_RUNDEN = 3
|
||||
const WARTEN_MS = 540000
|
||||
const OHNE_ABLESUNG = 2
|
||||
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
|
||||
const LANGE = 'Setze im Bash-Werkzeug den Parameter timeout auf 600000; der Befehl darf bis zu zehn Minuten laufen.'
|
||||
const PAKET_FELDER = ['ordner', 'anlass', 'art', 'wurzel', 'leer', 'basis', 'stand', 'anzahl_dateien', 'lesemarke', 'bericht_pfad', 'nummer', 'datum', 'vorher', 'suite', 'hinweise', 'paket_summe']
|
||||
const STATUS_TEXT = { mehrheit: 'Mehrheit', bestaetigt: 'bestätigt nach Nachprüfung', widerlegt: 'widerlegt nach Nachprüfung', nicht_geprueft: 'nicht geprüft', hinweis_ungeprueft: 'Hinweis, ungeprüft' }
|
||||
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
|
||||
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||
|
||||
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 BEFUND = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
datei: { type: 'string', description: 'Pfad relativ zur Wurzel aus dem Auftrag' },
|
||||
stelle: { type: 'string', description: 'Zeile, Zeilenbereich oder Abschnitt' },
|
||||
schwere: { type: 'string', enum: ['blockierend', 'wichtig', 'hinweis'] },
|
||||
aussage: { type: 'string' },
|
||||
beleg: { type: 'string', description: 'wörtlich zitierte Zeile oder Verweis auf das Paket; leer heißt unbelegt' },
|
||||
},
|
||||
required: ['datei', 'stelle', 'schwere', 'aussage', 'beleg'],
|
||||
}
|
||||
|
||||
const STIMME = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
lesemarke: { type: 'string', description: 'der Wert hinter Lesemarke: in paket.md' },
|
||||
befunde: { type: 'array', items: BEFUND },
|
||||
nicht_pruefbar: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['lesemarke', 'befunde', 'nicht_pruefbar'],
|
||||
}
|
||||
|
||||
const CODEX = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
exit_codex: { type: 'integer' },
|
||||
job_status: { type: 'string' },
|
||||
log_gelesen: { type: 'boolean' },
|
||||
usage_limit_treffer: { type: 'integer' },
|
||||
antwort_gefunden: { type: 'boolean' },
|
||||
dauer_s: { type: 'integer' },
|
||||
lesebeleg: { type: 'string' },
|
||||
befunde: { type: 'array', items: BEFUND },
|
||||
nicht_pruefbar: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['exit_codex', 'job_status', 'log_gelesen', 'usage_limit_treffer', 'antwort_gefunden', 'dauer_s', 'lesebeleg', 'befunde', 'nicht_pruefbar'],
|
||||
}
|
||||
|
||||
const GRUPPEN = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gruppen: { type: 'array', items: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string' } }, begruendung: { type: 'string' } }, required: ['ids', 'begruendung'] } },
|
||||
},
|
||||
required: ['gruppen'],
|
||||
}
|
||||
|
||||
const URTEILE = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
urteile: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, urteil: { type: 'string', enum: ['bestaetigt', 'widerlegt'] }, fundstelle: { type: 'string' }, begruendung: { type: 'string' } }, required: ['id', 'urteil', 'fundstelle', 'begruendung'] } },
|
||||
},
|
||||
required: ['urteile'],
|
||||
}
|
||||
|
||||
function pruefeArgs(a) {
|
||||
if (!a || typeof a !== 'object') throw new Error('quorum: args fehlen')
|
||||
if (!ANLAESSE.includes(a.anlass)) throw new Error('quorum: anlass muss einspielen, version oder kundendokument sein, ist ' + JSON.stringify(a.anlass))
|
||||
for (const k of ['projekt', 'basis', 'ordner', 'uebersicht', 'suite', 'suite_datei']) {
|
||||
if (a[k] !== undefined && (typeof a[k] !== 'string' || !a[k].trim())) throw new Error('quorum: ' + k + ' muss ein nicht leerer Text sein')
|
||||
}
|
||||
if (a.anlass === 'kundendokument') {
|
||||
const fehlt = ['ordner', 'uebersicht'].filter(k => a[k] === undefined)
|
||||
if (fehlt.length) throw new Error('quorum: args fehlen: ' + fehlt.join(', '))
|
||||
if (!Array.isArray(a.dateien) || !a.dateien.length || !a.dateien.every(x => typeof x === 'string' && x.trim())) throw new Error('quorum: dateien muss eine nicht leere Liste von Pfaden relativ zu ordner sein')
|
||||
if (new Set(a.dateien).size !== a.dateien.length) throw new Error('quorum: eine Datei steht doppelt in dateien')
|
||||
const fremd = ['projekt', 'basis', 'suite', 'suite_datei'].filter(k => a[k] !== undefined)
|
||||
if (fremd.length) throw new Error('quorum: ' + fremd.join(', ') + ' gilt nur für einspielen und version')
|
||||
if (vaultPfad(a.ordner) === null) throw new Error('quorum: ordner muss im Vault unter ' + VAULT + ' liegen; der Bericht verlinkt seine Übersicht mit vollem Vault-Pfad')
|
||||
} else {
|
||||
if (a.projekt === undefined) throw new Error('quorum: args fehlen: projekt')
|
||||
const fremd = ['ordner', 'dateien', 'uebersicht'].filter(k => a[k] !== undefined)
|
||||
if (fremd.length) throw new Error('quorum: ' + fremd.join(', ') + ' gilt nur für kundendokument')
|
||||
}
|
||||
if (a.suite !== undefined && a.suite_datei !== undefined) throw new Error('quorum: suite und suite_datei schließen sich aus')
|
||||
if (a.kontext !== undefined && !(Array.isArray(a.kontext) && a.kontext.every(x => typeof x === 'string' && x.trim()))) throw new Error('quorum: kontext muss eine Liste von Pfaden sein')
|
||||
if (a.codex !== undefined && typeof a.codex !== 'boolean') throw new Error('quorum: codex muss true oder false sein')
|
||||
if (a.codex_runden !== undefined && !(Number.isInteger(a.codex_runden) && a.codex_runden >= 1 && a.codex_runden <= 6)) throw new Error('quorum: codex_runden muss eine ganze Zahl von 1 bis 6 sein')
|
||||
}
|
||||
|
||||
function werkzeugAgent(label, phase, befehl, zweck, zusatz) {
|
||||
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}
|
||||
${zusatz ? '\n' + zusatz + '\n' : ''}
|
||||
Ä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 ausgabe(r) {
|
||||
return r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''
|
||||
}
|
||||
|
||||
function wurzel(a) {
|
||||
return a.anlass === 'kundendokument' ? a.ordner : a.projekt
|
||||
}
|
||||
|
||||
// Pfad relativ zur Vault-Wurzel, ohne Schrägstrich am Ende; null außerhalb des Vaults.
|
||||
function vaultPfad(ordner) {
|
||||
const o = String(ordner).replace(/\\/g, '/').replace(/\/+$/, '')
|
||||
return o.toLowerCase().startsWith(VAULT.toLowerCase()) ? o.slice(VAULT.length) : null
|
||||
}
|
||||
|
||||
const laufHinweise = []
|
||||
function hinweis(text) {
|
||||
laufHinweise.push(text)
|
||||
log(text)
|
||||
}
|
||||
|
||||
function paketBefehl(a) {
|
||||
const teile = [`${PY} ${q(WERKZEUG)} paket --anlass ${a.anlass} --ziel "$T"`]
|
||||
if (a.anlass === 'kundendokument') teile.push(`--ordner ${q(a.ordner)} --uebersicht ${q(a.uebersicht)} --dateien ${a.dateien.map(q).join(' ')}`)
|
||||
else teile.push(`--projekt ${q(a.projekt)}${a.basis ? ' --basis ' + q(a.basis) : ''}`)
|
||||
if (a.kontext && a.kontext.length) teile.push(`--kontext ${a.kontext.map(q).join(' ')}`)
|
||||
if (a.suite_datei) teile.push(`--suite-datei ${q(a.suite_datei)}`)
|
||||
if (a.suite) teile.push('--suite-datei "$T/suite.txt"')
|
||||
// Schreibt die Suite Dateien in den Arbeitsbaum, scheitert das Paket an "nicht sauber"; die
|
||||
// BEFUND-Zeile davor nennt dann die Suite als Ursache. GIT_OPTIONAL_LOCKS=0, damit status
|
||||
// .git/index nicht auffrischt und keinem git-Befehl des Koordinators das index.lock nimmt.
|
||||
const baum = `GIT_OPTIONAL_LOCKS=0 git -C ${q(a.projekt)} status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum'`
|
||||
const suite = a.suite ? `V=$(${baum}); (cd ${q(a.projekt)} && ${a.suite}) > "$T/suite.txt" 2>&1; echo "EXIT suite: $?" >> "$T/suite.txt"; [ "$V" = "$(${baum})" ] || echo "BEFUND: Die Suite hat den Arbeitsbaum verändert; suite_datei nutzen oder ihre Ausgaben in .gitignore aufnehmen"; ` : ''
|
||||
return `T=$(cygpath -m -l "$(mktemp -d)") && ${suite}${teile.join(' ')}; echo "EXIT paket: $?"`
|
||||
}
|
||||
|
||||
function lesePaket(r) {
|
||||
const aus = ausgabe(r)
|
||||
const m = /^QUORUM_PAKET (\{.*\})\s*$/m.exec(aus)
|
||||
if (!/EXIT paket: 0(?!\d)/.test(aus) || !m) {
|
||||
const zeilen = aus.split('\n').filter(z => /^(BEFUND|ABBRUCH|EXIT)/.test(z)).join('; ')
|
||||
return { fehler: 'Paket nicht gebaut: ' + (zeilen || 'Hilfsagent ohne verwertbare Ausgabe') }
|
||||
}
|
||||
let p = null
|
||||
try {
|
||||
p = JSON.parse(m[1])
|
||||
} catch (e) {
|
||||
return { fehler: 'QUORUM_PAKET ist kein JSON: ' + (e && e.message) }
|
||||
}
|
||||
const fehlt = PAKET_FELDER.filter(k => !p || typeof p !== 'object' || !(k in p))
|
||||
if (fehlt.length) return { fehler: 'QUORUM_PAKET ohne ' + fehlt.join(', ') }
|
||||
if (!/^[0-9a-f]{16}$/.test(p.lesemarke)) return { fehler: 'QUORUM_PAKET mit unbrauchbarer Lesemarke ' + JSON.stringify(p.lesemarke) }
|
||||
if (!/^[0-9a-f]{64}$/.test(p.paket_summe)) return { fehler: 'QUORUM_PAKET mit unbrauchbarer paket_summe ' + JSON.stringify(p.paket_summe) }
|
||||
if (!Number.isInteger(p.anzahl_dateien) || p.anzahl_dateien < 0) return { fehler: 'QUORUM_PAKET: anzahl_dateien muss eine ganze Zahl ab 0 sein, ist ' + JSON.stringify(p.anzahl_dateien) }
|
||||
if (!Array.isArray(p.hinweise)) return { fehler: 'QUORUM_PAKET: hinweise muss eine Liste sein' }
|
||||
return { paket: p }
|
||||
}
|
||||
|
||||
// Eine Hilfsfunktion ablesen() steht auch in plankritik.js und planung.js; 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: 'quorum:' + 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 stimmenAuftrag(a, p) {
|
||||
return `Auftragsart: Stimme.
|
||||
Prüfpaket: ${p.ordner}/paket.md
|
||||
Wurzel: ${wurzel(a)}
|
||||
Anlass: ${a.anlass}
|
||||
|
||||
Lies das Prüfpaket vollständig und prüfe, was seit der Basis dazukam, nach deiner Agentendefinition. Die anderen Stimmen kennst du nicht. Du änderst nichts und fährst keine Tests; das Suite-Ergebnis steht im Paket.
|
||||
Übertrage die Lesemarke aus dem Paket, deine Befunde und was du nicht prüfen konntest in die Felder des StructuredOutput-Werkzeugs.`
|
||||
}
|
||||
|
||||
function claudeStimme(e, p) {
|
||||
if (!e) return { abgegeben: false, grund: 'ohne Ergebnis', dauer_s: -1, befunde: [], nicht_pruefbar: [], lesemarke_ok: false }
|
||||
return { abgegeben: true, grund: '', dauer_s: -1, befunde: e.befunde, nicht_pruefbar: e.nicht_pruefbar, lesemarke_ok: String(e.lesemarke).includes(p.lesemarke) }
|
||||
}
|
||||
|
||||
function codexEndeAuftrag(a, p, job) {
|
||||
const datei = q(p.ordner + '/codex-ergebnis.json')
|
||||
const befehl = `node ${COMPANION} result ${q(job)} --json --cwd ${q(wurzel(a))} > ${datei}; echo "EXIT result: $?"; ${PY} ${q(WERKZEUG)} codex --ergebnis ${datei}; echo "EXIT codex: $?"`
|
||||
return `Du führst einen Werkzeugbefehl aus und überträgst seine Ausgabe in feste Felder. Zweck: das Ergebnis des Codex-Auftrags auswerten.
|
||||
|
||||
Führe genau diesen Befehl einmal im Bash-Werkzeug (Git-Bash) aus, unverändert:
|
||||
${befehl}
|
||||
|
||||
Übertrage aus der Ausgabe:
|
||||
- die Zahl hinter "EXIT codex:" nach exit_codex,
|
||||
- die Werte der Zeilen job_status, usage_limit_treffer und dauer_s unverändert in die gleichnamigen Felder,
|
||||
- log_gelesen und antwort_gefunden als true bei "ja", sonst als false,
|
||||
- aus dem Block unter "ANTWORT-JSON:" die Felder lesebeleg, befunde (je Befund datei, stelle, schwere, aussage, beleg) und nicht_pruefbar, Zeichen für Zeichen.
|
||||
Steht unter "ANTWORT-JSON:" nur "keine", setze lesebeleg auf "", befunde und nicht_pruefbar auf []. Fehlt eine Zeile, setze job_status auf "", die Zahlen auf -1 und die Wahrheitswerte auf false.
|
||||
Ändere keine Datei außer denen, die der Befehl selbst anlegt. Deute nichts, fasse nichts zusammen und ergänze keinen Befund. Übertrage das Ergebnis in die Felder des StructuredOutput-Werkzeugs.`
|
||||
}
|
||||
|
||||
function codexUngueltig(e, p) {
|
||||
if (!e) return 'Auswertung ohne Ergebnis'
|
||||
if (e.exit_codex !== 0) return `Auswertung endete mit Exit ${e.exit_codex}`
|
||||
if (e.job_status !== 'completed') return `Codex-Auftrag endete mit ${e.job_status || 'unbekanntem Status'}`
|
||||
if (e.log_gelesen !== true) return 'Log nicht gelesen'
|
||||
if (e.usage_limit_treffer !== 0) return '"usage limit" im Log'
|
||||
if (e.antwort_gefunden !== true) return 'keine Antwort im vereinbarten Format'
|
||||
if (!String(e.lesebeleg).includes('Lesemarke: ' + p.lesemarke)) return 'Lesebeleg fehlt oder zitiert eine andere Lesemarke'
|
||||
return null
|
||||
}
|
||||
|
||||
async function codexStimme(a, p) {
|
||||
const z = { abgegeben: false, grund: '', job: null, dauer_s: -1, befunde: [], nicht_pruefbar: [] }
|
||||
if (a.codex === false) {
|
||||
z.grund = 'im Auftrag abgeschaltet (codex: false)'
|
||||
return z
|
||||
}
|
||||
const P = q(wurzel(a))
|
||||
const start = ausgabe(await werkzeugAgent('codex:start', 'Stimmen',
|
||||
`node ${COMPANION} status --json --cwd ${P}; S=$?; echo "EXIT status: $S"; if [ "$S" -eq 0 ]; then node ${COMPANION} task --background --json --cwd ${P} --prompt-file ${q(p.ordner + '/codex-auftrag.md')}; echo "EXIT task: $?"; fi`,
|
||||
'den Codex-Status vor dem Lauf lesen und die Prüfung nur lesend im Hintergrund starten'))
|
||||
if (!/EXIT status: 0(?!\d)/.test(start)) {
|
||||
z.grund = 'Status vor dem Lauf nicht gelesen'
|
||||
return z
|
||||
}
|
||||
// Nur hinter "EXIT status:" suchen: status --json listet frühere Jobs derselben Sitzung samt
|
||||
// request.jobId (job-control.mjs 161-181); die erste Fundstelle wäre dann ein alter Job.
|
||||
const job = /"jobId": "([^"]+)"/.exec(start.slice(start.lastIndexOf('EXIT status:')))
|
||||
if (!/EXIT task: 0(?!\d)/.test(start) || !job) {
|
||||
z.grund = 'Codex-Auftrag nicht gestartet'
|
||||
return z
|
||||
}
|
||||
z.job = job[1]
|
||||
const runden = a.codex_runden || CODEX_RUNDEN
|
||||
let fertig = false
|
||||
let gescheitert = false
|
||||
for (let runde = 1; runde <= runden && !fertig && !gescheitert; runde++) {
|
||||
const w = ausgabe(await werkzeugAgent('codex:warten:' + runde, 'Stimmen',
|
||||
`node ${COMPANION} status ${q(z.job)} --wait --timeout-ms ${WARTEN_MS} --json --cwd ${P}; echo "EXIT warten: $?"`,
|
||||
'auf das Ende des Codex-Auftrags warten', LANGE))
|
||||
if (!/EXIT warten: 0(?!\d)/.test(w)) gescheitert = true
|
||||
else fertig = /"waitTimedOut": false/.test(w)
|
||||
}
|
||||
if (!fertig) {
|
||||
await werkzeugAgent('codex:abbruch', 'Stimmen', `node ${COMPANION} cancel ${q(z.job)} --json --cwd ${P}; echo "EXIT cancel: $?"`, 'den unfertigen Codex-Auftrag abbrechen, damit er kein Kontingent mehr verbraucht')
|
||||
z.grund = gescheitert ? 'Warten auf Codex gescheitert, Auftrag abgebrochen' : `nach ${runden} Warterunden nicht fertig, Auftrag abgebrochen`
|
||||
return z
|
||||
}
|
||||
const e = await agent(codexEndeAuftrag(a, p, z.job), { label: 'codex:ende', phase: 'Stimmen', schema: CODEX, model: 'sonnet', effort: 'low' })
|
||||
if (e && Number.isInteger(e.dauer_s)) z.dauer_s = e.dauer_s
|
||||
const ungueltig = codexUngueltig(e, p)
|
||||
if (ungueltig) {
|
||||
z.grund = ungueltig
|
||||
return z
|
||||
}
|
||||
z.abgegeben = true
|
||||
z.befunde = e.befunde
|
||||
z.nicht_pruefbar = e.nicht_pruefbar
|
||||
return z
|
||||
}
|
||||
|
||||
function zeile(b) {
|
||||
return `[${b.id}] Stimme ${NAMEN[b.stimme]} | ${b.datei} | ${b.stelle} | ${b.schwere}\nAussage: ${b.aussage}\nBeleg: ${b.beleg}`
|
||||
}
|
||||
|
||||
function zusammenfuehrenAuftrag(befunde) {
|
||||
return `Drei Prüfstimmen haben unabhängig voneinander denselben Stand geprüft. Unten stehen ihre belegten Befunde, je Befund eine ID. Lies keine Dateien und fahre keine Befehle; du arbeitest nur mit dieser Liste.
|
||||
|
||||
Fasse Befunde zu Gruppen zusammen, die denselben Fehler meinen. Gleich sind zwei Befunde nur, wenn Datei und Stelle übereinstimmen und die Aussage denselben Fehler beschreibt; eine Stelle stimmt überein, wenn sie dieselbe Zeile, dieselben Zeilen oder denselben Abschnitt meint. Im Zweifel bleiben Befunde getrennt, denn eine falsche Gruppe erzeugt eine falsche Mehrheit. Jede ID steht in genau einer Gruppe, ein Befund ohne Partner ist eine Gruppe für sich. Zähle nichts und bewerte nichts.
|
||||
|
||||
<befunde>
|
||||
${befunde.map(zeile).join('\n\n')}
|
||||
</befunde>
|
||||
|
||||
Übertrage je Gruppe die IDs und eine kurze Begründung in die Felder des StructuredOutput-Werkzeugs.`
|
||||
}
|
||||
|
||||
function pfadnorm(p) {
|
||||
return String(p).replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '').toLowerCase()
|
||||
}
|
||||
|
||||
// Stelle als Zeilenbereich [von, bis], oder null ohne Zeilennummer; dann gilt das Urteil des
|
||||
// Zusammenführ-Agenten. Gelesen werden "12", "12-14", "Zeile 12", "Zeilen 10-14", "Z. 10 bis 14",
|
||||
// "L12", "line 12" und "a.py:42", auch mitten im Text wie "Zeile 42 (Funktion f)"; ein Bereich
|
||||
// darf Bindestrich, Gedankenstrich, "bis" oder "to" tragen. Zuerst zählt eine Angabe mit Wort,
|
||||
// dann eine hinter Dateiendung und Doppelpunkt, zuletzt die reine Zahl.
|
||||
function zeilen(stelle) {
|
||||
const s = String(stelle)
|
||||
const m = /(?:^|[^\p{L}\d])(?:zeilen?|z\.|lines?|l)\s*(\d+)(?:\s*(?:-|–|bis|to)\s*l?\s*(\d+))?/iu.exec(s)
|
||||
|| /\.[a-z0-9]+:(\d+)(?:\s*(?:-|–)\s*(\d+))?/i.exec(s)
|
||||
|| /^\s*(\d+)(?:\s*(?:-|–|bis|to)\s*(\d+))?\s*$/i.exec(s)
|
||||
if (!m) return null
|
||||
const von = Number(m[1])
|
||||
const bis = m[2] === undefined ? von : Number(m[2])
|
||||
return von <= bis ? [von, bis] : [bis, von]
|
||||
}
|
||||
|
||||
// Zwei Zeilenbereiche gehören zusammen, wenn sie sich mit einer Toleranz von TOLERANZ Zeilen
|
||||
// überschneiden: "Zeile 42" und "Zeile 45" ja, "Zeile 42" und "Zeile 46" nicht.
|
||||
const TOLERANZ = 3
|
||||
function nahe(x, y) {
|
||||
return x[0] <= y[1] + TOLERANZ && y[0] <= x[1] + TOLERANZ
|
||||
}
|
||||
|
||||
// Teilt die Mitglieder einer Gruppe des Agenten in zusammenhängende Teilgruppen (transitive Hülle):
|
||||
// Zusammen gehören nur Stellen derselben Datei, zwei Stellen mit Zeilennummern nur, wenn sie nahe
|
||||
// liegen. Eine Stelle ohne Zeilennummer folgt dem Urteil des Agenten: Sie bleibt bei den Stellen
|
||||
// ihrer Datei, solange deren Zeilen eine einzige Teilgruppe bilden oder die Datei gar keine Zeilen
|
||||
// nennt. Zerfallen die Zeilen, steht sie für sich, denn eine falsche Gruppe erzeugt eine falsche
|
||||
// Mehrheit. Mitglieder und Teilgruppen bleiben in der Reihenfolge des Agenten.
|
||||
function teilgruppen(mitglieder) {
|
||||
const bereich = mitglieder.map(b => zeilen(b.stelle))
|
||||
const datei = mitglieder.map(b => pfadnorm(b.datei))
|
||||
const eltern = mitglieder.map((b, i) => i)
|
||||
const finde = i => (eltern[i] === i ? i : finde(eltern[i]))
|
||||
const verbinde = (i, j) => { eltern[finde(j)] = finde(i) }
|
||||
for (let i = 0; i < mitglieder.length; i++) {
|
||||
for (let j = i + 1; j < mitglieder.length; j++) {
|
||||
if (datei[i] === datei[j] && bereich[i] && bereich[j] && nahe(bereich[i], bereich[j])) verbinde(i, j)
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < mitglieder.length; i++) {
|
||||
if (bereich[i]) continue
|
||||
const gleich = mitglieder.map((b, j) => j).filter(j => j !== i && datei[j] === datei[i])
|
||||
const stuecke = new Set(gleich.filter(j => bereich[j]).map(finde))
|
||||
if (stuecke.size === 1) verbinde([...stuecke][0], i)
|
||||
else if (stuecke.size === 0 && gleich.length) verbinde(gleich[0], i)
|
||||
}
|
||||
const teile = new Map()
|
||||
mitglieder.forEach((b, i) => {
|
||||
if (!teile.has(finde(i))) teile.set(finde(i), [])
|
||||
teile.get(finde(i)).push(b)
|
||||
})
|
||||
return [...teile.values()]
|
||||
}
|
||||
|
||||
// Gruppen des Agenten prüfen: unbekannte IDs fallen weg, eine ID zählt nur in ihrer ersten
|
||||
// Gruppe, und jede Gruppe zerfällt in ihre Teilgruppen (Funktion teilgruppen); jede Aufteilung
|
||||
// steht als Hinweis im Bericht. Was keiner Gruppe angehört, bleibt eine Gruppe für sich.
|
||||
function gruppenBilden(roh, befunde) {
|
||||
const jeId = new Map(befunde.map(b => [b.id, b]))
|
||||
const vergeben = new Set()
|
||||
const gruppen = []
|
||||
const meldungen = []
|
||||
const liste = roh && Array.isArray(roh.gruppen) ? roh.gruppen : []
|
||||
for (const g of liste) {
|
||||
const mitglieder = []
|
||||
for (const id of Array.isArray(g.ids) ? g.ids : []) {
|
||||
if (!jeId.has(id)) meldungen.push(`Zusammenführen: unbekannte ID ${id} verworfen`)
|
||||
else if (vergeben.has(id)) meldungen.push(`Zusammenführen: ID ${id} steht in zwei Gruppen; nur die erste zählt`)
|
||||
else {
|
||||
vergeben.add(id)
|
||||
mitglieder.push(jeId.get(id))
|
||||
}
|
||||
}
|
||||
if (!mitglieder.length) continue
|
||||
const teile = teilgruppen(mitglieder)
|
||||
if (teile.length > 1) meldungen.push(`Zusammenführen: Gruppe ${mitglieder.map(b => b.id).join(', ')} aufgeteilt in ${teile.map(t => t.map(b => b.id).join(', ')).join(' | ')} (${mitglieder.map(b => `${b.id} ${b.datei} ${b.stelle}`).join('; ')})`)
|
||||
teile.forEach(t => gruppen.push(t))
|
||||
}
|
||||
for (const b of befunde) if (!vergeben.has(b.id)) gruppen.push([b])
|
||||
return { gruppen, meldungen }
|
||||
}
|
||||
|
||||
function nachpruefAuftrag(a, p, gruppen) {
|
||||
const liste = gruppen.map(g => `Befund ${g.mitglieder[0].id}:\n${g.mitglieder.map(zeile).join('\n')}`).join('\n\n')
|
||||
return `Auftragsart: Nachprüfung.
|
||||
Prüfpaket: ${p.ordner}/paket.md
|
||||
Wurzel: ${wurzel(a)}
|
||||
|
||||
Jeder Befund unten stammt von nur einer Stimme. Prüfe jeden gegen den Code bzw. das Dokument im Stand des Pakets, nach deiner Agentendefinition. Stehen unter einer Zeile "Befund <ID>:" mehrere Fassungen, meinen sie denselben Befund; urteile einmal, unter dieser ID.
|
||||
|
||||
<befunde>
|
||||
${liste}
|
||||
</befunde>
|
||||
|
||||
Übertrage je Befund id, urteil, fundstelle und begruendung in die Felder des StructuredOutput-Werkzeugs.`
|
||||
}
|
||||
|
||||
function laufzeit(s) {
|
||||
return Number.isInteger(s) && s >= 0 ? `${Math.floor(s / 60)} min ${s % 60} s` : 'nicht gemessen'
|
||||
}
|
||||
|
||||
function verbrauchText(anlass) {
|
||||
const r = ablesungen.find(x => x.anlass === anlass)
|
||||
return r && r.status === 'ok' && r.fuenf_stunden && r.woche ? `5 Stunden ${r.fuenf_stunden.prozent} %, Woche ${r.woche.prozent} %` : 'nicht gelesen'
|
||||
}
|
||||
|
||||
function urteilText(urteil) {
|
||||
return urteil === 'frei' ? 'frei' : urteil === 'nicht_frei' ? 'nicht frei' : 'kein Urteil'
|
||||
}
|
||||
|
||||
// Die Zeile stand: PAKET ersetzt quorum_werkzeug.py ablegen durch den Stand aus paket.json.
|
||||
function kopf(a, p, urteil, summary, codexFeld) {
|
||||
return ['---', 'type: note', `title: "Quorum ${p.nummer}: ${a.anlass}"`, 'status: done', `summary: "${summary}"`, 'tags: [quorum]', `created: ${p.datum}`, `updated: ${p.datum}`, `nummer: ${p.nummer}`, `anlass: ${a.anlass}`, `urteil: ${urteil}`, `basis: "${p.basis}"`, 'stand: PAKET', `codex: ${codexFeld}`, '---'].join('\n')
|
||||
}
|
||||
|
||||
// Hub-Zeile nach der Hub-Regel (AI/Regelwerk/navigation.md, "Links schreiben und Dateien
|
||||
// verschieben"): im Code-Projekt ein relativer Markdown-Link aufs README, sonst ein Wikilink mit
|
||||
// vollem Vault-Pfad auf die Übersicht des Kundendokuments. Der Bericht liegt in <ordner>/quorum/.
|
||||
function hubZeile(a) {
|
||||
if (a.anlass !== 'kundendokument') return 'Projekt: [README](../../README.md)'
|
||||
const rel = vaultPfad(a.ordner)
|
||||
const teile = rel.split('/')
|
||||
if (teile[0].toLowerCase() === 'code projekte' && teile.length >= 2) return `Projekt: [README](${'../'.repeat(teile.length - 1)}README.md)`
|
||||
const u = a.uebersicht.replace(/\\/g, '/')
|
||||
return `Übersicht: [[${rel}/${u.replace(/\.md$/i, '')}|${u.split('/').pop()}]]`
|
||||
}
|
||||
|
||||
function berichtLeer(a, p, urteil, grund) {
|
||||
const vorher = p.vorher ? String(p.vorher.pfad).split('/').pop() : 'dem letzten Bericht'
|
||||
return [kopf(a, p, urteil, `Urteil ${urteil}: ${grund}.`, 'nicht gelaufen'), '', `# Quorum ${p.nummer}: ${a.anlass}`, '', hubZeile(a), '', '## Urteil', '', `**${urteilText(urteil)}**: Seit dem letzten Quorum hat sich nichts geändert. Das Urteil stammt aus ${vorher}; keine Stimme ist gelaufen.`, ''].join('\n')
|
||||
}
|
||||
|
||||
// Freitext als Klartext für Obsidian: Jede öffnende eckige Klammer in einer Folge von zwei oder
|
||||
// mehr bekommt einen Backslash, damit kein Wikilink und keine Einbettung entsteht. Eine Raute
|
||||
// bekommt einen Backslash, wenn ihr kein Buchstabe, keine Ziffer, kein Unterstrich und kein
|
||||
// Backslash vorausgeht und weder Leerraum noch eine zweite Raute folgt; so entsteht kein Tag.
|
||||
// Die Leseansicht zeigt den Text wie zitiert. Zweimal angewendet ändert sich nichts mehr.
|
||||
function klartext(s) {
|
||||
return String(s)
|
||||
.replace(/\[{2,}/g, folge => folge.replace(/\[/g, '\\['))
|
||||
.replace(/(^|[^\p{L}\p{N}_\\])#(?=[^\s#])/gu, '$1\\#')
|
||||
}
|
||||
|
||||
function berichtVoll(a, p, x) {
|
||||
const namen = Object.keys(x.stimmen).filter(n => x.stimmen[n].abgegeben).map(n => NAMEN[n]).join(', ')
|
||||
const codexFeld = x.stimmen.codex.abgegeben ? 'abgegeben' : 'ersetzt'
|
||||
const z = [kopf(a, p, x.urteil, `Urteil ${x.urteil}: ${x.grund}; Stimmen ${namen}.`, codexFeld), '', `# Quorum ${p.nummer}: ${a.anlass}`, '', hubZeile(a), '']
|
||||
z.push('## Urteil', '', `**${urteilText(x.urteil)}**: ${x.grund}.`, '', `Basis ${p.basis}, Stand ${p.stand}; ${p.anzahl_dateien} Datei(en) im Unterschied.`, '')
|
||||
z.push('## Stimmen', '', '| Stimme | abgegeben | Befunde | belegt | bestätigt | Alleinfunde | Laufzeit |', '|---|---|---|---|---|---|---|')
|
||||
for (const n of Object.keys(x.stimmen)) {
|
||||
const s = x.stimmen[n]
|
||||
const c = x.zaehlung[n]
|
||||
const name = n === 'haiku' ? 'Haiku (Ersatz für Codex)' : NAMEN[n]
|
||||
z.push(`| ${name} | ${s.abgegeben ? 'ja' : 'nein: ' + klartext(s.grund)} | ${c.gesamt} | ${c.belegt} | ${c.bestaetigt} | ${c.alleinfunde} | ${laufzeit(s.dauer_s)} |`)
|
||||
}
|
||||
z.push('')
|
||||
if (x.stimmen.haiku) z.push('Haiku stand statt Codex im Quorum. Dieses Quorum war nur mit Claude besetzt.', '')
|
||||
z.push(`Verbrauch vor den Stimmen: ${verbrauchText('vor Stimmen')}; am Ende: ${verbrauchText('Ende')}.`, '')
|
||||
z.push('## Befunde', '')
|
||||
if (!x.gruppen.length) z.push('keine belegten Befunde', '')
|
||||
for (const g of x.gruppen) {
|
||||
const erste = g.mitglieder[0]
|
||||
z.push(`### G${g.nr}: ${g.schwere}, ${STATUS_TEXT[g.status]} (${g.stimmen.map(n => NAMEN[n]).join(', ')})`, '', `\`${erste.datei}\`, Stelle ${klartext(erste.stelle)}`, '')
|
||||
for (const b of g.mitglieder) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}, Stelle ${klartext(b.stelle)}): ${klartext(b.aussage)}`, ` Beleg: ${klartext(b.beleg)}`)
|
||||
if (g.nachpruefung) z.push('', `Nachprüfung: ${g.nachpruefung.urteil}, ${klartext(g.nachpruefung.fundstelle)}. ${klartext(g.nachpruefung.begruendung)}`)
|
||||
z.push('')
|
||||
}
|
||||
z.push('## Unbelegt', '')
|
||||
if (!x.unbelegt.length) z.push('keine')
|
||||
for (const b of x.unbelegt) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}) \`${b.datei}\`, Stelle ${klartext(b.stelle)}: ${klartext(b.aussage)}`)
|
||||
z.push('', '## Nicht prüfbar', '')
|
||||
const offen = Object.keys(x.stimmen).flatMap(n => (x.stimmen[n].nicht_pruefbar || []).map(t => `- ${NAMEN[n]}: ${klartext(t)}`))
|
||||
z.push(...(offen.length ? offen : ['keine']))
|
||||
z.push('', '## Hinweise des Laufs', '')
|
||||
const alleHinweise = [...p.hinweise, ...laufHinweise]
|
||||
z.push(...(alleHinweise.length ? alleHinweise.map(h => '- ' + klartext(h)) : ['keine']), '')
|
||||
return z.join('\n')
|
||||
}
|
||||
|
||||
// Der fertige Befehl für den Koordinator, nachdem er bericht_md als bericht.md im Paketordner
|
||||
// gespeichert hat. --summe ist paket_summe aus QUORUM_PAKET: ablegen setzt den Stand nur aus dem
|
||||
// paket.json ein, dessen SHA-256 der Paketbau gemeldet hat, und endet sonst mit BEFUND.
|
||||
function ablegenBefehl(p) {
|
||||
return `${PY} ${q(WERKZEUG)} ablegen --paket ${q(p.ordner)} --bericht ${q(p.ordner + '/bericht.md')} --ziel ${q(p.bericht_pfad)} --summe ${q(p.paket_summe)}`
|
||||
}
|
||||
|
||||
function ergebnis(status, urteil, grund, md, stimmen, gruppen, unbelegt, zaehlung, naechster) {
|
||||
return { status, urteil, grund, bericht_pfad: md ? paket.bericht_pfad : null, bericht_md: md, paket: paket.ordner, ablegen_befehl: md ? ablegenBefehl(paket) : null, stimmen, gruppen, unbelegt, zaehlung, verbrauch: ablesungen, hinweise: [...paket.hinweise, ...laufHinweise], naechster_schritt: naechster }
|
||||
}
|
||||
|
||||
pruefeArgs(args)
|
||||
phase('Paket')
|
||||
const gebaut = lesePaket(await werkzeugAgent('werkzeug:paket', 'Paket', paketBefehl(args), 'Prüfpaket für das Quorum bauen', args.suite ? LANGE : ''))
|
||||
if (gebaut.fehler) throw new Error('quorum: ' + gebaut.fehler)
|
||||
const paket = gebaut.paket
|
||||
paket.hinweise.forEach(h => log('Paket: ' + h))
|
||||
|
||||
if (paket.leer) {
|
||||
const urteil = paket.vorher && paket.vorher.urteil === 'frei' ? 'frei' : 'nicht_frei'
|
||||
const grund = `Unterschied leer; Urteil aus Quorum ${paket.vorher ? paket.vorher.nummer : '?'} übernommen, keine Stimme gestartet`
|
||||
log(grund)
|
||||
return ergebnis('leer', urteil, grund, berichtLeer(args, paket, urteil, grund), {}, [], [], null, 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren und committen; das übernommene Urteil gilt.')
|
||||
}
|
||||
|
||||
phase('Stimmen')
|
||||
const vorStimmen = await ablesen('vor Stimmen', 1)
|
||||
if (vorStimmen.gebremst) {
|
||||
const grund = 'gebremst vor den Stimmen: ' + vorStimmen.grund
|
||||
log('Quorum ' + grund)
|
||||
return ergebnis('gebremst', 'kein_urteil', grund, null, {}, [], [], null, 'Nach der Zurücksetzung des Fensters neu starten, nicht mit resumeFromRunId fortsetzen; die Basis bleibt die alte.')
|
||||
}
|
||||
const claude = modell => agent(stimmenAuftrag(args, paket), { label: 'stimme:' + modell, phase: 'Stimmen', schema: STIMME, agentType: 'quorum-stimme', model: modell })
|
||||
.catch(e => {
|
||||
log(`stimme:${modell} abgebrochen: ${e && e.message}`)
|
||||
return null
|
||||
})
|
||||
const [rohOpus, rohSonnet, codex] = await Promise.all([
|
||||
claude('opus'),
|
||||
claude('sonnet'),
|
||||
codexStimme(args, paket).catch(e => ({ abgegeben: false, grund: 'Absturz: ' + (e && e.message), job: null, dauer_s: -1, befunde: [], nicht_pruefbar: [] })),
|
||||
])
|
||||
const stimmen = { opus: claudeStimme(rohOpus, paket), sonnet: claudeStimme(rohSonnet, paket), codex }
|
||||
if (!codex.abgegeben) {
|
||||
hinweis('Codex ohne gültige Stimme (' + codex.grund + '); Haiku ist eingesprungen')
|
||||
stimmen.haiku = claudeStimme(await claude('haiku'), paket)
|
||||
}
|
||||
for (const n of ['opus', 'sonnet', 'haiku']) {
|
||||
const s = stimmen[n]
|
||||
if (s && s.abgegeben && !s.lesemarke_ok) hinweis(`${NAMEN[n]}: Lesemarke fehlt oder weicht ab; ob die Stimme das Paket gelesen hat, ist unbelegt`)
|
||||
}
|
||||
const abgegeben = Object.keys(stimmen).filter(n => stimmen[n].abgegeben)
|
||||
const alle = []
|
||||
for (const n of abgegeben) {
|
||||
stimmen[n].befunde.forEach((b, i) => alle.push({ id: KUERZEL[n] + (i + 1), stimme: n, datei: String(b.datei), stelle: String(b.stelle), schwere: b.schwere, aussage: String(b.aussage), beleg: String(b.beleg), belegt: String(b.beleg).trim() !== '' }))
|
||||
}
|
||||
const belegt = alle.filter(b => b.belegt)
|
||||
const unbelegt = alle.filter(b => !b.belegt)
|
||||
|
||||
if (abgegeben.length < 2) {
|
||||
await ablesen('Ende', 0)
|
||||
const grund = `nur ${abgegeben.length} Stimme(n) abgegeben; ein Quorum braucht zwei`
|
||||
log(grund)
|
||||
return ergebnis('unvollstaendig', 'kein_urteil', grund, null, stimmen, [], unbelegt, null, 'Ursache der fehlenden Stimmen klären und neu starten; ohne Bericht bleibt die Basis die alte.')
|
||||
}
|
||||
|
||||
phase('Zusammenführen')
|
||||
let rohGruppen = null
|
||||
if (new Set(belegt.map(b => b.stimme)).size >= 2) {
|
||||
rohGruppen = await agent(zusammenfuehrenAuftrag(belegt), { label: 'zusammenfuehren', phase: 'Zusammenführen', schema: GRUPPEN, model: 'sonnet', effort: 'medium' }).catch(() => null)
|
||||
if (!rohGruppen) hinweis('Zusammenführen ohne Ergebnis; jeder Befund blieb eine eigene Gruppe')
|
||||
}
|
||||
const gebildet = gruppenBilden(rohGruppen, belegt)
|
||||
gebildet.meldungen.forEach(hinweis)
|
||||
const gruppen = gebildet.gruppen.map((mitglieder, i) => {
|
||||
const beteiligt = [...new Set(mitglieder.map(b => b.stimme))]
|
||||
const schwere = mitglieder.map(b => b.schwere).sort((x, y) => RANG[x] - RANG[y])[0]
|
||||
return { nr: i + 1, schwere, stimmen: beteiligt, mehrheit: beteiligt.length >= 2, status: '', nachpruefung: null, mitglieder }
|
||||
})
|
||||
|
||||
phase('Nachprüfen')
|
||||
const nachzupruefen = gruppen.filter(g => !g.mehrheit && g.schwere !== 'hinweis')
|
||||
let rohUrteile = null
|
||||
if (nachzupruefen.length) {
|
||||
rohUrteile = await agent(nachpruefAuftrag(args, paket, nachzupruefen), { label: 'nachpruefung', phase: 'Nachprüfen', schema: URTEILE, agentType: 'quorum-stimme', model: 'sonnet' }).catch(() => null)
|
||||
if (!rohUrteile) hinweis('Nachprüfung ohne Ergebnis; die Einzelbefunde blieben ungeprüft')
|
||||
}
|
||||
const jeUrteil = new Map()
|
||||
for (const u of rohUrteile && Array.isArray(rohUrteile.urteile) ? rohUrteile.urteile : []) if (!jeUrteil.has(u.id)) jeUrteil.set(u.id, u)
|
||||
for (const g of gruppen) {
|
||||
if (g.mehrheit) g.status = 'mehrheit'
|
||||
else if (g.schwere === 'hinweis') g.status = 'hinweis_ungeprueft'
|
||||
else {
|
||||
g.nachpruefung = jeUrteil.get(g.mitglieder[0].id) || null
|
||||
g.status = g.nachpruefung ? g.nachpruefung.urteil : 'nicht_geprueft'
|
||||
}
|
||||
}
|
||||
|
||||
const gilt = g => g.status === 'mehrheit' || g.status === 'bestaetigt'
|
||||
const sperrend = gruppen.filter(g => g.schwere === 'blockierend' && (gilt(g) || g.status === 'nicht_geprueft'))
|
||||
const urteil = sperrend.length ? 'nicht_frei' : 'frei'
|
||||
const grund = sperrend.length
|
||||
? `${sperrend.filter(gilt).length} bestätigte und ${sperrend.filter(g => !gilt(g)).length} ungeprüfte blockierende Befunde`
|
||||
: 'kein bestätigter und kein ungeprüfter blockierender Befund mit Beleg' + (unbelegt.some(b => b.schwere === 'blockierend') ? `; ${unbelegt.filter(b => b.schwere === 'blockierend').length} blockierende ohne Beleg stehen unter Unbelegt und sperren nicht` : '')
|
||||
await ablesen('Ende', 0)
|
||||
const zaehlung = {}
|
||||
for (const n of Object.keys(stimmen)) {
|
||||
const anteil = liste => liste.reduce((summe, g) => summe + g.mitglieder.filter(b => b.stimme === n).length, 0)
|
||||
zaehlung[n] = {
|
||||
abgegeben: stimmen[n].abgegeben,
|
||||
gesamt: alle.filter(b => b.stimme === n).length,
|
||||
belegt: belegt.filter(b => b.stimme === n).length,
|
||||
bestaetigt: anteil(gruppen.filter(gilt)),
|
||||
alleinfunde: anteil(gruppen.filter(g => g.status === 'bestaetigt')),
|
||||
}
|
||||
}
|
||||
const ORDNUNG = { mehrheit: 0, bestaetigt: 0, nicht_geprueft: 1, widerlegt: 2, hinweis_ungeprueft: 3 }
|
||||
gruppen.sort((x, y) => (ORDNUNG[x.status] - ORDNUNG[y.status]) || (RANG[x.schwere] - RANG[y.schwere]) || (x.nr - y.nr))
|
||||
gruppen.forEach((g, i) => { g.nr = i + 1 })
|
||||
const md = berichtVoll(args, paket, { urteil, grund, stimmen, zaehlung, gruppen, unbelegt })
|
||||
return ergebnis('fertig', urteil, grund, md, stimmen, gruppen, unbelegt, zaehlung, urteil === 'frei'
|
||||
? 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren, den Bericht im README bzw. in der Übersicht verlinken und committen; der Meilenstein ist frei.'
|
||||
: 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren und committen. Die Korrekturen entscheidet der Koordinator, umgesetzt über die Standardschleife; danach bestätigt eine quorum-stimme auf Sonnet (Auftragsart Nachkontrolle) je Befund, dass er behoben ist.')
|
||||
Reference in new issue
Block a user