# Reviewer Environment Variables

## Problem

The reviewer protocol passes the base directory as an argument and the agent's answer on stdin, but nothing else. When using a second Swival instance (or any LLM) as a reviewer, the judge needs the original task to evaluate whether the answer is correct. Right now the only workaround is a user-managed `SWIVAL_TASK` environment variable, which means specifying the task twice.

A related gap: the reviewer has no way to know which review round it's in, which could be useful for adjusting strictness or bailing out early.

## Design

Pass context to the reviewer via environment variables set by Swival before spawning the subprocess. Environment variables are the right mechanism here: they don't change the positional argument contract, they're opt-in (the reviewer ignores them if it doesn't care), and they're trivially accessible from any language.

### Variables

| Variable | Value | Available |
|----------|-------|-----------|
| `SWIVAL_TASK` | The original question (`args.question`) | Always |
| `SWIVAL_REVIEW_ROUND` | Current round number, 1-indexed (`str(review_round)`) | Always |
| `SWIVAL_MODEL` | Resolved model ID (`args._resolved_model_id`) | When set |

`SWIVAL_TASK` is the critical one. The other two are cheap to include and useful for LLM-as-judge and benchmark scripts.

### Why not more?

Candidates considered and rejected:

- **Full message history (JSON):** Too large, risks shell argument limits even as an env var. A reviewer that needs the full context should read files from base_dir instead.
- **System prompt:** Could be sensitive (contains project instructions). Omit.
- **Turn count / token count:** Low value. The report captures this when `--report` is active. If a reviewer needs these stats and `--report` is enabled, it can parse the report file; otherwise this data isn't available.
- **Provider / base URL:** Leaks infrastructure details. Omit.

## Implementation

### 1. `run_reviewer()` signature

Add `env_extra: dict[str, str] | None = None` parameter:

```python
def run_reviewer(
    reviewer_cmd: str,
    base_dir: str,
    answer: str,
    verbose: bool,
    timeout: int = 120,
    env_extra: dict[str, str] | None = None,
) -> tuple[int, str]:
```

Build the subprocess environment by copying `os.environ` and merging `env_extra`:

```python
env = os.environ.copy()
if env_extra:
    env.update(env_extra)
proc = subprocess.run(
    [reviewer_cmd, base_dir],
    input=answer.encode(),
    capture_output=True,
    timeout=timeout,
    env=env,
)
```

This keeps `run_reviewer()` generic — it doesn't know or care what the variables mean. The caller decides what to pass.

### 2. Call site in `_run_main()`

Build the env dict before calling `run_reviewer()`:

```python
reviewer_env = {"SWIVAL_TASK": args.question}
if hasattr(args, "_resolved_model_id") and args._resolved_model_id:
    reviewer_env["SWIVAL_MODEL"] = args._resolved_model_id

# Inside the loop, before calling run_reviewer:
reviewer_env["SWIVAL_REVIEW_ROUND"] = str(review_round)

exit_code, review_text = run_reviewer(
    reviewer_cmd, base_dir, answer, args.verbose,
    env_extra=reviewer_env,
)
```

`SWIVAL_TASK` and `SWIVAL_MODEL` are set once before the loop. `SWIVAL_REVIEW_ROUND` is updated each iteration.

### 3. Tests

Add to `TestRunReviewer`:

- **test_env_vars_passed_to_subprocess**: Script that echoes `$SWIVAL_TASK`, `$SWIVAL_REVIEW_ROUND`, and `$SWIVAL_MODEL`. Verify all three appear in stdout.
- **test_env_vars_inherit_parent_env**: Set a sentinel env var (`SWIVAL_TEST_SENTINEL=hello`) in the test process via `monkeypatch.setenv`. Script echoes `$SWIVAL_TEST_SENTINEL`. Verify it appears in stdout — proves the parent environment is inherited, not replaced.
- **test_env_extra_none**: Call with `env_extra=None` (the default). Verify it still works (no crash from `None` env).
- **test_env_vars_override_parent**: Set `SWIVAL_TASK=old` in the parent process via `monkeypatch.setenv`. Pass `env_extra={"SWIVAL_TASK": "new"}`. Script echoes `$SWIVAL_TASK`. Verify stdout contains "new", not "old". This locks the override behavior — Swival's values are authoritative.

Add to `TestReviewLoop`:

- **test_review_round_env_increments**: Script writes `$SWIVAL_REVIEW_ROUND` to a file on each invocation. After 3 rounds, verify the file contains "1", "2", "3".
- **test_task_env_matches_question**: Script writes `$SWIVAL_TASK` to a file. Verify it matches `args.question`.
- **test_model_env_set**: Script writes `$SWIVAL_MODEL` to a file. Set `args._resolved_model_id = "test-model"`. Verify the file contains "test-model".

### 4. Documentation

Update `docs.md/reviews.md`:

- Add a "Environment variables" section after "The protocol" explaining the three variables.
- Update the "Writing a reviewer script" examples to use `$SWIVAL_TASK`.
- Add an LLM-as-judge example that reads `$SWIVAL_TASK` instead of requiring a user-managed env var.

Update `CLAUDE.md`:
- Mention that the reviewer receives environment variables in the agent.py architecture description.

## File change summary

| File | Change |
|------|--------|
| `swival/agent.py` | `env_extra` param on `run_reviewer()`, build + pass env dict at call site |
| `tests/test_reviewer.py` | 7 new test cases for env vars |
| `docs.md/reviews.md` | New "Environment variables" section, updated examples |
| `CLAUDE.md` | Mention env vars in architecture blurb |
