I now have all the context needed. Let me generate the section content.

# Section 2: Pre-flight Scene Checks (GDScript)

## Overview

This section adds a `_preflight_scene_check()` helper to `godotiq_server.gd` that validates preconditions before `_handle_build_scene` and `_handle_node_ops` execute. Instead of cryptic errors, the agent receives an immediate, actionable error message explaining exactly what to do (which tool to call, with what arguments) to fix the problem.

**Dependency**: Section 01 (Editor State in Every Response) must be implemented first. Pre-flight failures are reported via `_send_error()`, which Section 01 modifies to include `_editor_state`. This means preflight error responses will automatically include ambient context (open scene, game running, recent errors).

## File to Modify

`/Users/salvatorefarruggio/Desktop/godotiq/godot-addon/addons/godotiq/godotiq_server.gd`

## Background: Current State

Both `_handle_build_scene` (line 1142) and `_handle_node_ops` (line 351) already have partial scene checks:

- They call `EditorInterface.get_edited_scene_root()` and send a `NO_SCENE` error if null
- The error message is currently `"No scene is currently open in the editor"` -- not actionable (it does not tell the agent how to fix the problem)
- Neither handler checks whether the correct scene is open (wrong-scene errors surface as confusing node-not-found failures downstream)

## Design

### New Helper: `_preflight_scene_check(params: Dictionary) -> Dictionary`

This function returns either `{"ok": true}` or `{"ok": false, "error": "actionable message"}`.

**Check 1 -- No scene is open:**
If `EditorInterface.get_edited_scene_root()` is null, return:
```
{"ok": false, "error": "No scene is open in the editor. Call godotiq_exec to open a scene first: EditorInterface.open_scene_from_path('res://YourScene.tscn')"}
```

**Check 2 -- Wrong scene** (only if `params` contains a `"scene"` key):
If the open scene's `scene_file_path` does not match the value in `params["scene"]`, return:
```
{"ok": false, "error": "Scene 'res://Current.tscn' is open but you requested 'res://Target.tscn'. Save and switch scenes first: call godotiq_save_scene() then godotiq_exec('EditorInterface.open_scene_from_path(\"res://Target.tscn\")')"}
```
The actual scene paths in the message must be interpolated from the real values. This check must NOT auto-open a different scene, as that would discard unsaved changes.

If both checks pass, return `{"ok": true}`.

### Integration

Call `_preflight_scene_check(params)` at the very top of both `_handle_build_scene` and `_handle_node_ops`. If the check returns `{"ok": false}`, call `_send_error(peer_id, id, "PREFLIGHT_FAILED", check_result["error"])` and return early.

This replaces the existing `EditorInterface.get_edited_scene_root() == null` checks in both handlers. The existing `scene_root` variable assignment should move after the preflight check, since the check already verifies the scene root is non-null.

The `undo_redo == null` check should remain in each handler after the preflight (it is a separate concern).

### Important Constraints

- Do NOT auto-open scenes. Provide instructions for the agent to do it explicitly.
- Error messages must be actionable: include the exact tool name and arguments the agent should use.
- The `scene` key in params comes from the `node_ops` tool signature (the `scene` parameter). For `build_scene`, the equivalent is `target_scene` in params. The preflight should check for both `"scene"` and `"target_scene"` keys.

## Tests

These tests verify the Python-side handling of preflight error responses from the bridge. The actual GDScript `_preflight_scene_check` logic runs in the Godot editor and is tested manually, but the error response format and content are testable from Python by examining how `BridgeError` propagates to the tool wrappers.

### Test file

`/Users/salvatorefarruggio/Desktop/godotiq/tests/test_tools/test_preflight_checks.py`

### Test stubs

