Now I have all the context I need. Let me generate the section content.

# Section 5: Adaptive Timeout in run.py

## Overview

This section adds adaptive timeout logic to `src/godotiq/tools/bridge/run.py`. Instead of relying on the hardcoded `TOOL_TIMEOUTS["run"] = 10.0` in `protocol.py`, the `run()` function will compute an appropriate timeout based on the number of 3D asset files (`.glb`) in the project directory. The computed timeout is passed to GDScript (for its polling duration) and to `bridge.request()` with a +5 second buffer (so Python always waits longer than GDScript).

This section depends on **Section 2 (handle-run)** being implemented first, because the GDScript `_handle_run` must already accept a `timeout` parameter in `params` and use it for `_pending_run.timeout_at`.

## Background: Dual Timeout Coordination

The system has two independent timeout mechanisms:

1. **GDScript polling timeout**: How long `_process()` polls `is_playing_scene()` (stored in `_pending_run.timeout_at`)
2. **Python bridge timeout**: How long `bridge.request()` waits for a WebSocket response (the `timeout` kwarg, which overrides `TOOL_TIMEOUTS` default)

The Python bridge timeout MUST always exceed the GDScript polling timeout. Otherwise, Python times out and discards the future while GDScript is still polling. The GDScript response arrives with no matching handler, causing silent failures.

The solution: `run.py` passes `timeout=computed_timeout + 5` to `bridge.request()`. The `+5` buffer accounts for GDScript overhead (pre-flight checks, scene opening). The GDScript side receives `computed_timeout` in params for its polling duration.

## Files to Modify

- `src/godotiq/tools/bridge/run.py` -- main implementation changes
- `src/godotiq/tools/bridge/__init__.py` -- update wrapper call if signature changes (though in this case, no new params are exposed to MCP; the `timeout` parameter is on the internal `run()` function)

## File to Create

- `tests/test_bridge/test_run_adaptive_timeout.py` -- new test file

## Tests FIRST

Create `tests/test_bridge/test_run_adaptive_timeout.py` with the following test cases. Use `unittest.mock.AsyncMock` to mock the bridge (matching the pattern in `tests/test_bridge/test_tools.py`), `tmp_path` for creating temporary directories with `.glb` files, and `unittest.mock.patch.dict` for patching `os.environ`.

### Test: timeout auto-detects from GLB count -- 0 GLB files gives 10.0s

Set up `GODOTIQ_PROJECT_ROOT` pointing to a `tmp_path` with zero `.glb` files. Call `run(action="play")`. Assert that `bridge.request` was called with `params` containing `timeout` equal to `10.0`, and that the `timeout` kwarg to `bridge.request` is `15.0` (10.0 + 5).

### Test: timeout auto-detects -- 60 GLB files gives 15.0s

Create 60 `.glb` files in the `tmp_path` directory (they can be empty -- only presence matters). Call `run(action="play")`. Assert `params["timeout"]` is `15.0` and the `timeout` kwarg is `20.0` (15.0 + 5).

### Test: timeout auto-detects -- 120 GLB files gives 20.0s

Create 120 `.glb` files (can be in subdirectories, since `rglob` is used). Call `run(action="play")`. Assert `params["timeout"]` is `20.0` and the `timeout` kwarg is `25.0` (20.0 + 5).

### Test: explicit timeout > 0 overrides auto-detection

Call `run(action="play", timeout=30.0)`. Assert `params["timeout"]` is `30.0` and the `timeout` kwarg is `35.0` (30.0 + 5). The GLB counting logic should not run at all (no filesystem scanning).

### Test: missing GODOTIQ_PROJECT_ROOT env var gives 15.0s default

Do NOT set `GODOTIQ_PROJECT_ROOT` in the environment. Call `run(action="play")`. Assert `params["timeout"]` is `15.0` and the `timeout` kwarg is `20.0` (15.0 + 5).

### Test: bridge.request receives timeout kwarg = computed_timeout + 5

This is the critical invariant test. For any computed timeout value, verify the `timeout` kwarg passed to `bridge.request()` is exactly `computed_timeout + 5`. Use `mock_bridge.request.call_args` to inspect both `kwargs["timeout"]` and `kwargs["params"]["timeout"]`.

### Test: bridge.request receives params with timeout = computed_timeout (for GDScript)

Verify that the `params` dict passed to `bridge.request("run", params=...)` includes `timeout` set to the raw computed value (NOT the +5 version). GDScript uses this value for its own polling timeout.

### Test: action="stop" does not compute adaptive timeout

