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

**Root cause (verified):** On pool exhaustion via a fatal-auth error the dispatcher calls `self.pause()` (`dispatcher.py:449`), which only sets an in-process `self._paused` flag (`dispatcher.py:286-289`). Back in `run_cli.py`, the paused branch (`run_cli.py:1407-1441`) polls `GET /project/paused` — a DB flag that **only Stop-All sets** — gets `paused=False`, prints "Resumed", re-fetches pending/failed tasks, and re-dispatches. The loop repeats until the whole DAG is `failed`. This is the Serviq-box failure mode; it defeats the fatal-auth cascade prevention documented in CLAUDE.md.

**Files:**
- Modify: `claw_forge/orchestrator/dispatcher.py` (add `pause_reason`)
- Modify: `claw_forge/orchestrator/run_cli.py` (~line 1361 and ~1407: branch on fatal-auth pause)
- Test: `tests/orchestrator/test_run_cli_fatal_auth_drain.py` (create)

**Interfaces:**
- Consumes: `Dispatcher.is_paused` (existing property, `dispatcher.py:280`).
- Produces: `Dispatcher.pause_reason: str | None` — set to `"fatal_auth"` by the fatal-auth pause site, `None` otherwise. `Dispatcher.pause(reason: str | None = None)`.

- [ ] **Step 1: Write the failing test**

```python
# tests/orchestrator/test_run_cli_fatal_auth_drain.py
from claw_forge.orchestrator.dispatcher import Dispatcher


def test_pause_records_reason():
    d = Dispatcher(handler=lambda n: None, max_concurrency=1)
    d.pause(reason="fatal_auth")
    assert d.is_paused is True
    assert d.pause_reason == "fatal_auth"


def test_pause_default_reason_is_none():
    d = Dispatcher(handler=lambda n: None, max_concurrency=1)
    d.pause()
    assert d.pause_reason is None
```

- [ ] **Step 2: Run test to verify it fails**

Run: `uv run pytest tests/orchestrator/test_run_cli_fatal_auth_drain.py -v`
Expected: FAIL — `AttributeError: 'Dispatcher' object has no attribute 'pause_reason'`.

- [ ] **Step 3: Add `pause_reason` to the dispatcher**

In `claw_forge/orchestrator/dispatcher.py`, in `__init__` add `self.pause_reason: str | None = None`, then update `pause` (line 286) and `resume` (line 294):

```python
    def pause(self, reason: str | None = None) -> None:
        """Signal the dispatcher to drain: finish in-flight tasks, start no new ones."""
        logger.info("Dispatcher paused (%s) — draining in-flight tasks", reason or "operator")
        self._paused = True
        self.pause_reason = reason

    def resume(self) -> None:
        """Clear the paused flag and allow new waves to start."""
        logger.info("Dispatcher resumed — normal operation")
        self._paused = False
        self.pause_reason = None
```

Update the fatal-auth pause site at `dispatcher.py:449` from `self.pause()` to `self.pause(reason="fatal_auth")`.

- [ ] **Step 4: Halt the wave loop on fatal-auth pause in run_cli**

In `claw_forge/orchestrator/run_cli.py`, the `while current_nodes:` loop starts at line 1336 and the paused branch at line 1407. Immediately after `dispatch_result = await dispatcher.run()` and the merge-retry cleanup (after line 1359, before the `if not dispatcher.is_paused:` block at line 1361), add a fatal-auth short-circuit:

```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 exits the `while current_nodes:` loop with tasks still `pending` in the DB, instead of falling into the Stop-All resume-poll that re-dispatches them into failure.

- [ ] **Step 5: Run the tests to verify they pass**

Run: `uv run pytest tests/orchestrator/test_run_cli_fatal_auth_drain.py tests/orchestrator/test_dispatcher.py -v`
Expected: PASS (existing dispatcher tests must stay green — `pause()` keeps its zero-arg call form).

- [ ] **Step 6: Lint + commit**

Run: `uv run ruff check claw_forge/ tests/`
```bash
git add claw_forge/orchestrator/dispatcher.py claw_forge/orchestrator/run_cli.py tests/orchestrator/test_run_cli_fatal_auth_drain.py
git commit -m "fix(orchestrator): halt run loop on fatal-auth drain instead of re-dispatching to failure"
```

---

