# Plan: `--self-review` flag

## Goal

Replace verbose reviewer invocations like:

```bash
swival --yolo --provider chatgpt --model gpt-5.4 \
  --skills-dir ~/src/fastly-agent-toolkit/skills \
  --report /tmp/full.json \
  --reviewer 'swival --reviewer-mode --provider chatgpt --model gpt-5.4 --quiet --yolo --report /tmp/review.json --skills-dir ~/src/fastly-agent-toolkit/skills' \
  "Do something"
```

with:

```bash
swival --yolo --provider chatgpt --model gpt-5.4 \
  --skills-dir ~/src/fastly-agent-toolkit/skills \
  --report /tmp/full.json \
  --self-review \
  "Do something"
```

`--self-review` automatically builds the reviewer command by mirroring the main agent's
provider/model/auth flags, adding `--reviewer-mode --quiet`, and passing through skills and yolo.

## Scope decisions

- **`--report` and `--cache` are NOT mirrored.** Reviewer mode exits before report setup or
  cache initialization in `main()`, so passing those flags to the inner command would be dead
  weight. If reviewer-mode report/cache support is added later, this can be revisited.
- **`--api-key` is NOT put on the command line.** That would expose the secret in process
  listings and logs. Instead, when `args.api_key` is set, it is injected into the reviewer
  subprocess environment as the **provider-specific** env var that `resolve_provider()` reads:
  `HF_TOKEN` for huggingface, `OPENROUTER_API_KEY` for openrouter, `OPENAI_API_KEY` for
  generic, `CHATGPT_API_KEY` for chatgpt. The first three already have env-var fallbacks in
  `resolve_provider()`. The chatgpt branch currently only reads `api_key` with no env-var
  fallback — this plan adds `CHATGPT_API_KEY` support to close that gap (one-line change).
  For providers where the key was already set via the standard env var (not `--api-key`), it
  inherits automatically since `run_reviewer` copies `os.environ`.
- **`--base-url` IS mirrored on the command line.** It is not a secret and is required for
  generic/custom provider parity.

## Changes

### 1. Add `--self-review` argument — `swival/agent.py` (near `--reviewer`, ~line 1781)

```python
parser.add_argument(
    "--self-review",
    action="store_true",
    default=_UNSET,
    help="Use a second swival instance as reviewer, inheriting provider, model, "
    "skills-dir, and yolo settings from the current invocation.",
)
```

Note: `default=_UNSET` (not `False`) so config merge via `apply_config_to_args` works correctly —
it only applies config values when the dest is still `_UNSET`.

### 2. Config file support — `swival/config.py`

Four touch points:

a) Add to `CONFIG_KEYS` schema (~line 28):
```python
"self_review": bool,
```

b) Add to `_ARGPARSE_DEFAULTS` (~line 85):
```python
"self_review": False,
```

c) Add to `_DROP_KEYS` in `config_to_session_kwargs` (~line 510) since `self_review` is a
CLI/orchestration concern, not a Session concern:
```python
_DROP_KEYS = {
    "color",
    "reviewer",
    "self_review",
    ...
}
```

d) Add commented example to `generate_config()` (~line 589):
```python
"# self_review = false              # use self as reviewer (mirrors provider/model flags)",
```

### 3. Config inheritance hazard — `swival/agent.py` (~line 1913)

The inner reviewer process loads `swival.toml` and inherits config. If the project sets
`self_review = true`, the inner process (running with `--reviewer-mode`) would inherit it,
and the conflict check would reject it. This is the same hazard that `reviewer` already has.

Fix: clear `self_review` in the reviewer-mode branch, alongside the existing `reviewer` clearing:

```python
# Config inheritance hazard: clear keys that don't apply in reviewer mode
if reviewer_from_cli:
    parser.error("--reviewer-mode and --reviewer cannot be used together")
args.reviewer = None

# Same pattern for self_review — project config may set it, but it's
# meaningless (and would conflict) when we're already in reviewer mode.
self_review_from_cli = args.self_review is not _UNSET and args.self_review
if self_review_from_cli:
    parser.error("--self-review is incompatible with --reviewer-mode")
args.self_review = False
```

**Important:** the `self_review_from_cli` snapshot must be taken *before* `apply_config_to_args`
(same pattern as `reviewer_from_cli`). Move the snapshot up to ~line 1904:

