diff --git a/docs/cli-feature-probe.md b/docs/cli-feature-probe.md new file mode 100644 index 0000000..e2dac88 --- /dev/null +++ b/docs/cli-feature-probe.md @@ -0,0 +1,430 @@ +# CLI Feature Probe — DisClaw + +Manuelles Sanity-Check-Script für kritische Claude Code CLI-Annahmen. +Muss vor Phase-2-Freigabe ausgeführt und ausgefüllt werden. + +Stand: [Datum der letzten Ausführung] +CLI-Version: [Ausgabe von `claude --version`] + +--- + +## Kontext + +Dieses Dokument deckt die vier Annahmen ab, die in `docs/cli-feature-answers.md` +als "Unsicherheit: Mittel" oder "empirisch noch nicht bestätigt" markiert sind. +Jede Probe muss in einer sauberen Shell-Session ausgeführt werden (keine +gespeicherten Umgebungsvariablen aus vorherigen Probes). + +Voraussetzungen: +- `claude` ist auf PATH (oder `CLAUDE_PATH` zeigt auf die CLI) +- `jq` ist installiert (`jq --version` gibt etwas aus) +- `python3` ist installiert (nur für Probe 3, als Fallback-Pretty-Printer) +- Aktiver Claude Pro/Max-Account (die CLI benötigt eine Subscription, keinen + separaten API-Key) + +--- + +## Probe 1: `session_id` Top-Level-Position und Stabilität über Resume + +### Hypothese + +Aus `cli-feature-answers.md` Abschnitt 2: + +> "Die Session-ID liegt als Top-Level-Feld `session_id` im +> `--output-format json`-Result. [...] Per Default bleibt die ID beim +> `--resume` stabil — die Session wird in der existierenden JSONL +> fortgeschrieben." + +Wir nehmen also an: + +1. Der erste `claude -p` Call gibt ein JSON-Objekt zurück, in dem `.session_id` + direkt auf Top-Level liegt (nicht unter `.result.session_id` oder ähnlichem). +2. Ein zweiter Call mit `--resume ` gibt dieselbe `session_id` zurück. + +### Kommandos + +```bash +# Schritt 1: Erste Session starten und session_id extrahieren +id1=$(claude -p "antworte mit dem wort ALPHA und sonst nichts" \ + --output-format json --bare \ + | tee /tmp/probe1-run1.json \ + | jq -r '.session_id') + +echo "id1=$id1" + +# Schritt 2: Session fortsetzen und id aus dem Response lesen +id2=$(claude -p "antworte mit dem wort BETA und sonst nichts" \ + --resume "$id1" \ + --output-format json --bare \ + | tee /tmp/probe1-run2.json \ + | jq -r '.session_id') + +echo "id2=$id2" + +# Schritt 3: Vergleich +if [ "$id1" = "$id2" ]; then + echo "PASS: IDs sind gleich" +else + echo "ABWEICHUNG: id1=$id1 id2=$id2" +fi + +# Schritt 4: Komplettes JSON-Top-Level zur Orientierung +echo "--- Top-Level-Felder Run 1 ---" +jq 'keys' /tmp/probe1-run1.json + +echo "--- Top-Level-Felder Run 2 ---" +jq 'keys' /tmp/probe1-run2.json +``` + +### Erwartete Ausgabe + +``` +id1= +id2= ← gleiche UUID wie id1 +PASS: IDs sind gleich +--- Top-Level-Felder Run 1 --- +[ + "result", + "session_id", + "type", + ... +] +--- Top-Level-Felder Run 2 --- +[ + "result", + "session_id", + "type", + ... +] +``` + +`session_id` erscheint direkt in `keys` (Top-Level), nicht nur tief im Baum. +Die UUID ist in beiden Runs identisch. + +### Tatsächliche Ausgabe + +``` +[NOCH NICHT AUSGEFÜLLT — Bitte vor Phase-2-Freigabe ausfüllen] +``` + +### Status + +- [ ] Verifiziert +- [ ] Abweichung gefunden: + +### Notizen + +Falls `id2 != id1`: Defensiv-Strategie aus `cli-feature-answers.md` Abschnitt 2 +ist relevant — nach jedem Run die `session_id` aus dem Response-JSON lesen und +in der DB aktualisieren, unabhängig davon ob sie sich geändert hat. + +Falls `.session_id` nicht auf Top-Level liegt: `jq 'path(.. | strings | +select(test("^[0-9a-f-]{36}$")))' /tmp/probe1-run1.json` zeigt den tatsächlichen +Pfad. + +--- + +## Probe 2: `--resume` Flow — Kontext-Kontinuität + +### Hypothese + +Aus `cli-feature-answers.md` Abschnitt 1: + +> "`claude -p "" --resume --output-format json` lädt die +> existierende Session und hängt `` als neuen User-Turn an." + +Wir nehmen an: + +1. Claude "erinnert" sich nach einem `--resume` an den Inhalt der vorherigen + Turns dieser Session. +2. Der `--resume`-Call schlägt nicht stillschweigend fehl (z. B. mit einer leeren + Antwort oder einer neuen, unverbundenen Session), wenn die `session_id` gültig + ist. +3. Das Muster `--resume "$session_id" --output-format json --bare` ist ohne + weitere Flags ausführbar (kein `--verbose` o. ä. erforderlich). + +### Kommandos + +```bash +# Schritt 1: Session mit einem Geheimnis starten +run1=$(claude -p "Merke dir folgendes Codewort für diese Session: XYLOPHON42. Bestätige nur mit OK." \ + --output-format json --bare) + +sid=$(echo "$run1" | jq -r '.session_id') +echo "session_id=$sid" +echo "Antwort Run 1: $(echo "$run1" | jq -r '.result')" + +# Schritt 2: In derselben Session das Geheimnis abfragen +run2=$(claude -p "Was war das Codewort, das du dir gemerkt hast?" \ + --resume "$sid" \ + --output-format json --bare) + +echo "Antwort Run 2: $(echo "$run2" | jq -r '.result')" + +# Schritt 3: Prüfen ob das Codewort im Response vorkommt +if echo "$run2" | jq -r '.result' | grep -q "XYLOPHON42"; then + echo "PASS: Codewort in Antwort gefunden" +else + echo "ABWEICHUNG: Codewort fehlt in der Antwort" +fi +``` + +### Erwartete Ausgabe + +``` +session_id= +Antwort Run 1: OK +Antwort Run 2: Das Codewort war XYLOPHON42. +PASS: Codewort in Antwort gefunden +``` + +### Tatsächliche Ausgabe + +``` +[NOCH NICHT AUSGEFÜLLT — Bitte vor Phase-2-Freigabe ausfüllen] +``` + +### Status + +- [ ] Verifiziert +- [ ] Abweichung gefunden: + +### Notizen + +Falls das Codewort fehlt: Prüfe ob `--resume` die richtige Session lädt. +`ls ~/.claude/projects/` zeigt alle gespeicherten Projekt-Slugs. Das Verzeichnis +für den aktuellen cwd hat den Slug ``. + +Falls `--resume` + `--output-format json` + `--bare` zusammen einen Exit-Code +ungleich 0 erzeugen: Flags einzeln testen, um die Inkompatibilität einzugrenzen. + +--- + +## Probe 3: `stream-json` Event-Typen + +### Hypothese + +Aus `cli-feature-answers.md` Abschnitt 4: + +> "Relevante Typen: `system`, `assistant`, `user`, `stream_event`, `result`." +> "Text-Deltas landen unter `.event.delta.type == "text_delta"` mit `.event.delta.text`." +> "Für `stream-json` müssen praktisch immer `--verbose` und +> `--include-partial-messages` mitgegeben werden, sonst gibt es keine +> Text-Deltas, nur den Final-`result`-Block." + +Wir nehmen an: + +1. Mit `--output-format stream-json --verbose --include-partial-messages` erscheinen + tatsächlich Zeilen mit `"type":"stream_event"` im Output. +2. Mindestens ein `stream_event` hat `.event.delta.type == "text_delta"` mit einem + nicht-leeren `.event.delta.text`. +3. Die letzte (oder eine der letzten) Zeilen hat `"type":"result"`. +4. Ohne `--include-partial-messages` erscheint nur `"type":"result"` (keine + `stream_event`-Zeilen). + +### Kommandos + +```bash +# Schritt 1: Stream MIT partial messages — alle Event-Typen anzeigen +echo "=== MIT --include-partial-messages ===" +claude -p "schreibe genau drei Saetze ueber den Mond" \ + --output-format stream-json \ + --verbose \ + --include-partial-messages \ + --bare \ + | tee /tmp/probe3-stream.ndjson \ + | jq -c '{type, subtype: .subtype?, event_type: .event.type?, delta_type: .event.delta.type?}' + +# Schritt 2: Welche type-Werte kommen vor? +echo "=== Vorkommende type-Werte ===" +jq -r '.type' /tmp/probe3-stream.ndjson | sort | uniq -c | sort -rn + +# Schritt 3: Einen text_delta-Block vollständig anzeigen +echo "=== Erster text_delta-Block ===" +jq -c 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | {type, text: .event.delta.text}' \ + /tmp/probe3-stream.ndjson | head -3 + +# Schritt 4: Stream OHNE partial messages zum Vergleich +echo "=== OHNE --include-partial-messages ===" +claude -p "antworte mit einem einzigen Wort: HALLO" \ + --output-format stream-json \ + --verbose \ + --bare \ + | jq -c '.type' +``` + +### Erwartete Ausgabe + +``` +=== MIT --include-partial-messages === +{"type":"system","subtype":"init","event_type":null,"delta_type":null} +{"type":"stream_event","subtype":null,"event_type":"content_block_delta","delta_type":"text_delta"} +... (mehrere text_delta-Zeilen) +{"type":"result","subtype":null,"event_type":null,"delta_type":null} + +=== Vorkommende type-Werte === + 42 stream_event + 3 assistant + 2 user + 1 system + 1 result + +=== Erster text_delta-Block === +{"type":"stream_event","text":"Der "} +{"type":"stream_event","text":"Mond "} +{"type":"stream_event","text":"ist "} + +=== OHNE --include-partial-messages === +"system" +"result" +``` + +### Tatsächliche Ausgabe + +``` +[NOCH NICHT AUSGEFÜLLT — Bitte vor Phase-2-Freigabe ausfüllen] +``` + +### Status + +- [ ] Verifiziert +- [ ] Abweichung gefunden: + +### Notizen + +Falls neue, unbekannte `type`-Werte auftauchen: In die Tabelle eintragen. +Der DisClaw-Parser in Phase 5 (`src/agent/runner.ts`) muss unknown types +silently skippen statt zu crashen. + +Falls `stream_event` fehlt auch mit `--include-partial-messages`: Prüfe die +CLI-Version. Möglicherweise ist der Flag in der installierten Version noch +nicht vorhanden. `claude --help | grep partial` prüft das. + +Falls das Feld-Layout der Text-Deltas abweicht: Den tatsächlichen Pfad mit +`jq 'select(.type == "stream_event") | paths' /tmp/probe3-stream.ndjson | head -20` +ermitteln und hier dokumentieren. + +--- + +## Probe 4: `usage`-Block Pfade im JSON-Output + +### Hypothese + +Aus `cli-feature-answers.md` Abschnitt 7: + +> "Der `--output-format json`-Response enthält einen `usage`-Block nach +> Anthropic-API-Format mit `cache_read_input_tokens`, `cache_creation_input_tokens`, +> `input_tokens`, `output_tokens`." +> "Die **genauen JSON-Pfade** sollten aber empirisch geprüft werden — +> wahrscheinlich `result.usage.cache_read_input_tokens` oder top-level +> `usage.cache_read_input_tokens`." + +Wir nehmen an: + +1. Der `usage`-Block ist im JSON-Output vorhanden (irgendwo im Baum). +2. Er enthält mindestens die Felder `input_tokens` und `output_tokens`. +3. Nach einem `--resume`-Call (zweiter Turn) erscheint `cache_read_input_tokens` + mit einem Wert größer 0. + +### Kommandos + +```bash +# Schritt 1: Ersten Run durchführen und usage-Struktur suchen +echo "=== usage-Struktur im ersten Run ===" +run1=$(claude -p "erklaere in drei saetzen was eine linked list ist" \ + --output-format json --bare) + +echo "$run1" | tee /tmp/probe4-run1.json | python3 -m json.tool + +# Schritt 2: Alle Pfade finden die "token" enthalten +echo "=== Pfade mit 'token' im Namen ===" +jq '[path(.. | numbers?) | select(. | map(tostring) | join(".") | test("token"; "i"))]' \ + /tmp/probe4-run1.json + +# Schritt 3: usage direkt ansprechen (alle möglichen Pfade testen) +echo "=== usage top-level ===" +jq '.usage // "NICHT VORHANDEN"' /tmp/probe4-run1.json + +echo "=== result.usage ===" +jq '.result.usage // "NICHT VORHANDEN"' /tmp/probe4-run1.json + +# Schritt 4: Zweiten Run mit Resume — cache_read prüfen +sid=$(jq -r '.session_id' /tmp/probe4-run1.json) +echo "=== usage im Resume-Run (cache_read erwartet) ===" +claude -p "fasse deine Erklaerung in einem Satz zusammen" \ + --resume "$sid" \ + --output-format json --bare \ + | tee /tmp/probe4-run2.json \ + | jq '.usage // .result.usage // "usage nicht gefunden"' + +# Schritt 5: cache_read_input_tokens im zweiten Run prüfen +echo "=== cache_read_input_tokens in Run 2 ===" +jq '(.usage // .result.usage).cache_read_input_tokens // "FELD FEHLT"' \ + /tmp/probe4-run2.json +``` + +### Erwartete Ausgabe + +``` +=== usage top-level === +{ + "input_tokens": 42, + "output_tokens": 87, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0 +} + +=== result.usage === +"NICHT VORHANDEN" ← oder vice versa, je nach tatsächlichem Pfad + +=== usage im Resume-Run (cache_read erwartet) === +{ + "input_tokens": 12, + "output_tokens": 23, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 412 ← größer 0 beim Resume +} + +=== cache_read_input_tokens in Run 2 === +412 +``` + +### Tatsächliche Ausgabe + +``` +[NOCH NICHT AUSGEFÜLLT — Bitte vor Phase-2-Freigabe ausfüllen] +``` + +### Status + +- [ ] Verifiziert +- [ ] Abweichung gefunden: + +### Notizen + +Falls `cache_read_input_tokens` auch im zweiten Run 0 ist: Das deutet darauf +hin, dass Prompt-Caching für kurze Prompts nicht greift (Anthropic cached erst +ab ca. 1024 Tokens). Mit einem längeren Prompt testen oder nach mehreren +Turns prüfen. + +Falls der `usage`-Block gar nicht vorhanden ist: Das wäre ein signifikantes +Problem für Phase 4/5 (Token-Stats pro Turn). Als Fallback: `null` in der +`agent_runs`-Tabelle tolerieren. + +Tatsächlichen Pfad hier eintragen und in `src/agent/runner.ts` als Konstante +definieren, damit er genau einmal im Code steht und leicht geändert werden kann. + +--- + +## Ausfüll-Checkliste + +| Probe | Ausgeführt von | Datum | Status | +|-------|---------------|-------|--------| +| 1: session_id Top-Level | | | [ ] | +| 2: --resume Flow | | | [ ] | +| 3: stream-json Events | | | [ ] | +| 4: usage-Block Pfade | | | [ ] | + +Alle vier Probes müssen den Status "Verifiziert" tragen, bevor Phase 2 +(Session-Resume-Implementation) als freigegeben gilt. Abweichungen müssen +vor dem Phase-2-Start in `docs/cli-feature-answers.md` nachgezogen werden.