From affde3a45f59a18a5e00746453e34708e94f1309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=B6rer?= Date: Wed, 26 Aug 2026 16:54:32 +0200 Subject: [PATCH] =?UTF-8?q?gpt=20skill=20erg=C3=BCnzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/run-experiment/SKILL.md | 224 +++++++++++++++--- .../run-experiment/codex-output-schema.json | 20 ++ .../run-experiment/normalise-codex-result.py | 165 +++++++++++++ 3 files changed, 376 insertions(+), 33 deletions(-) create mode 100644 .claude/skills/run-experiment/codex-output-schema.json create mode 100644 .claude/skills/run-experiment/normalise-codex-result.py diff --git a/.claude/skills/run-experiment/SKILL.md b/.claude/skills/run-experiment/SKILL.md index 8fb3d19a..2afeb0b7 100644 --- a/.claude/skills/run-experiment/SKILL.md +++ b/.claude/skills/run-experiment/SKILL.md @@ -1,15 +1,15 @@ --- name: run-experiment -description: Führt einen Versuchs-Prompt aus einer Prompt-Datei als messbaren Headless-Lauf aus (claude -p) und schreibt ein Messprotokoll mit Start-/Endzeit, Modell, Tokenverbrauch und weiteren Metriken. Verwenden bei "/run-experiment " oder wenn der User einen Versuch/ein Experiment ausführen und tracken will. +description: Führt einen Versuchs-Prompt aus einer Prompt-Datei als messbaren Headless-Lauf mit Claude Code oder Codex CLI aus und schreibt ein Messprotokoll mit Start-/Endzeit, Modell, Tokenverbrauch und weiteren Metriken. Verwenden bei "/run-experiment " oder wenn der User einen Versuch/ein Experiment ausführen und tracken will. argument-hint: -version: 4.5.0 +version: 5.0.0 --- # RunExperiment – Versuchslauf mit Messprotokoll Du führst einen wissenschaftlichen Versuchslauf für die Masterarbeit aus. Der Prompt aus der -übergebenen Datei wird als **separater Headless-Lauf** von Claude Code ausgeführt, damit -Tokenverbrauch und Modell exakt und maschinenlesbar erfasst werden. Anschließend +übergebenen Datei wird als **separater Headless-Lauf** über den zum Modell passenden +Werkzeugadapter ausgeführt, damit Tokenverbrauch und Modell maschinenlesbar erfasst werden. Anschließend schreibst du ein Messprotokoll neben die Prompt-Datei. ## Parameter @@ -39,7 +39,8 @@ Laufverzeichnis neben der Prompt-Datei**, niemals im Root-Verzeichnis: \\\\ _Lauf__v-\ Protokoll.md - RawResult.json + RawResult.json (adapterübergreifend normalisierte Messdaten) + RawEvents.jsonl (nur Codex: unveränderter JSONL-Ereignisstrom) Stderr.log _meta\ (Laufsteuerung: combined_prompt.md, before.txt, after.txt, Zeitstempel, subagenten.md/.json, anforderungen.md/.json) @@ -99,7 +100,8 @@ Der Skill ist zweigeteilt: - **`## Prozess`** beschreibt werkzeugneutral, **was** je Lauf zu tun und zu erheben ist. Dieser Teil gilt unabhängig davon, mit welchem LLM oder welcher CLI gearbeitet wird. - **`## Werkzeugadapter`** beschreibt **wie** das mit einem konkreten Werkzeug umgesetzt wird. - Derzeit ist nur der Adapter für Claude Code ausgearbeitet. + Ausgearbeitet sind Adapter für Claude Code und Codex CLI. Konkrete Flags und Rohfelder sind + ausschließlich dem gewählten Adapterabschnitt zu entnehmen. **Arbeitsteilung mit der Prompt-Datei:** Der Prompt enthält ausschließlich die *Analyseanweisung* und ist damit von jedem LLM verwendbar. Alles Werkzeug- und Ablaufbezogene – verfügbare @@ -120,7 +122,11 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" Prompt-Versionen bleiben unverändert liegen – sie sind der Beleg dafür, unter welcher Fassung frühere Läufe entstanden sind, und dürfen nicht nachträglich angepasst werden. 2. Aus dem Metadaten-Block der Prompt-Datei (falls vorhanden) Versuch/Prompt-Version übernehmen. -3. **CLI-Pfad auflösen.** Unter Windows liegt `claude` in der Regel **nicht im PATH**. Erst +3. **Werkzeugadapter bestimmen und CLI-Pfad auflösen.** Die Modell-ID entscheidet eindeutig: + `claude-*` verwendet Claude Code, OpenAI-IDs wie `gpt-*` oder `o*` verwenden Codex CLI. + Keine Modell-ID an eine CLI übergeben, die sie nicht unterstützt. + + Unter Windows liegt `claude` in der Regel **nicht im PATH**. Erst `Get-Command claude -ErrorAction SilentlyContinue` versuchen; schlägt das fehl, auf die VSCode-Extension zurückfallen und die höchste Versionsnummer wählen: ```powershell @@ -130,13 +136,17 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" Sort-Object FullName | Select-Object -Last 1 -ExpandProperty FullName } ``` - Findet sich keine ausführbare Datei: abbrechen und den User informieren. Den aufgelösten - Pfad fürs Protokoll festhalten. + Für OpenAI-Modelle entsprechend `Get-Command codex -ErrorAction SilentlyContinue` verwenden; + als Fallback die höchste VS-Code-Extension unter + `$env:USERPROFILE\.vscode\extensions\openai.chatgpt-*-win32-x64\bin\windows-x86_64\codex.exe` + wählen. Findet sich die benötigte ausführbare Datei nicht: abbrechen und den User informieren. + Adapter und aufgelösten Pfad fürs Protokoll festhalten. 4. **Modell beim User erfragen.** Hat der User das Modell nicht bereits im Aufruf genannt, **immer** per `AskUserQuestion` nachfragen – auch dann, wenn frühere Läufe derselben Versuchsreihe ein bestimmtes Modell verwendet haben. Es gibt bewusst keinen Default. - Als Optionen die vollen Modell-IDs anbieten, nicht die Kurzformen: + Als Optionen die vollen Modell-IDs anbieten, nicht die Kurzformen. Nur aktuell dokumentierte + und von der installierten CLI angebotene IDs aufnehmen: | Modell-ID | Einordnung | |---|---| @@ -145,6 +155,12 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" | `claude-fable-5` | schnell und günstig | | `claude-haiku-4-5-20251001` | günstigstes Modell | + | OpenAI-Modell-ID | Einordnung | + |---|---| + | `gpt-5.6-sol` | stärkstes Modell der GPT-5.6-Familie, 1.050.000 Kontexttokens | + | `gpt-5.6-terra` | ausgewogenes Verhältnis aus Leistung und Verbrauch | + | `gpt-5.6-luna` | schnelle und günstige GPT-5.6-Variante | + In der Frage den letzten verwendeten Stand nennen, damit der User bewusst wechseln oder bewusst wiederholen kann (z. B. „Lauf 3 lief mit `claude-sonnet-5`, 11.516.200 Tokens, 23:40"). @@ -153,6 +169,11 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" Versuchsbedingung nicht reproduzierbar. Für ein 1-Mio.-Token-Fenster die Variante mit `[1m]`-Suffix wählen (z. B. `claude-opus-5[1m]`). + Für OpenAI ebenfalls keine Familienaliase wie `gpt-5.6` verwenden. Die gewünschte Variante + vollständig binden, beispielsweise `gpt-5.6-sol`. Vor einem Lauf die Modell-ID gegen die + aktuelle offizielle OpenAI-Modelldokumentation und, soweit verfügbar, den lokalen + `codex debug models`-Katalog prüfen. + Weicht das gewählte Modell vom letzten Lauf ab, im Protokoll unter „Anmerkungen" als geänderte Versuchsbedingung vermerken und in der Änderungshistorie des Skills ergänzen. 5. **Agentenmodus beim User erfragen.** Wie beim Modell: kein Default, immer nachfragen, wenn @@ -194,23 +215,29 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" der Prompt per SHA-256 gehasht und im Protokoll geführt. So bleibt der eingefrorene Snapshot unangetastet und die Agentenkonfiguration ist eine dokumentierte, versionierte Versuchsbedingung. Format siehe `claude --help` zu `--agents`. + + **Codex-Adapter in Version 5.0.0:** ausschließlich `solo` ist freigegeben und wird doppelt + mit `--disable multi_agent` sowie `-c agents.enabled=false` erzwungen. Bei `builtin` oder + `custom` abbrechen und mitteilen, dass dieser Adaptermodus noch nicht verifiziert ist. Eine + stillschweigende Annäherung an Claude-Agentendefinitionen wäre keine reproduzierbare Bedingung. 6. **Effort beim User erfragen.** Kein Default, immer nachfragen, wenn der User die Stufe nicht bereits im Aufruf genannt hat. Stufen: `low`, `medium`, `high`, `xhigh`, `max`. - Übergabe per `--effort `. + Übergabe bei Claude per `--effort `, bei Codex per + `-c model_reasoning_effort=\"\"`. In der Frage den bisherigen Stand nennen. **Alle Läufe bis einschließlich Lauf B liefen auf `high`** – geerbt aus der Sitzungseinstellung, nicht bewusst gesetzt; der Wert wurde nachträglich aus den Transkripten belegt. Wer die Stufe wechselt, ändert die Versuchsbedingung und macht den Lauf mit den bisherigen unvergleichbar. - **Wichtig – der Effort ist aus `RawResult.json` nicht rekonstruierbar.** Das Ergebnisobjekt + **Claude-spezifisch:** Der Effort ist aus dessen `RawResult.json` nicht rekonstruierbar. Das Ergebnisobjekt enthält kein `effort`-Feld. Nur das Session-Transkript hält ihn je Nachricht fest (`"effort": "high"`). Fehlt er im Protokoll, ist die Bedingung nachträglich allein über das Transkript belegbar – und das nur, solange die Session persistiert ist. 7. Messgrößen VOR dem Lauf erfassen (PowerShell): - Startzeit: `Get-Date -Format o` - SHA-256 der Prompt-Datei: `(Get-FileHash -Algorithm SHA256).Hash` - - Claude-Code-Version: `& $claude --version` + - Adapter und CLI-Version: `& $cli --version` - Git-Zustand des Root-Verzeichnisses: `git -C rev-parse HEAD` und `git -C status --porcelain -- ` (dirty ja/nein). **Immer pfadskopiert prüfen.** Liegt die Codebasis als Unterverzeichnis im Arbeitsrepo @@ -228,7 +255,7 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" Sort-Object { [int]($_.Name -replace '\D','') } $iteration = if ($iterationen) { $iterationen[-1].Name } else { 'Iteration 1' } $zelle = Join-Path (Join-Path (Join-Path $iteration $modell) $modus) $effort - $skillVer = 'v4.5.0' # entspricht version: im Frontmatter dieses Skills + $skillVer = 'v5.0.0' # entspricht version: im Frontmatter dieses Skills do { $id4 = '{0:x4}' -f (Get-Random -Maximum 65536) $lauf = Join-Path "\$zelle" "_Lauf_$(Get-Date -Format 'yyyy-MM-dd_HHmmss')_${skillVer}-$id4" @@ -320,9 +347,26 @@ Schreibe ALLE zu erzeugenden Ergebnisdateien in das Verzeichnis Verändere keine Dateien im Arbeitsverzeichnis (der analysierten Codebasis). ``` +**Codex-Abweichung bei Block 2.** Der Codex-Adapter läuft mit einem technisch erzwungenen +`read-only`-Sandboxmodus. Er kann deshalb auch das externe Laufverzeichnis nicht direkt als +Agent beschreiben. Block 2 wird dort durch folgende strukturierte Rückgabeanweisung ersetzt; +die CLI erzwingt `codex-output-schema.json`, anschließend materialisiert +`normalise-codex-result.py` die Dateien: + +``` +### Ergebnisausgabe (überschreibt anderslautende Pfadangaben oben) +Verändere keine Dateien. Gib alle geforderten Ergebnisdateien im vorgegebenen JSON-Schema zurück. +Jeder Eintrag in `files` enthält unter `path` einen relativen Pfad innerhalb von `Ergebnisse` +und unter `content` den vollständigen Dateiinhalt. `summary` enthält nur eine kurze Laufzusammenfassung. +``` + ### 3. Ausführung (Headless-Lauf) -Dann den kombinierten Prompt per stdin an `claude -p` übergeben. `--add-dir` gibt dem +Den Adapter anhand der Modell-ID wählen. Der folgende Aufruf ist **ausschließlich der +Claude-Code-Adapter**. Der Codex-Aufruf steht vollständig unter `Adapter: Codex CLI` und darf +nicht mit Claude-Flags vermischt werden. + +Den kombinierten Prompt per stdin an `claude -p` übergeben. `--add-dir` gibt dem Headless-Lauf Schreibrecht auf das Laufverzeichnis außerhalb seines Arbeitsverzeichnisses. Versuchsläufe können lange dauern – **immer als Background-Task starten** (`run_in_background`), nicht mit Foreground-Timeout arbeiten: @@ -426,8 +470,9 @@ Regeln: ### 4. Ergebnis auswerten -`RawResult.json` im Laufverzeichnis lesen und defensiv parsen (Feldnamen können je nach Claude-Code-Version -leicht abweichen). Relevante Felder: +`RawResult.json` im Laufverzeichnis lesen und defensiv parsen. Beim Claude-Adapter ist dies die +unveränderte CLI-Antwort; beim Codex-Adapter erzeugt `normalise-codex-result.py` diese Datei aus +`RawEvents.jsonl` und `_meta\final_response.json`. Relevante Claude-Felder: | Feld | Bedeutung | |---|---| @@ -541,12 +586,12 @@ Vorgabe delegiert – gegenüber den vorformulierten Agenten aus `--agents`. **Anforderungen auswerten** – der inhaltliche Ertrag des Laufs, nicht nur sein Aufwand: ```powershell -python "nalyse-anforderungen.py" "" +python "\analyse-anforderungen.py" "" ``` Das Skript parst das im Prompt vorgegebene Blockformat (`ID:`, `Typ:`, `Belege:`, `Status:` …) -aus `Ergebnisse\StRS.md`, `SyRS.md` und `SwRS.md` und schreibt `_metanforderungen.md` -(fertiger Protokollabschnitt) sowie `_metanforderungen.json` (maschinenlesbar). Erhoben werden: +aus `Ergebnisse\StRS.md`, `SyRS.md` und `SwRS.md` und schreibt `_meta\anforderungen.md` +(fertiger Protokollabschnitt) sowie `_meta\anforderungen.json` (maschinenlesbar). Erhoben werden: - **Verteilung** über die drei Ebenen - **Anforderungstypen** (funktional, Sicherheit, Daten, Schnittstelle …) @@ -588,7 +633,7 @@ Der erzeugte Abschnitt wird **unverändert** als `## Gefundene Anforderungen` in Erzeugte Dateien: Inhalt von `\Ergebnisse\` auflisten. Zusätzlich prüfen, ob das Root unverändert blieb: `git -C status --porcelain -- ` – mit **demselben Pfadfilter wie in Abschnitt 1** – gegen `before.txt` -vergleichen (Nachher-Stand nach `$lauf\_metafter.txt`) – Abweichungen als Auffälligkeit +vergleichen (Nachher-Stand nach `$lauf\_meta\after.txt`) – Abweichungen als Auffälligkeit ins Protokoll. Beim Vergleich Zeilenenden normalisieren (`before.txt` entsteht je nach Werkzeug mit CRLF, der Nachher-Stand mit LF), sonst meldet ein naiver `diff` alle Zeilen als geändert. @@ -616,11 +661,14 @@ Vorlage: ## Werkzeugkonfiguration - **Skill-Version:** -- **Claude-Code-Version:** -- **CLI-Pfad:** +- **Werkzeugadapter:** +- **CLI-Version:** +- **CLI-Pfad:** - **Modell (angefordert):** -- **Modelle (tatsächlich eingesetzt):** -- **Kontrolle Modell:** mit Tokens> +- **Modelle (tatsächlich eingesetzt):** +- **Kontrolle Modell:** mit Tokens | + nicht prüfbar: Codex-JSONL enthält keine tatsächliche Modell-ID> - **Effort:** (per `--effort` gesetzt; Gegenprobe im Transkript-Feld `effort`) - **Laufverzeichnis-ID:** `v-` aus dem Verzeichnisnamen @@ -632,9 +680,9 @@ Vorlage: - **Sampling-Parameter:** - **Nur bei lokalem Modellbetrieb:** Inferenz-Runtime samt Version, Quantisierungsstufe des Modell-Builds -- **Permission-Mode:** -- **Toolfreigabe:** `--allowedTools ` / `--disallowedTools ` -- **Isolationsmechanismus:** <--safe-mode, --strict-mcp-config, ggf. --setting-sources> +- **Permission-/Sandbox-Modus:** +- **Toolfreigabe:** +- **Isolationsmechanismus:** - **MCP-Server / Agentendateien:** - **Subagenten:** - **Verschachtelung:** `spawned` = , davon `spawned_by_subagents` = , `max_depth` = . @@ -676,9 +724,13 @@ Das ist die **berichtete Aufwandsgröße** der Versuchsreihe. `total_cost_usd` b `RawResult.json` erhalten, wird aber nicht ins Protokoll übernommen: Token sind modell- und preisunabhängig und bleiben damit über Preisänderungen und Modellwechsel hinweg vergleichbar. +**Codex-Semantik:** `cached_input_tokens` ist eine Teilmenge von `input_tokens` und wird nicht +erneut addiert. Dort gilt `Tokens gesamt = input_tokens + output_tokens`; Reasoning-Tokens sind +eine Teilmenge der Output-Tokens. Cache-Write-Tokens werden von `codex exec --json` nicht geliefert. + ## Gefundene Anforderungen - ## Ergebnis @@ -709,8 +761,9 @@ Werkzeug umgesetzt wird und welche Messgrößen dieses Werkzeug liefert. ### Adapter: Claude Code -Der einzige derzeit ausgearbeitete Adapter. Alle konkreten Aufrufe, Flags und Feldnamen im -Prozessteil beziehen sich auf ihn; er wurde gegen **CLI 2.1.245** entwickelt und verifiziert. +Alle mit `claude -p`, `--safe-mode`, `--allowedTools`, `modelUsage` und `subagent_stats` +bezeichneten Aufrufe und Felder im Prozessteil gehören zu diesem Adapter. Er wurde gegen +**CLI 2.1.245** entwickelt und verifiziert. **Gelieferte Messgrößen** – vollständig aus `RawResult.json`: @@ -729,10 +782,114 @@ Prozessteil beziehen sich auf ihn; er wurde gegen **CLI 2.1.245** entwickelt und werden nicht ausgewiesen; der Effort steht **nicht** in `RawResult.json` und ist nur über das Session-Transkript belegbar (siehe Schritt 6 der Vorbereitung). +### Adapter: Codex CLI / OpenAI-Modelle + +Dieser Adapter wurde für den reproduzierbaren Headless-Betrieb mit `codex exec` und exakten +OpenAI-Modell-IDs entworfen. Referenzstand bei Einführung: **Codex CLI 0.149.0-alpha.4.3**. +Vor jedem Lauf `codex --version` protokollieren; bei geändertem JSONL-Schema den Normalisierer +zuerst mit einem kleinen, ausdrücklich freigegebenen Smoke-Test prüfen. + +**Unterstützter Agentenmodus:** In Version 5.0.0 nur `solo`. `builtin` und `custom` müssen +abbrechen. `solo` wird mit zwei unabhängigen Einstellungen erzwungen: +`--disable multi_agent` und `-c agents.enabled=false`. + +**Isolation und Ausgabe.** Codex arbeitet im Root mit `--sandbox read-only`. Anders als beim +Claude-Adapter erhält der Agent deshalb kein beschreibbares Zusatzverzeichnis. Er liefert ein +Schemaobjekt mit vollständigen Dateiinhalten; `--output-last-message` schreibt dieses durch die +CLI nach `_meta\final_response.json`. Erst nach Ende des Agenten materialisiert das lokale, +deterministische Skript die validierten relativen Pfade unter `Ergebnisse\`. Absolute Pfade, +`..`, Laufwerkspräfixe und case-insensitive Duplikate werden abgelehnt. + +Vor dem Start `$codex`, `$skillDir`, `$lauf`, `$root`, `$modell` und `$effort` auf absolute +Pfade beziehungsweise die bestätigten Versuchsbedingungen setzen. Den Prompt mit dem +Codex-Ausgabeblock aus Abschnitt 2 nach `_meta\combined_prompt.md` schreiben. Der eigentliche +Aufruf lautet: + +```powershell +$codexArgs = @( + '--model', $modell, + '-c', "model_reasoning_effort=`"$effort`"", + '-c', 'service_tier="default"', + '--sandbox', 'read-only', + '--ask-for-approval', 'never', + '--disable', 'multi_agent', + '-c', 'agents.enabled=false', + '--disable', 'plugins', + '--disable', 'apps', + '--disable', 'hooks', + '--disable', 'skill_search', + '--cd', $root, + 'exec', + '--ignore-user-config', + '--ignore-rules', + '--strict-config', + '--json', + '--output-schema', (Join-Path $skillDir 'codex-output-schema.json'), + '--output-last-message', (Join-Path $lauf '_meta\final_response.json'), + '-' +) + +Set-Content -Path "$lauf\_meta\startzeit.txt" -Value (Get-Date -Format o) +Get-Content "$lauf\_meta\combined_prompt.md" -Raw | + & $codex @codexArgs 2> "$lauf\Stderr.log" | + Set-Content "$lauf\RawEvents.jsonl" -Encoding utf8 +$codexExit = $LASTEXITCODE +Set-Content -Path "$lauf\_meta\exitcode.txt" -Value $codexExit +Set-Content -Path "$lauf\_meta\endzeit.txt" -Value (Get-Date -Format o) + +python (Join-Path $skillDir 'normalise-codex-result.py') $lauf ` + --model $modell --effort $effort +``` + +Auch dieser Aufruf ist als Background-Task zu starten. Das Erscheinen von `RawEvents.jsonl` +allein bedeutet noch nicht, dass der Lauf fertig ist; Ende ist der abgeschlossene Prozess plus +erzeugte `RawResult.json`. Schlägt die Normalisierung fehl, Rohdateien unverändert lassen und +den Lauf als Fehler protokollieren. + +**Warum diese Flags:** + +| Flag / Einstellung | Zweck | +|---|---| +| `--model ` | vollständige, vom User bestätigte OpenAI-Modell-ID | +| `model_reasoning_effort` | expliziter Denkaufwand | +| `service_tier="default"` | verhindert eine geerbte Fast-/Priority-Bedingung | +| `--sandbox read-only` | technische Schreibsperre für den Codebasis-Snapshot | +| `--ask-for-approval never` | keine interaktiven Unterbrechungen im Headless-Lauf | +| `--ignore-user-config`, `--ignore-rules` | keine User-Konfiguration und keine Exec-Regeln | +| `--disable plugins/apps/hooks/skill_search` | keine externen oder benutzerspezifischen Erweiterungen | +| `--disable multi_agent`, `agents.enabled=false` | keine Subagenten im Modus `solo` | +| `--json` | vollständiger maschinenlesbarer Ereignisstrom | +| `--output-schema`, `--output-last-message` | validierbare Ergebnisdateien ohne Schreibrecht des Agenten | + +`--ignore-user-config` lässt die gespeicherte Authentifizierung weiterhin nutzbar; niemals +`auth.json` kopieren oder in Laufartefakten ablegen. Live-Websuche ist nicht freigegeben, weil +`--search` fehlt. Der bereinigte Snapshot bleibt zusätzlich Pflicht, da projektlokale +Anweisungsdateien eine eigene Versuchsbedingung wären. + +**Gelieferte Messgrößen** aus `RawEvents.jsonl`, normalisiert nach `RawResult.json`: + +| Messgröße | Quelle | +|---|---| +| Abbruchstatus | Prozess-Exitcode, `turn.failed`, `error`, fehlerhafte JSONL-Zeilen | +| Wanduhrdauer | `_meta\startzeit.txt` bis `_meta\endzeit.txt` | +| Tokens gesamt | `turn.completed.usage.input_tokens + output_tokens` | +| Cache-Read-Tokens | `turn.completed.usage.cached_input_tokens`, Teilmenge der Input-Tokens | +| Reasoning-Tokens | `turn.completed.usage.reasoning_output_tokens`, Teilmenge der Output-Tokens | +| Agent-Turns | Anzahl `turn.completed` | +| Sitzungskennung | `thread.started.thread_id` | +| Werkzeugaktivität | Anzahl abgeschlossener `item.*` je Item-Typ | +| Erzeugte Artefakte | validierte Einträge aus `_meta\final_response.json` | + +**Nicht erfasst und niemals schätzen:** reine API-Dauer, Cache-Write-Tokens, einzelne +Permission-Denials und die tatsächlich serverseitig bediente Modell-ID. `--model` dokumentiert +die angeforderte ID; da `codex exec --json` sie im Ereignisstrom nicht wiederholt, lautet die +Modellkontrolle im Protokoll `nicht prüfbar`. Temperatur und weitere Sampling-Parameter sind in +diesem CLI-Ablauf nicht steuerbar; Effort und Service-Tier werden dagegen explizit festgelegt. + ### Weitere Adapter (für Versuch 4 nachzurüsten) -Kapitel 4 der Arbeit sieht einen LLM-Querschnitt über Codex CLI, Qwen Code CLI über LM Studio -und DeepSeek über die Cloud-API vor. Diese Adapter sind noch nicht ausgearbeitet. Damit ein +Kapitel 4 der Arbeit sieht zusätzlich Qwen Code CLI über LM Studio und DeepSeek über die +Cloud-API vor. Diese Adapter sind noch nicht ausgearbeitet. Damit ein Lauf als Messpunkt taugt, muss ein Adapter mindestens liefern: | Pflichtangabe | Zweck | @@ -789,6 +946,7 @@ der Historie unten – im selben Arbeitsschritt. | Version | Änderung | Grund | Verwendet in | |---|---|---|---| +| **5.0.0** | **Neuer Codex-CLI-Adapter für OpenAI-Modelle.** Exakte OpenAI-Modell-IDs, expliziter Reasoning-Effort und Service-Tier; technisch read-only ausgeführtes `codex exec`; schemaerzwungene Dateirückgabe; JSONL-Rohdaten und deterministische Normalisierung nach `RawResult.json`. Codex ist zunächst nur im Modus `solo` freigegeben. Zusätzlich fünf beschädigte Backslashes vor `analyse-anforderungen.py`, `anforderungen.*` und `after.txt` repariert. | Der bisherige Skill konnte nur Claude Code ausführen. Ein Codex-Lauf braucht andere Isolations-, Ausgabe- und Messmechanismen. MAJOR, weil Werkzeug, Rohdatenformat und Ergebnisübergabe neue Versuchsbedingungen bilden; der nächste Lauf eröffnet deshalb eine neue Iteration. | ab dem ersten Codex-Lauf | | **1.0.0** | Ausgangsfassung: `--permission-mode acceptEdits`, kein Shell-Zugriff; Isolation durch Löschen der KI-Konfigurationsdateien im Root vor dem Lauf und `git restore` danach | Erstaufsetzung des Versuchsaufbaus | Lauf 1 (`01_Lauf_2026-08-25_1228`) | | **2.0.0** | `--allowedTools "Bash" "PowerShell"` + 33er-Denylist für schreibende und bauende Kommandos. Isolation über **eingefrorenen Codebasis-Snapshot ohne KI-Konfigurationen** (Commit `79c1142`, Parent `89ccfd6`, GitHub-Remote entkoppelt), zusätzlich `--safe-mode` und `--strict-mcp-config`. Der destruktive Lösch-/Restore-Schritt pro Lauf entfällt. `permission_denials` und `subagent_stats` werden reguläre Messgrößen; Verbrauchstabelle trennt Hauptagent und Gesamtlauf. CLI-Pfad-Auflösung als eigener Schritt. | Lauf 1 erzeugte 36 Permission-Denials (32 × Bash, 4 × PowerShell); Shell-gestützte Verzeichnisinventuren fehlten dem Agenten. `--safe-mode` allein genügt nicht: Es unterdrückt das Vorladen von `CLAUDE.md`/`AGENTS.md`, verhindert aber nicht, dass der Agent sie mit Shell-Zugriff selbst liest – im Smoke-Test nachgewiesen. | Lauf 2 (`01_Lauf_2026-08-25_1349`, API-Abbruch) | | **2.0.1** | `before.txt` wird leerwertsicher geschrieben (`Set-Content -Value (… \| Out-String)` statt Pipeline) | Bei sauberem Root schreibt `Set-Content` aus leerer Pipeline die Datei nicht und lässt alten Inhalt stehen; der Vorher/Nachher-Vergleich meldete dadurch eine Abweichung, die es nicht gab | Lauf 3 (`01_Lauf_2026-08-25_1429`) | diff --git a/.claude/skills/run-experiment/codex-output-schema.json b/.claude/skills/run-experiment/codex-output-schema.json new file mode 100644 index 00000000..b20a77ad --- /dev/null +++ b/.claude/skills/run-experiment/codex-output-schema.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { "type": "string", "minLength": 1 }, + "content": { "type": "string" } + }, + "required": ["path", "content"], + "additionalProperties": false + } + }, + "summary": { "type": "string" } + }, + "required": ["files", "summary"], + "additionalProperties": false +} diff --git a/.claude/skills/run-experiment/normalise-codex-result.py b/.claude/skills/run-experiment/normalise-codex-result.py new file mode 100644 index 00000000..239fb2e2 --- /dev/null +++ b/.claude/skills/run-experiment/normalise-codex-result.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Materialize Codex result files and normalize its JSONL measurements.""" + +from __future__ import annotations + +import argparse +import json +from collections import Counter +from datetime import datetime +from pathlib import Path, PurePosixPath +from typing import Any + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("run_directory", type=Path) + parser.add_argument("--model", required=True) + parser.add_argument("--effort", required=True) + return parser.parse_args() + + +def read_jsonl(path: Path) -> tuple[list[dict[str, Any]], list[str]]: + events: list[dict[str, Any]] = [] + malformed: list[str] = [] + for number, line in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1): + if not line.strip(): + continue + try: + value = json.loads(line) + except json.JSONDecodeError as exc: + malformed.append(f"line {number}: {exc}") + continue + if isinstance(value, dict): + events.append(value) + else: + malformed.append(f"line {number}: JSON value is not an object") + return events, malformed + + +def safe_result_path(results_dir: Path, raw_path: str) -> Path: + relative = PurePosixPath(raw_path.replace("\\", "/")) + if relative.is_absolute() or not relative.parts or ".." in relative.parts: + raise ValueError(f"unsafe result path: {raw_path!r}") + if any(part in ("", ".") or ":" in part for part in relative.parts): + raise ValueError(f"invalid result path: {raw_path!r}") + target = results_dir.joinpath(*relative.parts).resolve() + root = results_dir.resolve() + if root != target and root not in target.parents: + raise ValueError(f"result path escapes Ergebnisse: {raw_path!r}") + return target + + +def parse_iso(path: Path) -> datetime | None: + if not path.exists(): + return None + value = path.read_text(encoding="utf-8-sig").strip() + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + + +def main() -> int: + args = parse_args() + run_dir = args.run_directory.resolve() + meta_dir = run_dir / "_meta" + results_dir = run_dir / "Ergebnisse" + events, malformed = read_jsonl(run_dir / "RawEvents.jsonl") + envelope = json.loads((meta_dir / "final_response.json").read_text(encoding="utf-8-sig")) + if not isinstance(envelope, dict) or not isinstance(envelope.get("files"), list): + raise ValueError("final_response.json does not match the Codex output envelope") + + results_dir.mkdir(parents=True, exist_ok=True) + seen: set[str] = set() + materialized: list[str] = [] + for entry in envelope["files"]: + if not isinstance(entry, dict): + raise ValueError("file entry is not an object") + raw_path = entry.get("path") + content = entry.get("content") + if not isinstance(raw_path, str) or not isinstance(content, str): + raise ValueError("file entry requires string path and content") + target = safe_result_path(results_dir, raw_path) + key = str(target).casefold() + if key in seen: + raise ValueError(f"duplicate result path: {raw_path!r}") + seen.add(key) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content, encoding="utf-8", newline="\n") + materialized.append(str(target.relative_to(results_dir)).replace("\\", "/")) + + usage = Counter() + item_types = Counter() + errors: list[Any] = list(malformed) + thread_id = None + turns = 0 + for event in events: + event_type = event.get("type") + if event_type == "thread.started": + thread_id = event.get("thread_id") + elif event_type == "turn.completed": + turns += 1 + turn_usage = event.get("usage") or {} + for field in ("input_tokens", "cached_input_tokens", "output_tokens", "reasoning_output_tokens"): + value = turn_usage.get(field, 0) + if isinstance(value, int): + usage[field] += value + elif event_type in ("turn.failed", "error"): + errors.append(event) + if event_type == "item.completed": + item = event.get("item") or {} + if isinstance(item.get("type"), str): + item_types[item["type"]] += 1 + + start = parse_iso(meta_dir / "startzeit.txt") + end = parse_iso(meta_dir / "endzeit.txt") + duration_ms = round((end - start).total_seconds() * 1000) if start and end else None + + exit_code = None + exit_code_path = meta_dir / "exitcode.txt" + if exit_code_path.exists(): + try: + exit_code = int(exit_code_path.read_text(encoding="utf-8-sig").strip()) + except ValueError: + errors.append("invalid exitcode.txt") + if exit_code not in (None, 0): + errors.append({"exit_code": exit_code}) + + normalized = { + "adapter": "codex-cli", + "is_error": bool(errors), + "subtype": "success" if not errors else "error", + "session_id": thread_id, + "duration_ms": duration_ms, + "duration_api_ms": None, + "num_turns": turns, + "requested_model": args.model, + "actual_models": None, + "model_control": "not_verifiable_from_codex_exec_jsonl", + "effort": args.effort, + "usage": { + "input_tokens": usage["input_tokens"], + "cached_input_tokens": usage["cached_input_tokens"], + "output_tokens": usage["output_tokens"], + "reasoning_output_tokens": usage["reasoning_output_tokens"], + "total_tokens": usage["input_tokens"] + usage["output_tokens"], + "semantics": "cached_input_tokens is a subset of input_tokens and is not added again", + }, + "item_counts": dict(sorted(item_types.items())), + "permission_denials": None, + "subagent_stats": {"spawned": 0, "source": "multi-agent disabled by configuration"}, + "materialized_files": materialized, + "result": envelope.get("summary", ""), + "errors": errors, + } + (run_dir / "RawResult.json").write_text( + json.dumps(normalized, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + return 1 if errors else 0 + + +if __name__ == "__main__": + raise SystemExit(main())