```python
# Snapshot whether these were explicitly on CLI (before config merge)
reviewer_from_cli = args.reviewer is not _UNSET
self_review_from_cli = getattr(args, 'self_review', _UNSET) is not _UNSET and args.self_review
```

### 4. Normal-mode validation — `swival/agent.py` (~line 1939)

```python
if args.self_review and args.repl:
    parser.error("--self-review is incompatible with --repl")
```

### 5. Build the reviewer command — `swival/agent.py` (after `apply_config_to_args`, ~line 1934)

After config is fully resolved, synthesize `args.reviewer` from args when `--self-review` is set:

```python
if args.self_review:
    if args.reviewer:
        parser.error("--self-review and --reviewer cannot be used together")
    args.reviewer = _build_self_review_cmd(args)
```

### 6. Implement `_build_self_review_cmd(args)` helper — `swival/agent.py`

Placed as a module-level function near `run_reviewer()` (~line 1464).

Mirrored flags (all checked against resolved values, post-config-merge):

| Flag | Source | Notes |
|------|--------|-------|
| `--reviewer-mode` | always | Required |
| `--quiet` | always | Suppress diagnostics |
| `--yolo` | `args.yolo` | Mirror if truthy |
| `--provider X` | `args.provider` | Mirror (always resolved post-config) |
| `--model X` | `args.model` | Mirror if set |
| `--base-url X` | `args.base_url` | Mirror if set — required for generic/custom providers |
| `--skills-dir X` | `args.skills_dir` | Mirror each entry |
| `--max-context-tokens X` | `args.max_context_tokens` | Mirror if set |
| `--max-output-tokens X` | `args.max_output_tokens` | Mirror if non-default |

**Not mirrored on the command line:** `--api-key` (security — passed via env instead),
`--report` (reviewer mode doesn't initialize report), `--cache`/`--cache-dir` (reviewer mode
doesn't initialize cache).

```python
def _build_self_review_cmd(args) -> str:
    import shlex

    parts = [sys.executable, "-m", "swival.agent", "--reviewer-mode", "--quiet"]

    if args.yolo:
        parts.append("--yolo")
    if args.provider and args.provider != "lmstudio":
        parts.extend(["--provider", args.provider])
    if args.model:
        parts.extend(["--model", str(args.model)])
    if args.base_url:
        parts.extend(["--base-url", str(args.base_url)])
    if args.skills_dir:
        for d in args.skills_dir:
            parts.extend(["--skills-dir", d])
    if args.max_context_tokens:
        parts.extend(["--max-context-tokens", str(args.max_context_tokens)])
    if args.max_output_tokens and args.max_output_tokens != 32768:
        parts.extend(["--max-output-tokens", str(args.max_output_tokens)])

    return shlex.join(parts)
```

### 7. Pass API key via provider-specific env var — `swival/agent.py` (~line 2625)

When `self_review` is active and `args.api_key` was explicitly set, inject it into the
reviewer subprocess environment as the provider-specific env var that `resolve_provider()`
already reads. This avoids inventing a new env var and requires zero changes to
`resolve_provider()`.

Two sub-changes:

**a) Add `CHATGPT_API_KEY` env-var fallback to `resolve_provider()`** — `swival/agent.py` (~line 2193)

The chatgpt branch currently does `resolved_key = api_key` with no env-var fallback. Add one:

```python
resolved_key = api_key or os.environ.get("CHATGPT_API_KEY")
```

This is consistent with how huggingface, openrouter, and generic already work. No error is
raised if the key is still None — chatgpt via litellm can use OAuth without an explicit key.

**b) Inject provider-specific env var into reviewer subprocess** — `swival/agent.py` (~line 2625)

```python
# Provider → env var that resolve_provider() checks for that provider
_PROVIDER_KEY_ENV = {
    "huggingface": "HF_TOKEN",
    "openrouter": "OPENROUTER_API_KEY",
    "generic": "OPENAI_API_KEY",
    "chatgpt": "CHATGPT_API_KEY",
}
```

At the reviewer env setup (~line 2625):

```python
# Build env vars for reviewer subprocess
reviewer_env: dict[str, str] | None = None
if reviewer_cmd:
    reviewer_env = {"SWIVAL_TASK": args.question}
    model_id = getattr(args, "_resolved_model_id", None)
    if model_id:
        reviewer_env["SWIVAL_MODEL"] = model_id
    # Pass API key via provider-specific env var (avoid CLI exposure)
    if args.self_review and args.api_key:
        env_var = _PROVIDER_KEY_ENV.get(args.provider)
        if env_var:
            reviewer_env[env_var] = args.api_key
```