```python
"""Tests for pre-flight scene check error messages.

These test the Python-side handling of preflight error responses from
the GDScript bridge. The actual GDScript preflight logic runs in Godot
and is tested manually.
"""

from __future__ import annotations

import pytest

from godotiq.bridge.connection import BridgeError


class TestPreflightNoSceneOpen:
    """Verify error message content when no scene is open."""

    def test_preflight_error_contains_godotiq_exec(self) -> None:
        """The error message for no-scene-open must mention godotiq_exec
        so the agent knows which tool to call."""
        error = BridgeError(
            "PREFLIGHT_FAILED",
            "No scene is open in the editor. Call godotiq_exec to open a scene first: "
            "EditorInterface.open_scene_from_path('res://YourScene.tscn')",
        )
        assert "godotiq_exec" in error.error_message

    def test_preflight_error_contains_open_scene_from_path(self) -> None:
        """The error message must include the open_scene_from_path instruction."""
        error = BridgeError(
            "PREFLIGHT_FAILED",
            "No scene is open in the editor. Call godotiq_exec to open a scene first: "
            "EditorInterface.open_scene_from_path('res://YourScene.tscn')",
        )
        assert "open_scene_from_path" in error.error_message

    def test_preflight_error_code_is_preflight_failed(self) -> None:
        """The error code must be PREFLIGHT_FAILED for preflight failures."""
        error = BridgeError("PREFLIGHT_FAILED", "No scene is open")
        assert error.code == "PREFLIGHT_FAILED"


class TestPreflightWrongScene:
    """Verify error message content when the wrong scene is open."""

    def test_preflight_wrong_scene_includes_both_names(self) -> None:
        """Error for wrong-scene must include both the current and expected scene paths."""
        current = "res://Current.tscn"
        expected = "res://Target.tscn"
        msg = (
            f"Scene '{current}' is open but you requested '{expected}'. "
            f"Save and switch scenes first: call godotiq_save_scene() then "
            f"godotiq_exec('EditorInterface.open_scene_from_path(\"{expected}\")')"
        )
        error = BridgeError("PREFLIGHT_FAILED", msg)
        assert current in error.error_message
        assert expected in error.error_message

    def test_preflight_wrong_scene_suggests_save_first(self) -> None:
        """Error for wrong-scene must suggest saving before switching."""
        msg = (
            "Scene 'res://A.tscn' is open but you requested 'res://B.tscn'. "
            "Save and switch scenes first: call godotiq_save_scene() then "
            "godotiq_exec('EditorInterface.open_scene_from_path(\"res://B.tscn\")')"
        )
        error = BridgeError("PREFLIGHT_FAILED", msg)
        assert "save" in error.error_message.lower()
        assert "godotiq_save_scene" in error.error_message


class TestPreflightNoAutoOpen:
    """Verify preflight does NOT auto-open scenes."""

    def test_preflight_does_not_auto_open(self) -> None:
        """The error message instructs the agent to open the scene manually,
        not auto-opening it. The message must contain instructions, not
        confirmation of an action taken."""
        msg = (
            "Scene 'res://A.tscn' is open but you requested 'res://B.tscn'. "
            "Save and switch scenes first: call godotiq_save_scene() then "
            "godotiq_exec('EditorInterface.open_scene_from_path(\"res://B.tscn\")')"
        )
        error = BridgeError("PREFLIGHT_FAILED", msg)
        # The message should contain "call" (instruction) not "opened" (action taken)
        assert "call" in error.error_message.lower()


class TestPreflightPassesOnSuccess:
    """Verify that valid bridge responses pass through without preflight errors."""

    @pytest.mark.asyncio
    async def test_build_scene_succeeds_when_scene_open(self) -> None:
        """When the scene is open (mocked bridge returns success),
        build_scene returns a normal result without preflight error."""
        # This test verifies the happy path: no BridgeError is raised.
        # Use a mock bridge that returns a success response.
        # The build_scene tool wrapper should return a result dict
        # with 'created' key and no 'error' key.
        pass  # Implement with mock bridge

    @pytest.mark.asyncio
    async def test_node_ops_succeeds_when_scene_open(self) -> None:
        """When the scene is open (mocked bridge returns success),
        node_ops returns a normal result without preflight error."""
        pass  # Implement with mock bridge


class TestPreflightErrorPropagation:
    """Verify preflight errors propagate correctly through tool wrappers."""

    @pytest.mark.asyncio
    async def test_build_scene_returns_preflight_error(self) -> None:
        """When bridge raises BridgeError with PREFLIGHT_FAILED code,
        godotiq_build_scene catches it and returns an error dict."""
        # The tool wrapper catches BridgeError and formats:
        #   {"error": "[PREFLIGHT_FAILED] <message>"}
        # Verify the error dict contains the actionable message.
        pass  # Implement with mock bridge

    @pytest.mark.asyncio
    async def test_node_ops_returns_preflight_error(self) -> None:
        """When bridge raises BridgeError with PREFLIGHT_FAILED code,
        godotiq_node_ops catches it and returns an error dict."""
        pass  # Implement with mock bridge
```

