# Task 2 Report: Fatal-auth drain defeated by run_cli's pause-poll loop

## Changes Made

### `claw_forge/orchestrator/dispatcher.py`

1. Added `self.pause_reason: str | None = None` to `Dispatcher.__init__` (after the `self._paused` line).
2. Updated `pause()` signature to `pause(self, reason: str | None = None) -> None` — sets `self.pause_reason = reason` and updated log message to include the reason.
3. Updated `resume()` to clear `self.pause_reason = None` on resume.
4. Updated the fatal-auth pause site (line 449) from `self.pause()` to `self.pause(reason="fatal_auth")`.

### `claw_forge/orchestrator/run_cli.py`

After the `_pending_merge_retries.clear()` line (inside the `while current_nodes:` loop, before the `if not dispatcher.is_paused:` block), inserted:

```python
if dispatcher.is_paused and dispatcher.pause_reason == "fatal_auth":
    console.print(
        "\n[bold red]⛔ Halting: provider pool exhausted on a fatal "
        "auth error.[/bold red]\n"
        "[yellow]Remaining tasks left pending — fix credentials "
        "(check `claude login` / provider oauth_token / ANTHROPIC_* "
        "env) and re-run `claw-forge run` to resume.[/yellow]"
    )
    break
```

This breaks out of the `while current_nodes:` loop, leaving remaining tasks `pending` in the DB instead of falling into the Stop-All resume-poll that re-dispatches them into cascading failure.

### `tests/orchestrator/test_run_cli_fatal_auth_drain.py` (new)

Two unit tests per the brief:
- `test_pause_records_reason` — verifies `pause(reason="fatal_auth")` sets both `is_paused=True` and `pause_reason=="fatal_auth"`.
- `test_pause_default_reason_is_none` — verifies zero-arg `pause()` leaves `pause_reason=None` (backward compatibility).

## Test Runs

**Failing test (pre-implementation):**
```
FAILED tests/orchestrator/test_run_cli_fatal_auth_drain.py::test_pause_records_reason
TypeError: Dispatcher.pause() got an unexpected keyword argument 'reason'
FAILED tests/orchestrator/test_run_cli_fatal_auth_drain.py::test_pause_default_reason_is_none
AttributeError: 'Dispatcher' object has no attribute 'pause_reason'
```

**After implementation:**
```
tests/orchestrator/test_run_cli_fatal_auth_drain.py::test_pause_records_reason PASSED
tests/orchestrator/test_run_cli_fatal_auth_drain.py::test_pause_default_reason_is_none PASSED
tests/test_dispatcher.py — 40 existing tests PASSED
Total: 42 passed in 3.36s
```

**Lint:** `uv run ruff check claw_forge/ tests/` → All checks passed!

## Self-Review

- `pause()` keeps its zero-arg form (backward compatible — Stop-All resume-poll callers at line 761 unaffected).
- The fatal-auth short-circuit fires before the Stop-All resume-poll branch, so the two code paths remain independent.
- `resume()` clears `pause_reason` so a manually-resumed dispatcher (after a Stop-All) starts clean.
- No scope creep: only the three files specified in the brief were touched.

## Concerns

None. The fix is minimal and targeted.