For `lmstudio` (local, no key needed) and any provider where the key was already set via the
standard env var, this is a no-op — the env var either doesn't exist in the mapping or is
already inherited from `os.environ`.

## Testing

Add `tests/test_self_review.py`:

### Unit tests for `_build_self_review_cmd`

1. **`test_build_cmd_basic`** — provider/model/yolo set → output contains
   `--reviewer-mode --quiet --yolo --provider chatgpt --model gpt-5.4`.
2. **`test_build_cmd_skills_dir_multiple`** — two `--skills-dir` entries → both appear.
3. **`test_build_cmd_base_url`** — `--base-url` mirrored for generic provider setups.
4. **`test_build_cmd_no_api_key_on_cmdline`** — `args.api_key` is set → verify `--api-key`
   does NOT appear in the command string.
5. **`test_build_cmd_no_report_no_cache`** — `--report` and `--cache` are NOT present even
   when set on args.
6. **`test_build_cmd_correct_token_flags`** — set `max_context_tokens` and `max_output_tokens`
   → verify `--max-context-tokens` and `--max-output-tokens` appear (correct flag names).

### Validation / conflict tests

7. **`test_self_review_and_reviewer_conflict`** — both set → `parser.error`.
8. **`test_self_review_and_repl_conflict`** — both set → `parser.error`.
9. **`test_reviewer_mode_and_self_review_cli_conflict`** — `--reviewer-mode --self-review`
   on CLI → `parser.error` (tests the early branch).

### Config integration

10. **`test_config_self_review_true`** — `self_review = true` in config, no CLI flag →
    `args.self_review` is `True` after `apply_config_to_args`.
11. **`test_config_self_review_inherited_in_reviewer_mode`** — simulate inner reviewer
    process: `--reviewer-mode` on CLI, `self_review = true` in loaded config → verify
    `args.self_review` is cleared to `False` (not rejected), and `run_as_reviewer` can
    proceed. This is the config inheritance hazard regression test.

### Env-based API key

12. **`test_api_key_passed_via_provider_env`** — parametrize over all keyed providers
    (huggingface→`HF_TOKEN`, openrouter→`OPENROUTER_API_KEY`, generic→`OPENAI_API_KEY`,
    chatgpt→`CHATGPT_API_KEY`): set `args.self_review`, `args.api_key`, and `args.provider`
    → verify `reviewer_env` contains the correct provider-specific env var with the key
    value, and that `--api-key` does not appear in the synthesized command string.
13. **`test_api_key_env_not_set_for_lmstudio`** — `args.provider = "lmstudio"` with
    `args.api_key` set → verify no key env var is injected (lmstudio is local, no key needed).
14. **`test_chatgpt_resolve_provider_reads_env`** — set `CHATGPT_API_KEY` in env, call
    `resolve_provider(provider="chatgpt", model="gpt-5.4", api_key=None, ...)` → verify
    `resolved_key` equals the env value. This proves the new env-var fallback in
    `resolve_provider()` actually works for the chatgpt path.

## Follow-through

- **Document `CHATGPT_API_KEY`** wherever provider env vars are listed (CLI `--help` text for
  `--api-key`, `generate_config()` comments, README/docs if any). The other three providers'
  env vars are already mentioned in their `ConfigError` messages — add the same for chatgpt.
- **Add one integration-style test for the handoff boundary.** Unit-testing
  `_build_self_review_cmd` and the env injection separately is necessary but not sufficient —
  the real risk is in the seam between the synthesized command and the env vars arriving in the
  subprocess. Test #14 covers `resolve_provider()` reading the env, but add a test that
  exercises the full path: build the command via `_build_self_review_cmd`, populate
  `reviewer_env` with the provider-specific key, and verify that a mocked `run_reviewer` call
  receives both the correct argv (no `--api-key`) and the correct env (with the key). This can
  stay as a unit test with mocked subprocess — no need for a real LLM call.

## Not in scope

- Allowing `--self-review` to accept an optional model override (e.g. `--self-review gpt-4o` to
  use a cheaper/different model for review). Can be added later as `--self-review-model`.
- Reviewer-specific flags like `--review-prompt` or `--verify` passthrough — these can still be
  set via `swival.toml` config in the project.
- Making `--report` or `--cache` work in reviewer mode — that's a separate feature.
