Iteration 3: Modell- und Modusraster erweitert, Skill 7.0.0, V2/V3 vorbereitet
Neue gueltige Zellen in Iteration 3 - claude-opus-5/solo/high: 363 Anforderungen, 99,7 % mit Primaerbeleg, Belege je Anforderung Median 2,0, 38,1 Mio. Tokens - claude-fable-5/solo/high: 241 Anforderungen, 98,3 % mit Primaerbeleg, 89,0 % PRIMAER-Anteil, vollstaendig regelkonform, 30,6 Mio. Tokens Damit sind 6 von 12 Zellen des Rasters belegt. Zwei Befunde daraus: Die Belegdichte folgt dem Modell, nicht dem Effort. Opus erreicht Median 2,0 auch auf high; alle 44 Sonnet-Laeufe lagen bei 1,0. max hebt Opus auf 3,0. Die frueher dem Effort zugeschriebene Verdopplung ist damit eingegrenzt. Die Fable-Modellverletzung ist reproduziert und abgegrenzt. Bei builtin laufen die Subagenten auf claude-opus-5[1m] statt Fable (zweiter Fall nach Iteration 1), bei solo dagegen sauber. Nicht das Modell ist die Ursache, sondern Fable in Kombination mit Delegation. Fehlmessungen, vollstaendig protokolliert - vier 429-Abbrueche (Session-Kontingent) aus dem Parallelblock 19:59; drei davon mit Teilbestand, einer ohne Ergebnis - opus-5/builtin/high zum dritten Mal gescheitert: 790,7 Mio. Tokens ueber drei Anlaeufe ohne Artefakt. Zelle mit dieser Prompt-Version nicht messbar. Skill 7.0.0 (MAJOR) - Isolationsmechanismus modusabhaengig: --safe-mode schaltet MCP-Server und Custom-Agenten ab und ist mit V2/V3 unvereinbar. Smoke-Test verifiziert: mit Flag spawned=0, ohne Flag spawned=2. Ersatz fuer custom/MCP: --strict-mcp-config plus --disallowedTools Skill WebSearch WebFetch SlashCommand. - 6.1.0: Pflichtpruefung leeres Ergebnisverzeichnis = Fehlmessung unabhaengig von is_error; CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0; Protokollfeld Gueltigkeit - extract-subagenten.py: Start-Quittung wird nicht mehr als Ertragsmass ausgewiesen Versuch 2 und 3 vorbereitet - Prompt-Kette V1 -> V2 (02-A, angepasst an Agentendateien) -> V3 (02-B, MCP) - V2: acht Rollen inkl. nicht delegierendem ISO-29148-Orchestrator - V3: elf Rollen, fuenf Werkzeugserver, neue Belegklasse LAUFZEIT Ablaufprotokoll um Phase 6 und 7 sowie die Vorbereitung von V2/V3 ergaenzt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
affde3a45f
commit
3d5b691bfa
@@ -2,7 +2,7 @@
|
||||
name: run-experiment
|
||||
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 <Pfad-zur-Prompt-Datei>" oder wenn der User einen Versuch/ein Experiment ausführen und tracken will.
|
||||
argument-hint: <Pfad zur Prompt-Datei> <Root-Verzeichnis>
|
||||
version: 5.0.0
|
||||
version: 7.0.0
|
||||
---
|
||||
|
||||
# RunExperiment – Versuchslauf mit Messprotokoll
|
||||
@@ -16,8 +16,9 @@ schreibst du ein Messprotokoll neben die Prompt-Datei.
|
||||
|
||||
- **Prompt** (Pflicht): Pfad zur Prompt-Datei (Markdown). Erstes Argument in `$ARGUMENTS`.
|
||||
- **Root** (Pflicht): Verzeichnis, das als Root des Versuchslaufs dient. Zweites Argument.
|
||||
Der Headless-Lauf wird mit diesem Verzeichnis als Arbeitsverzeichnis gestartet – es ist
|
||||
die Wurzel der zu analysierenden Codebasis und wird nur GELESEN. Fehlt das Argument: den
|
||||
Es ist die Wurzel der zu analysierenden Codebasis und wird nur GELESEN. Der Codex-Adapter
|
||||
erstellt daraus ein isoliertes temporäres Arbeitsabbild; andere Adapter starten
|
||||
unmittelbar mit diesem Root als Arbeitsverzeichnis. Fehlt das Argument: den
|
||||
User danach fragen und NICHT stillschweigend das aktuelle Verzeichnis verwenden. Vor dem
|
||||
Lauf prüfen, dass das Verzeichnis existiert; sonst abbrechen und den User informieren.
|
||||
- **Modell** (Pflicht, **kein Default**): Wird **vor jedem Lauf beim User erfragt** – siehe
|
||||
@@ -216,7 +217,7 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen"
|
||||
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
|
||||
**Codex-Adapter ab 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.
|
||||
@@ -255,7 +256,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 = 'v5.0.0' # entspricht version: im Frontmatter dieses Skills
|
||||
$skillVer = 'v7.0.0' # entspricht version: im Frontmatter dieses Skills
|
||||
do {
|
||||
$id4 = '{0:x4}' -f (Get-Random -Maximum 65536)
|
||||
$lauf = Join-Path "<promptverzeichnis>\$zelle" "<NN>_Lauf_$(Get-Date -Format 'yyyy-MM-dd_HHmmss')_${skillVer}-$id4"
|
||||
@@ -347,15 +348,16 @@ 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;
|
||||
**Codex-Abweichung bei Block 2.** Der Codex-Adapter läuft in einer isolierten Arbeitskopie
|
||||
im System-Temp-Verzeichnis mit `workspace-write`. Das originale Root liegt außerhalb dieses
|
||||
Arbeitsbereichs und kann vom Agenten nicht erreicht werden. Trotz des technisch beschreibbaren
|
||||
Abbilds darf der Agent keine Dateien verändern. 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.
|
||||
Verändere keine Dateien im Arbeitsverzeichnis. 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.
|
||||
```
|
||||
@@ -376,6 +378,9 @@ $lauf = "<absoluter Pfad zum Laufverzeichnis>"
|
||||
$prompt = (Get-Content "<prompt-datei>" -Raw) + "`n`n<Ausgabe-Anweisung>"
|
||||
# Steuerdateien IMMER ins Laufverzeichnis - nie in den gemeinsamen Scratchpad (parallelfaehig)
|
||||
Set-Content -Path "$lauf\_meta\combined_prompt.md" -Value $prompt -Encoding utf8
|
||||
# Ohne diese Variable bricht der Headless-Modus nach 600 s ab, sobald noch
|
||||
# Hintergrund-Subagenten laufen - und meldet dabei `is_error: false`.
|
||||
$env:CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS = '0'
|
||||
Set-Content -Path "$lauf\_meta\startzeit.txt" -Value (Get-Date -Format o)
|
||||
|
||||
# Schreibende und bauende Shell-Kommandos sperren – die Codebasis wird nur gelesen.
|
||||
@@ -414,7 +419,7 @@ Bedeutung der Flags:
|
||||
|
||||
| Flag | Zweck |
|
||||
|---|---|
|
||||
| `--safe-mode` | Isolation: CLAUDE.md, Skills, Plugins, Hooks, MCP-Server, Custom-Agenten, Commands und Output-Styles aus – ohne Dateieingriff. Auth, Modellwahl, eingebaute Tools und Permissions bleiben normal aktiv. |
|
||||
| `--safe-mode` | Isolation: CLAUDE.md, Skills, Plugins, Hooks, MCP-Server, Custom-Agenten, Commands und Output-Styles aus – ohne Dateieingriff. Auth, Modellwahl, eingebaute Tools und Permissions bleiben normal aktiv. **Nur in den Modi `solo` und `builtin` verwendbar** – siehe „Isolation je Agentenmodus". |
|
||||
| `--strict-mcp-config` | zweite Absicherung gegen MCP-Server aus Projekt- oder User-Konfiguration |
|
||||
| `--permission-mode acceptEdits` | Schreibrechte für die Ergebnisdateien im Laufverzeichnis |
|
||||
| `--allowedTools "Bash" "PowerShell"` | **Shell-Zugriff ohne Rückfrage** – Standard seit Prompt-Version 02 |
|
||||
@@ -434,6 +439,55 @@ Die belastbare Read-only-Garantie bleibt der Vorher/Nachher-Vergleich per
|
||||
`git status --porcelain` aus Abschnitt 1 bzw. 3. Die Denylist senkt das Risiko, sie ersetzt die
|
||||
Verifikation nicht.
|
||||
|
||||
**Isolation je Agentenmodus – `--safe-mode` ist nicht immer verwendbar.**
|
||||
|
||||
`--safe-mode` schaltet ausweislich der CLI-Hilfe „all customizations (CLAUDE.md, skills, plugins,
|
||||
hooks, **MCP servers, custom commands and agents**, output styles, workflows, …)" ab. Damit
|
||||
deaktiviert es genau das, was die Modi `custom` (V2) und der MCP-Einsatz (V3) untersuchen sollen.
|
||||
|
||||
Verifiziert am 2026-08-26 gegen CLI 2.1.246 mit identischem Aufruf, nur `--safe-mode` variiert:
|
||||
|
||||
| Konfiguration | `subagent_stats.spawned` | `by_type` |
|
||||
|---|---:|---|
|
||||
| mit `--safe-mode` | **0** | leer – der Agent meldet, die Rollen seien „nicht in der Agent-Registry registriert" |
|
||||
| ohne `--safe-mode` | **2** | `{"modulinventar": 1, "konsistenzpruefer": 1}` |
|
||||
|
||||
Daraus folgt eine modusabhängige Isolation:
|
||||
|
||||
| Modus | Isolation |
|
||||
|---|---|
|
||||
| `solo`, `builtin` | `--safe-mode` + `--strict-mcp-config` (unverändert) |
|
||||
| `custom`, sowie jeder Lauf mit `--mcp-config` | **kein** `--safe-mode`; stattdessen `--strict-mcp-config` und die Sperre `--disallowedTools Skill WebSearch WebFetch SlashCommand` |
|
||||
|
||||
Der gezielte Ersatz wurde am selben Tag gegengeprüft. Der Agent antwortete auf eine
|
||||
Werkzeugabfrage: `VORGELADEN: NICHTS VORGELADEN` · `SKILLS: KEINE` · `WEB: KEIN WEBZUGRIFF` ·
|
||||
alle acht Custom-Rollen verfügbar. Ohne die Sperre lädt die CLI **16 global installierte Skills**
|
||||
(darunter `code-review`, `security-review`, `run`, `init`); `--setting-sources ''` unterdrückt sie
|
||||
**nicht**.
|
||||
|
||||
**Was der Ersatz nicht abdeckt.** `--safe-mode` deaktiviert zusätzlich Plugins, Hooks und
|
||||
Output-Styles. Die Sperre tut das nicht. Auf der Maschine, auf der die Versuchsreihe läuft, sind
|
||||
davon keine konfiguriert (`~/.claude/settings.json` enthält nur `model` und
|
||||
`agentPushNotifEnabled`, kein `hooks`; kein `plugins`- und kein `output-styles`-Verzeichnis) –
|
||||
das ist jedoch eine Eigenschaft der Umgebung, keine Garantie. **Vor jedem Lauf im Modus `custom`
|
||||
oder mit MCP ist deshalb zu prüfen:**
|
||||
|
||||
```powershell
|
||||
$us = Join-Path $env:USERPROFILE '.claude\settings.json'
|
||||
if (Test-Path $us) { (Get-Content $us -Raw | ConvertFrom-Json).PSObject.Properties.Name }
|
||||
foreach ($d in 'plugins','output-styles','skills') {
|
||||
$pfad = Join-Path $env:USERPROFILE ".claude\$d"
|
||||
if (Test-Path $pfad) { "VORHANDEN: $d -> " + ((Get-ChildItem $pfad | Measure-Object).Count) + ' Eintraege' }
|
||||
}
|
||||
```
|
||||
|
||||
Treten Hooks, Plugins oder Output-Styles auf, ist der Lauf **nicht** isoliert und die Bedingung
|
||||
im Protokoll als abweichend zu kennzeichnen.
|
||||
|
||||
**Unverändert bleibt:** `--safe-mode` hat die Modellwahl nie beeinflusst. Der Eintrag `model` in
|
||||
den User-Settings (hier `opus[1m]`) kann Subagenten binden – das ist der bekannte Grund der zwei
|
||||
dokumentierten Modellabweichungen und gilt mit wie ohne `--safe-mode`.
|
||||
|
||||
**`--safe-mode` bleibt als zweite Sicherung aktiv.** Die primäre Isolation leistet der
|
||||
bereinigte Snapshot (Abschnitt 1); `--safe-mode` fängt zusätzlich alles ab, was von außerhalb
|
||||
des Roots wirken könnte – User-Level-Settings, global installierte Skills, Plugins, Hooks.
|
||||
@@ -542,6 +596,32 @@ Subagenten gibt.
|
||||
Bei Modus `builtin` oder `custom` daher **nie** den Flag-Wert allein ins Protokoll schreiben,
|
||||
sondern die Modelle aus `modelUsage` mit ihrem jeweiligen Anteil ausweisen.
|
||||
|
||||
**Pflichtprüfung: Hat der Lauf überhaupt etwas erzeugt?** `is_error` allein genügt **nicht**.
|
||||
Ein Lauf kann `is_error: false`, `subtype: success` und `terminal_reason: completed` melden und
|
||||
trotzdem **null Ergebnisdateien** hinterlassen. Belegt am 2026-08-26, Lauf
|
||||
`Iteration 3/claude-opus-5/builtin/high/…160037_v4.5.0-116d`: Der Hauptagent startete zehn
|
||||
Subagenten im Hintergrund, beendete seinen Turn und schrieb als Abschlusstext „Die Erhebung
|
||||
läuft"; der Headless-Modus wartete 600 s und brach dann ab. `Stderr.log` enthielt
|
||||
„Background tasks still running after 600s; terminating." Verbraucht waren zu diesem Zeitpunkt
|
||||
**193,4 Mio. Tokens** – der teuerste Lauf der Reihe, ohne ein einziges Artefakt.
|
||||
|
||||
Nach jedem Lauf daher zwingend prüfen:
|
||||
|
||||
1. `Ergebnisse\` ist **nicht leer** und enthält die vom Prompt geforderten Dateien. Fehlen sie,
|
||||
ist der Lauf **unabhängig von `is_error` als Fehlmessung zu kennzeichnen**.
|
||||
2. `Stderr.log` enthält keine Abbruchmeldung. Die Datei ist im Normalfall 0 Byte groß.
|
||||
|
||||
Vorbeugend setzt der Ausführungsabschnitt `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0`, damit der
|
||||
Lauf auf seine Hintergrund-Subagenten wartet, statt sie abzuschneiden.
|
||||
|
||||
**Subagenten-Ergebnisse kommen nicht als Werkzeugergebnis zurück.** Startet der Hauptagent einen
|
||||
Subagenten im Hintergrund, liefert der Werkzeugaufruf sofort eine **Start-Quittung** von rund
|
||||
1.093 Zeichen („Async agent launched successfully …") zurück, nicht die Befunde. Deren Länge ist
|
||||
folglich **kein** Maß für den Ertrag des Subagenten. Die Quittung nennt einen Pfad
|
||||
`<temp>\<session>\tasks\<agentId>.output`; diese Dateien werden zwar angelegt, bleiben aber
|
||||
**leer** (geprüft an 33 Dateien aus zwei Läufen). Die Aussage, dass Subagenten-Transkripte nicht
|
||||
auswertbar persistiert werden, gilt damit unverändert.
|
||||
|
||||
`permission_denials` ist eine **reguläre Messgröße** und immer auszuweisen, auch bei 0. Ein
|
||||
hoher Wert bedeutet, dass die Werkzeugkonfiguration den Lauf eingeschränkt hat, und ist bei der
|
||||
Interpretation der Ergebnisqualität zu berücksichtigen.
|
||||
@@ -683,7 +763,11 @@ Vorlage:
|
||||
- **Permission-/Sandbox-Modus:** <acceptEdits | read-only / approval never | ...>
|
||||
- **Toolfreigabe:** <adapterabhängige Flags wörtlich>
|
||||
- **Isolationsmechanismus:** <adapterabhängige Flags wörtlich>
|
||||
- **MCP-Server / Agentendateien:** <keine – aus dem Snapshot entfernt, zusätzlich --safe-mode | Liste>
|
||||
- **MCP-Server / Agentendateien:** <keine – aus dem Snapshot entfernt, zusätzlich --safe-mode |
|
||||
Liste der Server mit Pfad und SHA-256 der `--mcp-config`-Datei; Pfad und SHA-256 der
|
||||
`--agents`-Datei>
|
||||
- **Umgebungsprüfung (nur `custom` / MCP):** <keine Hooks, Plugins, Output-Styles im
|
||||
User-Profil vorgefunden | **abweichend**: welche>
|
||||
- **Subagenten:** <Anzahl und Typ aus `subagent_stats`, z. B. 8 × Explore, 0 fehlgeschlagen>
|
||||
- **Verschachtelung:** `spawned` = <N>, davon `spawned_by_subagents` = <M>, `max_depth` = <D>.
|
||||
Bei `max_depth` > 1 ausdrücklich vermerken – die Tokens der tieferen Ebenen sind in
|
||||
@@ -742,6 +826,8 @@ Status, Regelkonformität>
|
||||
- **Kontrolle Agentenmodus:** `subagent_stats.spawned` = <Zahl> (bei `solo` muss 0 stehen,
|
||||
sonst Fehlmessung)
|
||||
- **Subagenten-Prompts:** <`_meta\subagenten.md`, N Aufrufe erfasst | entfällt (Modus solo)>
|
||||
- **Gültigkeit:** <gültig | **Fehlmessung**: Ergebnisverzeichnis leer / Abbruchmeldung in
|
||||
Stderr.log – Wortlaut zitieren>
|
||||
- **Erzeugte Dateien:** <Liste aus Ergebnisse\>
|
||||
- **Root unverändert:** <ja | nein: welche Abweichungen>
|
||||
- **Abschlusstext des Agenten:** siehe RawResult.json (`result`)
|
||||
@@ -789,28 +875,47 @@ OpenAI-Modell-IDs entworfen. Referenzstand bei Einführung: **Codex CLI 0.149.0-
|
||||
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
|
||||
**Unterstützter Agentenmodus:** Auch in Version 6.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.
|
||||
**Isolation und Ausgabe.** Unter Windows blockierte `--sandbox read-only` mit Codex CLI
|
||||
0.149.0-alpha.4.3 bereits den Start rein lesender Prozesse (`pwsh`, `cmd`, `rg`). Der Adapter
|
||||
erstellt deshalb vor dem Lauf eine isolierte Kopie der versionierten und nicht ignorierten
|
||||
Quelldateien im System-Temp-Verzeichnis und startet Codex dort mit `--sandbox workspace-write`.
|
||||
Das originale Root liegt außerhalb des Codex-Arbeitsbereichs und bleibt technisch getrennt.
|
||||
`prepare-codex-workspace.py` hasht die Kopie vor und nach dem Lauf; jede Änderung macht den Lauf
|
||||
ungültig. Quelle, Dateizahl, Größe und beide Manifeste werden unter `_meta` archiviert; die
|
||||
temporäre Kopie wird erst nach der Integritätsprüfung entfernt. Die Ablage außerhalb des
|
||||
IDE-Projektbaums verhindert automatische Design-Time-Restores, die sonst ungefragt `obj`-Dateien
|
||||
in der Kopie erzeugen.
|
||||
|
||||
Der Agent liefert weiterhin ausschließlich 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:
|
||||
Codex-Ausgabeblock aus Abschnitt 2 nach `_meta\combined_prompt.md` schreiben. Anschließend das
|
||||
isolierte Abbild anlegen; bei einem Fehler darf der API-Lauf nicht starten:
|
||||
|
||||
```powershell
|
||||
$meta = Join-Path $lauf '_meta'
|
||||
$workspace = Join-Path ([IO.Path]::GetTempPath()) "codex-experiment-<Laufverzeichnis-ID>"
|
||||
python (Join-Path $skillDir 'prepare-codex-workspace.py') create $root $workspace $meta
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Codex-Arbeitsabbild konnte nicht erstellt werden.' }
|
||||
```
|
||||
|
||||
Der eigentliche Aufruf lautet:
|
||||
|
||||
```powershell
|
||||
$codexArgs = @(
|
||||
'--model', $modell,
|
||||
'-c', "model_reasoning_effort=`"$effort`"",
|
||||
'-c', 'service_tier="default"',
|
||||
'--sandbox', 'read-only',
|
||||
'--sandbox', 'workspace-write',
|
||||
'--ask-for-approval', 'never',
|
||||
'--disable', 'multi_agent',
|
||||
'-c', 'agents.enabled=false',
|
||||
@@ -818,7 +923,7 @@ $codexArgs = @(
|
||||
'--disable', 'apps',
|
||||
'--disable', 'hooks',
|
||||
'--disable', 'skill_search',
|
||||
'--cd', $root,
|
||||
'--cd', $workspace,
|
||||
'exec',
|
||||
'--ignore-user-config',
|
||||
'--ignore-rules',
|
||||
@@ -837,14 +942,20 @@ $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 'prepare-codex-workspace.py') verify $workspace $meta
|
||||
$workspaceExit = $LASTEXITCODE
|
||||
|
||||
python (Join-Path $skillDir 'normalise-codex-result.py') $lauf `
|
||||
--model $modell --effort $effort
|
||||
|
||||
python (Join-Path $skillDir 'prepare-codex-workspace.py') cleanup $workspace $meta
|
||||
```
|
||||
|
||||
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.
|
||||
Integritätsprüfung und erzeugte `RawResult.json`. Ist `$workspaceExit` ungleich null, den Lauf
|
||||
wegen einer veränderten Arbeitskopie als ungültig kennzeichnen. Schlägt die Normalisierung fehl,
|
||||
Rohdateien unverändert lassen und den Lauf als Fehler protokollieren.
|
||||
|
||||
**Warum diese Flags:**
|
||||
|
||||
@@ -853,13 +964,13 @@ den Lauf als Fehler protokollieren.
|
||||
| `--model <id>` | 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 |
|
||||
| `--sandbox workspace-write` | erlaubt Windows-Prozessstarts nur innerhalb der isolierten Arbeitskopie |
|
||||
| `--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 |
|
||||
| `--output-schema`, `--output-last-message` | validierbare Ergebnisdateien außerhalb des Arbeitsabbilds |
|
||||
|
||||
`--ignore-user-config` lässt die gespeicherte Authentifizierung weiterhin nutzbar; niemals
|
||||
`auth.json` kopieren oder in Laufartefakten ablegen. Live-Websuche ist nicht freigegeben, weil
|
||||
@@ -946,6 +1057,9 @@ der Historie unten – im selben Arbeitsschritt.
|
||||
|
||||
| Version | Änderung | Grund | Verwendet in |
|
||||
|---|---|---|---|
|
||||
| **7.0.0** | **Isolationsmechanismus wird modusabhängig.** In den Modi `solo` und `builtin` unverändert `--safe-mode`; im Modus `custom` und bei jedem Lauf mit `--mcp-config` **kein** `--safe-mode`, stattdessen `--strict-mcp-config` plus `--disallowedTools Skill WebSearch WebFetch SlashCommand`. Neue Pflicht-Umgebungsprüfung auf Hooks, Plugins und Output-Styles im User-Profil; neue Protokollfelder für die MCP-Konfiguration und die Umgebungsprüfung. | `--safe-mode` schaltet ausweislich der CLI-Hilfe „MCP servers, custom commands and agents" ab – also genau das, was V2 und V3 untersuchen. Smoke-Test am 2026-08-26 (CLI 2.1.246, identischer Aufruf, nur `--safe-mode` variiert): mit Flag `spawned` = 0 und die Meldung, die Rollen seien „nicht in der Agent-Registry registriert"; ohne Flag `spawned` = 2 mit `{"modulinventar": 1, "konsistenzpruefer": 1}`. Ohne Ersatz hätte der erste V2-Lauf stillschweigend als V1-Lauf gemessen. Der Ersatz wurde gegengeprüft: nichts vorgeladen, keine Skills, kein Webzugriff, alle Rollen verfügbar. **MAJOR: Läufe im Modus `custom` sind hinsichtlich der Isolation nicht unmittelbar mit `solo`- und `builtin`-Läufen vergleichbar** – Plugins, Hooks und Output-Styles sind dort nicht durch das Flag, sondern nur durch die Umgebungsprüfung ausgeschlossen. | ab dem ersten V2-Lauf |
|
||||
| **6.1.0** | Zwei adapterunabhängige Pflichtprüfungen nach jedem Lauf: **(a)** `Ergebnisse\` darf nicht leer sein – sonst Fehlmessung, unabhängig von `is_error`; **(b)** `Stderr.log` auf Abbruchmeldungen prüfen. Der Ausführungsabschnitt setzt `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0`. Neues Protokollfeld „Gültigkeit". Dokumentiert, dass die Rückgabe eines Hintergrund-Subagenten eine Start-Quittung ist und ihre Länge kein Ertragsmaß. | Lauf `…160037_v4.5.0-116d` meldete `is_error: false`, `subtype: success` und lieferte **null Ergebnisdateien** bei 193,4 Mio. Tokens – der Headless-Modus hatte nach 600 s abgebrochen, während zehn Hintergrund-Subagenten noch liefen. Ohne die neue Prüfung wäre der Lauf als gültiger Messpunkt mit „0 Anforderungen" in den Modellvergleich eingegangen. MINOR: neue Prüfschritte; die Umgebungsvariable ändert die Versuchsbedingung für künftige `builtin`-Läufe und ist dort zu vermerken. | ab sofort |
|
||||
| **6.0.0** | **Windows-taugliche Codex-Isolation.** Pro Lauf wird im System-Temp-Verzeichnis außerhalb des IDE-Projektbaums eine Kopie der versionierten und nicht ignorierten Dateien des Codebasis-Roots erstellt. Codex läuft ausschließlich dort mit `--sandbox workspace-write`; SHA-256-Manifeste vor und nach dem Lauf erkennen jede Änderung. Ergebnisse und Nachweise bleiben im Laufverzeichnis, die temporäre Kopie wird danach entfernt. | Der erste Codex-Lauf mit 5.0.0 konnte fachlich nicht starten, weil die Windows-Read-only-Sandbox sämtliche lesenden Kindprozesse vor dem Start blockierte. Eine Kopie im Laufverzeichnis wurde zudem vom C#-Projektservice automatisch mit ignorierten `obj`-Dateien verändert. MAJOR, weil Sandboxmodus und Arbeitsverzeichnis eine neue Versuchsbedingung bilden; der erste Lauf eröffnet deshalb eine neue Iteration. | ab dem ersten wiederholten Codex-Lauf |
|
||||
| **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) |
|
||||
|
||||
@@ -64,6 +64,10 @@ for a in aufrufe:
|
||||
txt = ergebnisse.get(a['id']) or ''
|
||||
a['ergebnis_zeichen'] = len(txt)
|
||||
a['abgewiesen'] = ABSAGE in txt
|
||||
# Ein im Hintergrund gestarteter Subagent liefert sofort eine Start-Quittung
|
||||
# ('Async agent launched successfully'), nicht seine Befunde. Ihre Laenge ist
|
||||
# KEIN Ertragsmass - sie ist fuer alle Hintergrund-Subagenten praktisch gleich.
|
||||
a['nur_startquittung'] = 'Async agent launched successfully' in txt
|
||||
|
||||
abgewiesen = [a for a in aufrufe if a['abgewiesen']]
|
||||
echte = [a for a in aufrufe if not a['abgewiesen']]
|
||||
@@ -102,8 +106,11 @@ for i, a in enumerate(echte, 1):
|
||||
'',
|
||||
'- **Werkzeug:** `%s` **Typ:** `%s` **Hintergrund:** %s'
|
||||
% (a['werkzeug'], a['subagent_type'], a['run_in_background']),
|
||||
'- **Prompt-Zeichen:** %d **Ergebnis-Zeichen:** %s'
|
||||
% (len(a['prompt']), a['ergebnis_zeichen']),
|
||||
'- **Prompt-Zeichen:** %d **Rueckgabe:** %s'
|
||||
% (len(a['prompt']),
|
||||
'Start-Quittung (Befunde kommen per Benachrichtigung, nicht als '
|
||||
'Werkzeugergebnis - Laenge ist kein Ertragsmass)'
|
||||
if a.get('nur_startquittung') else '%s Zeichen' % a['ergebnis_zeichen']),
|
||||
'', '### Prompt', '', '```', a['prompt'].rstrip(), '```', '']
|
||||
io.open(os.path.join(meta, 'subagenten.md'), 'w', encoding='utf-8').write('\n'.join(md))
|
||||
|
||||
@@ -111,6 +118,8 @@ print('Subagenten gefunden: %d echte%s (direkt erwartet: %s, gesamt: %s, davon v
|
||||
% (len(echte), (', %d abgewiesen' % len(abgewiesen)) if abgewiesen else '',
|
||||
erwartet, gesamt, verschachtelt))
|
||||
for a in echte:
|
||||
print(' - %-42s %s Prompt %6d Z. Ergebnis %s Z.'
|
||||
% ((a['description'] or '')[:42], a['subagent_type'], len(a['prompt']), a['ergebnis_zeichen']))
|
||||
print(' - %-42s %-16s Prompt %6d Z. %s'
|
||||
% ((a['description'] or '')[:42], a['subagent_type'], len(a['prompt']),
|
||||
'Hintergrund (Start-Quittung)' if a.get('nur_startquittung')
|
||||
else 'Ergebnis %s Z.' % a['ergebnis_zeichen']))
|
||||
print('geschrieben:', os.path.join(meta, 'subagenten.md'))
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create and verify an isolated Codex analysis workspace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(message)
|
||||
|
||||
|
||||
def resolved(path: str) -> Path:
|
||||
return Path(path).resolve()
|
||||
|
||||
|
||||
def native_path(path: Path) -> str:
|
||||
value = str(path)
|
||||
if os.name == "nt" and not value.startswith("\\\\?\\"):
|
||||
return "\\\\?\\" + value
|
||||
return value
|
||||
|
||||
|
||||
def allowed_workspace(workspace: Path, meta: Path) -> bool:
|
||||
temporary_root = Path(tempfile.gettempdir()).resolve()
|
||||
return (
|
||||
(workspace.parent == meta and workspace.name == "workspace")
|
||||
or (workspace.parent == temporary_root and workspace.name.startswith("codex-experiment-"))
|
||||
)
|
||||
|
||||
|
||||
def git_output(cwd: Path, *args: str) -> bytes:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(cwd), *args],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def source_files(source: Path) -> tuple[list[Path], str | None]:
|
||||
try:
|
||||
top = resolved(git_output(source, "rev-parse", "--show-toplevel").decode().strip())
|
||||
relative_source = source.relative_to(top)
|
||||
raw = git_output(
|
||||
top,
|
||||
"ls-files",
|
||||
"-z",
|
||||
"--cached",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
"--",
|
||||
relative_source.as_posix(),
|
||||
)
|
||||
paths = []
|
||||
for entry in raw.split(b"\0"):
|
||||
if not entry:
|
||||
continue
|
||||
candidate = resolved(top / os.fsdecode(entry))
|
||||
if candidate.is_file() and candidate.is_relative_to(source):
|
||||
paths.append(candidate)
|
||||
return sorted(set(paths), key=lambda item: item.as_posix().casefold()), str(top)
|
||||
except (subprocess.CalledProcessError, ValueError):
|
||||
paths = [item for item in source.rglob("*") if item.is_file() and ".git" not in item.parts]
|
||||
return sorted(paths, key=lambda item: item.as_posix().casefold()), None
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with open(native_path(path), "rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest().upper()
|
||||
|
||||
|
||||
def manifest(workspace: Path) -> dict[str, dict[str, int | str]]:
|
||||
result: dict[str, dict[str, int | str]] = {}
|
||||
workspace_native = native_path(workspace)
|
||||
files: list[tuple[str, Path]] = []
|
||||
for directory, _, names in os.walk(workspace_native):
|
||||
for name in names:
|
||||
full_path = Path(directory) / name
|
||||
relative = os.path.relpath(str(full_path), workspace_native).replace("\\", "/")
|
||||
files.append((relative, full_path))
|
||||
for relative, path in sorted(files, key=lambda item: item[0].casefold()):
|
||||
result[relative] = {"size": os.stat(str(path)).st_size, "sha256": sha256(path)}
|
||||
return result
|
||||
|
||||
|
||||
def write_json(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def create(source_arg: str, workspace_arg: str, meta_arg: str) -> None:
|
||||
source = resolved(source_arg)
|
||||
workspace = resolved(workspace_arg)
|
||||
meta = resolved(meta_arg)
|
||||
if not source.is_dir():
|
||||
fail(f"Source directory does not exist: {source}")
|
||||
if not allowed_workspace(workspace, meta):
|
||||
fail("Workspace must be _meta/workspace or a codex-experiment-* directory in system temp")
|
||||
if workspace.exists():
|
||||
before_manifest = meta / "workspace-manifest-before.json"
|
||||
if workspace.parent == meta and workspace.name == "workspace" and not before_manifest.exists():
|
||||
shutil.rmtree(native_path(workspace))
|
||||
else:
|
||||
fail(f"Workspace already exists: {workspace}")
|
||||
|
||||
files, git_root = source_files(source)
|
||||
workspace.mkdir(parents=True)
|
||||
for path in files:
|
||||
relative = path.relative_to(source)
|
||||
target = workspace / relative
|
||||
os.makedirs(native_path(target.parent), exist_ok=True)
|
||||
try:
|
||||
shutil.copy2(native_path(path), native_path(target), follow_symlinks=False)
|
||||
except OSError as error:
|
||||
fail(f"Failed to copy {path} -> {target}: {error}")
|
||||
|
||||
before = manifest(workspace)
|
||||
write_json(meta / "workspace-manifest-before.json", before)
|
||||
write_json(
|
||||
meta / "workspace-source.json",
|
||||
{
|
||||
"source": str(source),
|
||||
"workspace": str(workspace),
|
||||
"git_root": git_root,
|
||||
"file_count": len(before),
|
||||
"total_bytes": sum(int(item["size"]) for item in before.values()),
|
||||
},
|
||||
)
|
||||
print(f"Created isolated workspace with {len(before)} files: {workspace}")
|
||||
|
||||
|
||||
def verify(workspace_arg: str, meta_arg: str) -> None:
|
||||
workspace = resolved(workspace_arg)
|
||||
meta = resolved(meta_arg)
|
||||
before_path = meta / "workspace-manifest-before.json"
|
||||
if not workspace.is_dir() or not before_path.is_file():
|
||||
fail("Workspace or before-manifest is missing")
|
||||
|
||||
before = json.loads(before_path.read_text(encoding="utf-8"))
|
||||
after = manifest(workspace)
|
||||
added = sorted(set(after) - set(before), key=str.casefold)
|
||||
removed = sorted(set(before) - set(after), key=str.casefold)
|
||||
modified = sorted(
|
||||
(path for path in set(before) & set(after) if before[path] != after[path]),
|
||||
key=str.casefold,
|
||||
)
|
||||
write_json(meta / "workspace-manifest-after.json", after)
|
||||
result = {
|
||||
"unchanged": not (added or removed or modified),
|
||||
"added": added,
|
||||
"removed": removed,
|
||||
"modified": modified,
|
||||
"file_count_before": len(before),
|
||||
"file_count_after": len(after),
|
||||
}
|
||||
write_json(meta / "workspace-integrity.json", result)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
if not result["unchanged"]:
|
||||
raise SystemExit(3)
|
||||
|
||||
|
||||
def cleanup(workspace_arg: str, meta_arg: str) -> None:
|
||||
workspace = resolved(workspace_arg)
|
||||
meta = resolved(meta_arg)
|
||||
if not allowed_workspace(workspace, meta):
|
||||
fail("Refusing to remove a directory outside the approved experiment workspace locations")
|
||||
if workspace.exists():
|
||||
shutil.rmtree(native_path(workspace))
|
||||
print(f"Removed temporary workspace: {workspace}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
create_parser = subparsers.add_parser("create")
|
||||
create_parser.add_argument("source")
|
||||
create_parser.add_argument("workspace")
|
||||
create_parser.add_argument("meta")
|
||||
verify_parser = subparsers.add_parser("verify")
|
||||
verify_parser.add_argument("workspace")
|
||||
verify_parser.add_argument("meta")
|
||||
cleanup_parser = subparsers.add_parser("cleanup")
|
||||
cleanup_parser.add_argument("workspace")
|
||||
cleanup_parser.add_argument("meta")
|
||||
args = parser.parse_args()
|
||||
if args.command == "create":
|
||||
create(args.source, args.workspace, args.meta)
|
||||
elif args.command == "verify":
|
||||
verify(args.workspace, args.meta)
|
||||
else:
|
||||
cleanup(args.workspace, args.meta)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user