Call `run(action="stop")`. Assert that `bridge.request` is called with `"stop"` and no timeout or params overrides. The adaptive timeout logic only applies to `action="play"`. No filesystem scanning should occur.

### Mock setup pattern

```python
mock_bridge = AsyncMock()
mock_bridge.request.return_value = {
    "action": "play",
    "success": True,
    "scene": "res://main.tscn",
    "waited_seconds": 2.5,
}
with patch("godotiq.tools.bridge.run.get_bridge", return_value=mock_bridge):
    result = await run(action="play", scene="main")
```

For environment variables, use `patch.dict(os.environ, {"GODOTIQ_PROJECT_ROOT": str(tmp_path)})` or `patch.dict(os.environ, {}, clear=True)` to remove the variable.

For GLB file creation in tests:

```python
for i in range(count):
    (tmp_path / f"asset_{i}.glb").touch()
```

For subdirectory GLB files (to test `rglob`):

```python
subdir = tmp_path / "models" / "buildings"
subdir.mkdir(parents=True)
for i in range(count):
    (subdir / f"building_{i}.glb").touch()
```

## Implementation Details

### Changes to `src/godotiq/tools/bridge/run.py`

The current file is 29 lines. It needs these modifications:

**1. Add imports**

Add `import os` and `from pathlib import Path` at the top.

**2. Add `timeout` parameter to the function signature**

```python
async def run(
    action: str = "play",
    scene: str = "main",
    timeout: float = 0,
) -> dict:
```

The `timeout` parameter defaults to `0`, meaning auto-detect.

**3. Add a helper function for timeout computation**

Create a module-level helper (not async, pure logic):

```python
def _compute_timeout(explicit_timeout: float) -> float:
    """Compute scene launch timeout based on project asset count.

    If explicit_timeout > 0, return it directly.
    Otherwise, count .glb files under GODOTIQ_PROJECT_ROOT:
      >100 GLB → 20.0s
      >50  GLB → 15.0s
      else     → 10.0s
    If GODOTIQ_PROJECT_ROOT is not set → 15.0s default.
    """
```

Logic:
1. If `explicit_timeout > 0`, return `explicit_timeout` immediately
2. Read `GODOTIQ_PROJECT_ROOT` from `os.environ.get("GODOTIQ_PROJECT_ROOT")`
3. If not set, return `15.0`
4. Use `Path(root).rglob("*.glb")` to count `.glb` files. Use `sum(1 for _ in ...)` to avoid building a full list in memory
5. Apply thresholds: `>100` returns `20.0`, `>50` returns `15.0`, else `10.0`

**4. Update the play branch**

For `action == "play"`:
- Call `_compute_timeout(timeout)` to get the computed value
- Call `bridge.request("run", params={"scene": scene, "timeout": computed_timeout}, timeout=computed_timeout + 5)`
- Return the bridge response directly (the GDScript handler now returns `{action, success, scene, waited_seconds}`)

**5. Keep the stop branch unchanged**

For `action == "stop"`:
- Continue calling `bridge.request("stop")` with no timeout override
- Return `{"action": "stop"}` as before

**6. Update the return value for play**

The current code constructs `{"action": "play", "scene": scene}` locally. Change this to return the bridge response dict directly. The GDScript `_handle_run` (modified in Section 2) now returns a richer response:
```json
{"action": "play", "success": true, "scene": "res://main.tscn", "waited_seconds": 2.5}
```

### Changes to `src/godotiq/tools/bridge/__init__.py`

The MCP wrapper `godotiq_run` currently calls `_run(action=action, scene=scene)`. Since the `timeout` parameter is internal (auto-detected, not exposed to the agent via MCP), the wrapper call does NOT need to change. The wrapper continues to call `_run(action=action, scene=scene)` and the `timeout` defaults to `0` (auto-detect).

No changes needed in `__init__.py` for this section.

### Key implementation constraint

Do NOT try to access the session object. The session lives in the MCP lifespan context and is not importable from `run.py`. Use the `GODOTIQ_PROJECT_ROOT` environment variable directly via `os.environ`.

### Edge cases to handle

- `GODOTIQ_PROJECT_ROOT` points to a nonexistent directory: `rglob` will raise or return empty. Wrap in a try/except and fall back to `15.0`.
- `rglob("*.glb")` on a very large directory tree: This should still be fast enough since it only counts files, but consider using `itertools.islice` to short-circuit once you hit 101 files (the highest threshold).
- Symlinks creating loops: `rglob` follows symlinks by default in some Python versions. This is unlikely in Godot projects but could theoretically cause issues. The try/except handles this.