# ClaudeCode Executor: stderr and Exit Code Observability

## The Problem

The ClaudeCode activation's `executor.rs` spawns the `claude` CLI as a child process and streams its stdout (JSON events). But when the CLI failed — bad API key, invalid model, network error, binary not found — the failure was **completely invisible**:

- stderr was piped (`.stderr(Stdio::piped())`) but **never read**
- Exit code was discarded: `let _ = child.wait().await`
- An empty stdout stream caused the loop to exit silently
- `activation.rs` then yielded a `Complete` event with default/empty values

The user saw: empty `claude_session_id`, null usage, no content. No indication anything went wrong. Every failure mode looked identical: silent nothing.

## The Fix

Three layers, all in the executor's `launch()` stream:

### 1. Stderr capture (background task)

```rust
let stderr_buffer: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let stderr = child.stderr.take().expect("stderr");
tokio::spawn(async move {
    // reads lines into buffer, capped at 100
});
```

A `tokio::spawn` task drains stderr into a bounded `Arc<Mutex<Vec<String>>>`. This serves two purposes: making stderr content available after stdout ends, and preventing the child process from blocking on a full stderr pipe buffer.

### 2. Exit code + error event

After the stdout loop ends, the executor now:
- Captures the actual `ExitStatus` instead of discarding it
- Tracks whether a `Result` event was received from stdout (`got_result` flag)
- If exit code is non-zero **or** no Result event arrived: yields a `RawClaudeEvent::Result { is_error: true }` containing stderr content and exit code

This follows the existing error shape, so `activation.rs` handles it without changes — it already short-circuits on `is_error: Some(true)`.

### 3. Activation-level guard

Belt-and-suspenders in `activation.rs`: if the stream produced no content and no session ID, yield `ChatEvent::Err` instead of a ghost `Complete`. Catches edge cases where the executor somehow doesn't emit an error.

## Design Decisions

**Why a background task for stderr?** Stderr and stdout must be read concurrently. If we only read stdout and the child fills the stderr pipe buffer (~64KB on most systems), the child blocks and stdout also stops. A separate task avoids this deadlock.

**Why cap at 100 lines?** A runaway process could spam stderr. 100 lines is enough for any reasonable error message while preventing unbounded memory growth.

**Why not merge stderr into the main event stream?** Stderr from the claude CLI is diagnostic noise (progress bars, warnings, debug output). It's only useful when something goes wrong. Surfacing it only on failure keeps the happy path clean.

**Why the activation guard?** Defense in depth. The executor *should* always emit an error event on failure now, but if it somehow doesn't (stream cancelled, panic, etc.), the activation layer catches the empty result instead of emitting a misleading `Complete`.

## Files Changed

- `executor.rs` — stderr capture, exit code checking, error event emission, tracing
- `activation.rs` — empty-stream guard before `Complete` emission