### What the tests verify

1. **Error message content**: The no-scene error mentions `godotiq_exec` and `open_scene_from_path` so the agent knows exactly what to call.
2. **Wrong-scene error content**: Both the current and requested scene paths appear in the message, and the message suggests saving before switching.
3. **No auto-open side effect**: The error message is an instruction ("call ...") not a confirmation of an action taken.
4. **Error code**: Preflight failures use the `PREFLIGHT_FAILED` error code consistently.
5. **Propagation**: The tool wrappers (`godotiq_build_scene`, `godotiq_node_ops`) catch `BridgeError` and return a formatted error dict containing the actionable message.
6. **Happy path**: When the scene is correctly open, the tools return normal results without preflight errors.

## GDScript Implementation Guidance

### Where to add `_preflight_scene_check`

Add it as a new function in `godotiq_server.gd`, near the existing `_send_error` function (around line 1494). The function signature is:

```gdscript
func _preflight_scene_check(params: Dictionary) -> Dictionary:
    """Check that a scene is open and matches the requested scene."""
```

### How to modify `_handle_build_scene` (line 1142)

Replace the existing `scene_root == null` check at the top with a call to `_preflight_scene_check`. The `build_scene` tool sends its scene parameter as `target_scene` in the params dict, so the preflight helper should check for that key as well as `"scene"`.

Current code (lines 1142-1146):
```gdscript
func _handle_build_scene(peer_id: int, id: String, params: Dictionary) -> void:
    var scene_root := EditorInterface.get_edited_scene_root()
    if scene_root == null:
        _send_error(peer_id, id, "NO_SCENE", "No scene is currently open in the editor")
        return
```

Replace with:
```gdscript
func _handle_build_scene(peer_id: int, id: String, params: Dictionary) -> void:
    var preflight := _preflight_scene_check(params)
    if not preflight["ok"]:
        _send_error(peer_id, id, "PREFLIGHT_FAILED", preflight["error"])
        return
    var scene_root := EditorInterface.get_edited_scene_root()
    # scene_root is guaranteed non-null after preflight passes
```

### How to modify `_handle_node_ops` (line 351)

Apply the same pattern. The `node_ops` tool sends a `scene` parameter in params.

Current code (lines 357-360):
```gdscript
    var scene_root := EditorInterface.get_edited_scene_root()
    if scene_root == null:
        _send_error(peer_id, id, "NO_SCENE", "No scene is currently open in the editor")
        return
```

Replace the scene_root null check with preflight. Insert the preflight call after the operations validation (keep the operations check first since it does not depend on scene state):

```gdscript
func _handle_node_ops(peer_id: int, id: String, params: Dictionary) -> void:
    var operations: Array = params.get("operations", [])
    if not (operations is Array) or operations.size() == 0:
        _send_error(peer_id, id, "INVALID_PARAMS", "operations must be a non-empty array")
        return
    var preflight := _preflight_scene_check(params)
    if not preflight["ok"]:
        _send_error(peer_id, id, "PREFLIGHT_FAILED", preflight["error"])
        return
    var scene_root := EditorInterface.get_edited_scene_root()
    # scene_root is guaranteed non-null after preflight passes
```

### Scene key lookup in preflight

The preflight function should check for a requested scene path in params using both `"scene"` and `"target_scene"` keys:

```gdscript
var requested_scene: String = ""
if params.has("scene"):
    requested_scene = str(params["scene"])
elif params.has("target_scene"):
    requested_scene = str(params["target_scene"])
```

Only perform the wrong-scene check when `requested_scene` is non-empty. An empty/missing scene key means the caller does not care which scene is open, so only the null-scene check applies.

## Checklist

1. Add `_preflight_scene_check(params: Dictionary) -> Dictionary` to `godotiq_server.gd`
2. Implement Check 1: scene root is null -> actionable error mentioning `godotiq_exec` and `open_scene_from_path`
3. Implement Check 2: wrong scene open (only if `scene` or `target_scene` key present) -> actionable error mentioning both scene paths, `godotiq_save_scene()`, and `open_scene_from_path`
4. Modify `_handle_build_scene` to call preflight at top, replace existing null check
5. Modify `_handle_node_ops` to call preflight after operations validation, replace existing null check
6. Create test file `tests/test_tools/test_preflight_checks.py` with the test stubs above
7. Run `pytest tests/ -v` to verify all tests pass and no regressions