# Sprint 10b Implementation Plan: Script Error Detection + Real Gameplay Testing

## 1. Background & Motivation

GodotIQ is an MCP server that bridges AI assistants with the Godot 4.x editor via a WebSocket-based addon. The AI agent can read scenes, modify nodes, run the game, and inspect runtime state. However, two critical gaps prevent reliable autonomous game development:

**Gap 1: No visibility into GDScript compilation errors.** When the agent writes a script with errors (wrong enum name, failed type inference, missing class), Godot shows these in the editor's Errors tab, but the agent never sees them. The existing `_editor_state.recent_errors` only captures runtime errors sent via `EngineDebugger` from the game process. Compilation errors happen in the editor process and use a different system entirely.

**Gap 2: Superficial gameplay testing.** The MCP prompt tells the agent to "gameplay test," but in practice the agent just starts the game, runs `state_inspect` to read initial values, and stops. It never clicks buttons, places towers, or verifies that interactive features actually work during gameplay.

**Gap 3: Excessive verbosity.** The agent tends to write multi-paragraph plans between tool calls instead of just acting. The prompt needs stronger guidance toward immediate action.

This sprint addresses all three gaps with targeted changes to the Godot addon, MCP tools, and the development prompt.

---

## 2. Architecture Overview

### Current Error Flow (Runtime Only)
```
Game Process (runtime.gd) → EngineDebugger("godotiq:error")
    → debugger.gd captures message
    → server._record_error(msg)
    → _recent_errors circular buffer (max 10)
    → _editor_state.recent_errors (last 3 per response)
```

### New Error Flow (Compilation + Runtime)

**Primary path (all Godot 4.x versions):**
```
check_errors handler:
  Scans scene scripts + autoloads → load() + can_instantiate() + reload()
    → If load fails or can_instantiate() returns false → error detected
    → Returns structured error array with file path and pass/fail status

_handle_run pre-check:
  Before EditorInterface.play_*() → runs synchronous script check
    → Calls EditorInterface.save_all_open_scenes() first
    → Uses load() + can_instantiate() on scene scripts + autoloads
    → If errors found → _send_error with SCRIPT_ERRORS code, does NOT start game
    → If clean → proceeds normally
```

**Enhanced path (Godot 4.5+ with Logger):**
```
Editor Process:
  Logger subclass → intercepts ALL errors (parse, semantic, runtime)
    → Filters for script errors (ERR_SCRIPT, ERR_ERROR with .gd paths)
    → Stores in _script_errors buffer (separate from _recent_errors)
    → Adds line numbers and detailed error messages to check_errors results
```

The Logger is an **enhancement**, not a requirement. The core `load() + can_instantiate()` approach works on any Godot 4.x version and provides pass/fail per script. When the Logger is available (4.5+), it adds line-number detail and specific error messages.

### Files Modified

| File | Purpose |
|------|---------|
| `godot-addon/addons/godotiq/godotiq_server.gd` | Logger integration (optional), `_handle_check_errors`, pre-run check in `_handle_run` |
| `src/godotiq/tools/bridge/check_errors.py` | New Python bridge wrapper |
| `src/godotiq/tools/bridge/__init__.py` | Register `godotiq_check_errors` MCP tool |
| `src/godotiq/prompts/godot_development_prompt_v2.md` | Real Gameplay Testing, Test-Driven Completion, efficiency rules |
| `tests/test_tools/test_check_errors.py` | Tests for the new tool |

### Files NOT Modified
- Parsers (tscn, gd, scene_resolver, project_index)
- Analysis tools (scene_map, dependency_graph, etc.)
- build_scene, node_ops implementations
- Existing tests

---

## 3. GDScript Error Logger (godotiq_server.gd) — Optional Enhancement

### 3.1 Logger Subclass (Godot 4.5+ Only)

Godot 4.5 introduces the `Logger` class with virtual methods `_log_error` and `_log_message` that intercept all engine output. We register a custom Logger when the server initializes, **only if the Logger class is available**.

The Logger subclass is defined as an inner class within `godotiq_server.gd`. It holds a reference to the server's error buffer and a `Mutex` for thread safety (Logger methods can be called from any thread).

**Key behaviors:**
- `_log_error(function, file, line, code, rationale, editor_notify, error_type, script_backtraces)` — captures errors where `file` ends in `.gd` or `.tscn`, or where `error_type` is `ERR_SCRIPT`
- Stores structured error entries: `{file: String, line: int, message: String, type: String, timestamp: float}`
- Does NOT write to `_recent_errors` — the Logger only populates `_script_errors`, keeping runtime and compilation error channels separate
- `_script_errors` has a max size (50 entries) and can be cleared on demand
- Does NOT call `print()` or `push_error()` inside handlers (prevents infinite recursion)

**Registration:** In `_ready()`, after the TCP server starts, attempt to register the Logger with a version check:
```gdscript
var _error_logger  # Variant — null if Logger unavailable
var _has_logger: bool = false

# In _ready():
if ClassDB.class_exists("Logger"):
    _error_logger = GodotIQLogger.new(self)
    OS.add_logger(_error_logger)
    _has_logger = true
```

### 3.2 Script Error Buffer

New instance variables on the server:
```gdscript
var _script_errors: Array = []  # [{file, line, message, type, timestamp}]
var _script_error_mutex: Mutex = Mutex.new()
```

**Mutex pattern (explicit):**
- `_clear_script_errors()`: Acquires mutex → clears array → releases mutex
- `_append_script_error(entry)`: Acquires mutex → appends if under max size → releases mutex
- `_get_script_errors() -> Array`: Acquires mutex → duplicates array → releases mutex → returns copy

Each operation acquires and releases the mutex independently. The Logger callback and the handler never nest mutex acquisitions (no deadlock risk).

---

## 4. check_errors Handler (godotiq_server.gd)

### 4.1 Handler Registration

Add `"check_errors"` to the `_dispatch` match statement, pointing to `_handle_check_errors`.

### 4.2 Handler Logic

`_handle_check_errors(peer_id: int, id: String, params: Dictionary) -> void`

**Parameters:**
- `scope` (optional, default `"scene"`): One of `"project"`, `"scene"`, or a `res://` path
  - `"project"` — all `.gd` files in `res://`
  - `"scene"` — scripts used by the currently open scene + autoloads
  - `res://path/to/script.gd` — single file check

**Algorithm:**

1. If Logger is available (`_has_logger`): Clear `_script_errors` buffer
2. Collect script paths based on scope:
   - For `"project"`: Recursively scan `EditorInterface.get_resource_filesystem().get_filesystem()` for `.gd` files
   - For `"scene"`: Get scripts from current scene tree (iterate nodes, check `script` property) + get autoloads from `ProjectSettings`
   - For specific path: Just that one file
3. For each script path:
   a. `ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)` — bypass cache to get fresh state
   b. If result is null → append `{file: path, error: "Failed to load script", line: 0}` to errors
   c. If result is a `GDScript`:
      - Check `can_instantiate()` — if false, there are parse errors → append basic error entry
      - Call `reload()` — if returns non-OK, there are compilation errors → append basic error entry
      - If Logger is available, it captures detailed error messages with line numbers during reload
   d. Brief yield (`await get_tree().process_frame`) every N scripts to prevent UI freeze
4. If Logger is available: Read `_script_errors` buffer and merge with basic errors (Logger entries have line numbers, basic entries don't)
5. Deduplicate errors by file+line+message (all unique tuples kept — multiple errors on same line with different messages are distinct issues)
6. Send response:
```gdscript
send_response(peer_id, id, {
    "errors": errors_array,
    "total": errors_array.size(),
    "scripts_checked": total_scripts_checked,
    "scope": scope_used,
})
```

### 4.3 Filesystem Scanning Helper

`_get_script_paths_recursive(dir: EditorFileSystemDirectory) -> Array[String]`

Recursively walks `EditorInterface.get_resource_filesystem().get_filesystem()` collecting all files ending in `.gd`. Excludes these directories (non-user code):
- `addons/` — third-party addon scripts
- `.godot/` — Godot import cache
- `build/` — export builds
- `export/` — export templates

### 4.4 Scene Script Collection Helper

`_get_scene_script_paths(root: Node) -> Array[String]`

Walks the scene tree from the edited scene root. For each node with a `script` property that is a `GDScript`, adds its `resource_path` to the result.

**Autoload discovery** uses the actual ProjectSettings API (there is no `get_autoload_list()` method):
```gdscript
for prop in ProjectSettings.get_property_list():
    if prop.name.begins_with("autoload/"):
        var value: String = ProjectSettings.get_setting(prop.name)
        var path: String = value.lstrip("*")  # Strip singleton marker
        if path.ends_with(".gd"):
            result.append(path)
        elif path.ends_with(".tscn"):
            # Autoload points to a scene — load it and check root script
            var scene = load(path)
            if scene and scene is PackedScene:
                var state = scene.get_state()
                if state.get_node_count() > 0:
                    for i in state.get_node_property_count(0):
                        if state.get_node_property_name(0, i) == "script":
                            var script_path = state.get_node_property_value(0, i)
                            if script_path is GDScript:
                                result.append(script_path.resource_path)
```

---

## 5. Pre-run Script Check (godotiq_server.gd)

### 5.1 Modified _handle_run

Before calling `EditorInterface.play_*()`, insert a **synchronous** script validation step:

1. Call `EditorInterface.save_all_open_scenes()` to ensure disk matches editor state
2. Determine which scene will be played (from `params.scene`)
3. Collect script paths using `_bundled` property (no instantiation) + autoload scripts
4. For each script: `load()` + `can_instantiate()` check (synchronous, no `await`)
5. If any scripts fail: return error via `_send_error`:
```gdscript
_send_error(peer_id, id, "SCRIPT_ERRORS", "Cannot start game: %d script(s) have errors. Use godotiq_check_errors for details." % errors.size())
```
6. If all scripts pass: proceed with existing `play_main_scene()` / `play_current_scene()` / `play_custom_scene()` logic

**Important:** The pre-run check uses `_send_error` exclusively (not `send_response` with embedded error data). This is consistent with existing error patterns and the Python bridge's `BridgeError` handling path.

### 5.2 Lightweight Check (No Logger Dependency)

The pre-run check is always synchronous and uses only `load() + can_instantiate()`. No Logger, no `await`, no mutex. It only checks scene scripts + autoloads (not full project), so the blocking time is bounded to the small number of scripts in a typical scene.

The full `check_errors` tool uses the Logger for detailed error messages with line numbers; the pre-run check just needs a pass/fail per script.

### 5.3 Scene Script Discovery for Pre-run (No Instantiation)

To find scripts used by a scene **without** instantiating the scene (which would run `_init()` and have side effects):

1. Load the `PackedScene` resource
2. Use `PackedScene.get_state()` (returns `SceneState`) to iterate nodes
3. For each node in the SceneState, check its properties for script references
4. Collect all script `resource_path` values

This avoids scene instantiation entirely. The `SceneState` API gives read-only access to the scene's node structure and properties.

For the currently edited scene: use `EditorInterface.get_edited_scene_root()` and walk the live tree directly (already instantiated by the editor).

5. Add autoload scripts from ProjectSettings (same pattern as Section 4.4)

---

## 6. Python Bridge Tool (check_errors.py)

### 6.1 Bridge Wrapper

New file `src/godotiq/tools/bridge/check_errors.py`:

```python
async def check_errors(scope: str = "scene") -> dict:
    """Check GDScript files for compilation errors."""
```

Calls `bridge.request("check_errors", params={"scope": scope})` and returns the result.

### 6.2 MCP Tool Registration

In `src/godotiq/tools/bridge/__init__.py`, add the `@mcp.tool()` decorated function:

```python
@mcp.tool(name="godotiq_check_errors", annotations={...})
async def godotiq_check_errors(scope: str = "scene", ctx: Context = None) -> dict:
    """Check GDScript files for compilation/parse errors.

    Args:
        scope: "scene" (current scene + autoloads), "project" (all .gd files),
               or "res://path/to/script.gd" (single file)
    """
```

No `detail` parameter — errors don't have meaningful detail levels. Apply `apply_output_limit(result, "normal")` on the result for consistency with other bridge tools.

Follows the existing error-handling pattern (catch BridgeConnectionError, BridgeTimeoutError, BridgeError, Exception).

### 6.3 Timeout

Add `"check_errors": 15.0` to `TOOL_TIMEOUTS` in protocol.py. Full project scans on large projects may take several seconds.

---

## 7. MCP Prompt Updates (godot_development_prompt_v2.md)

### 7.1 Test-Driven Completion Section

Add a new section **before** the "MANDATORY: Final QA" section. This establishes the mindset that "compiles" ≠ "done":

- The agent is not done when code compiles or the game starts without crashing
- Done means every player-facing feature works during actual gameplay
- Examples of "broken but compiles": towers that don't shoot, buttons that don't respond, scores that don't update
- After building, always ask: "If a player tried every feature, would it all work?"
- If unsure, test it with runtime tools

### 7.2 Real Gameplay Testing (Replace QA Step 5)

Replace the current "Gameplay test" step in the QA checklist with an expanded protocol:

**Step 5.1** — Check for script errors first:
- Call `godotiq_check_errors(scope="scene")` before starting
- If errors found, fix them before attempting to run

**Step 5.2** — Start and verify initial state:
- `godotiq_run(action="play")`, wait for confirmation
- `godotiq_state_inspect` to verify starting values

**Step 5.3** — Test every interactive feature:
- Use `godotiq_ui_map(detail="brief")` to find clickable elements
- Use `godotiq_input` to click/tap each interactive element
- Use `godotiq_state_inspect` to verify expected changes
- If something didn't change, that's a bug — fix it

**Step 5.4** — Wait for a complete cycle:
- If the game has waves/rounds, start one
- Wait with `input(commands=[{"wait_ms": 10000}])`
- Verify enemies were processed, resources changed

**Step 5.5** — Check for runtime errors:
- Read `_editor_state.recent_errors` from the response
- If errors appeared during gameplay, fix them

**Step 5.6** — Stop and report results

### 7.3 Efficiency Rules Enhancement

Add to the existing "Efficiency Rules" section:

**Act immediately:** When you know the next step, execute it. Don't write multi-paragraph plans. The tool will tell you if something is wrong — act on the response, don't pre-plan for every contingency.

**Maximum 2 paragraphs between tool calls:** After a tool call, explain what happened in 1-2 sentences, then make the next call. Don't write essay-length analysis.

### 7.4 Error Recovery Addition

Add to the "Error Recovery" section:
- `SCRIPT_ERRORS` → `godotiq_check_errors(scope="scene")` to see details, then fix scripts
- This is the error code returned by `run` when pre-run check fails

### 7.5 Tool Reference Addition

Add `godotiq_check_errors` to the "UNDERSTAND" section of the tool reference:

**`godotiq_check_errors`** — Check GDScript files for compilation/parse errors. Call before `godotiq_run` or after writing scripts. Use `scope="scene"` for current scene scripts, `scope="project"` for all scripts.

---

## 8. Test Plan

### 8.1 Unit Tests (test_check_errors.py)

Test the Python bridge wrapper:

1. **test_check_errors_no_errors** — Mock bridge returns empty errors array. Verify clean response.
2. **test_check_errors_with_errors** — Mock bridge returns errors. Verify they're passed through.
3. **test_check_errors_scope_parameter** — Verify scope is forwarded correctly to bridge.
4. **test_check_errors_connection_error** — BridgeConnectionError returns helpful hint.
5. **test_check_errors_timeout** — BridgeTimeoutError returns timeout message.

### 8.2 Run Pre-check Tests

Test the modified run tool behavior:

1. **test_run_blocked_by_script_errors** — Mock bridge returns SCRIPT_ERRORS as BridgeError. Verify the response includes error code and hint.
2. **test_run_clean_scripts_proceeds** — Mock bridge returns successful run. Verify normal behavior.

### 8.3 MCP Registration Tests

1. **test_check_errors_tool_registered** — Verify `godotiq_check_errors` appears in tool list.
2. **test_check_errors_invalid_scope** — Pass invalid scope value, verify graceful handling.
3. **test_check_errors_empty_project** — Mock bridge returns 0 scripts checked, verify response.

### 8.4 Manual Verification

1. Write a script with intentional error (undefined identifier like `TONE_MAP_ACES`)
2. Call `godotiq_check_errors` — verify error appears in response
3. Call `godotiq_run(action="play")` — verify it's blocked with error details
4. Fix the script
5. Call `godotiq_run(action="play")` — verify it starts normally

---

## 9. Implementation Order

### Section 1: Logger + Error Buffer (godotiq_server.gd)
Add the Logger inner class (with version check), `_script_errors` buffer, mutex with explicit lock/unlock pattern, and conditional registration in `_ready()`. This is an optional enhancement — the check_errors handler works without it.

### Section 2: check_errors Handler (godotiq_server.gd)
Add `_handle_check_errors`, filesystem scanning helper (excluding `.godot/`, `build/`, `export/`, `addons/`), scene script collection helper (with correct autoload discovery via `ProjectSettings.get_property_list()`), and dispatch entry. Uses Logger when available for detailed errors, falls back to basic pass/fail without it.

### Section 3: Pre-run Check (godotiq_server.gd)
Modify `_handle_run` to call a synchronous, lightweight script check before starting the game. Saves all open scenes first. Uses `PackedScene.get_state()` (no instantiation) for script discovery. Reports errors via `_send_error` (consistent with existing error patterns). Can reuse the scene script collection helper from Section 2.

### Section 4: Python Tool (check_errors.py + __init__.py)
Create the bridge wrapper, register the MCP tool (no `detail` parameter), apply `apply_output_limit`, add timeout config. Depends on Section 2 being functional on the Godot side.

### Section 5: Prompt Updates (godot_development_prompt_v2.md)
Add Test-Driven Completion, Real Gameplay Testing, efficiency rules, error recovery, and tool reference. Independent of other sections.

### Section 6: Tests (test_check_errors.py)
Unit tests for the Python tool, registration tests, edge case tests (invalid scope, empty project). Depends on Section 4 for the code to test.

---

## 10. Edge Cases & Considerations

### Thread Safety
The Logger's `_log_error` can be called from any thread. All writes to `_script_errors` go through the mutex. Each mutex operation (clear, append, get) acquires and releases independently — no nested locks, no deadlock risk. The `check_errors` handler runs on the main thread.

### Performance
Full project scan on a large project (hundreds of scripts) could take several seconds. The handler yields every N scripts to prevent UI freeze. The 15-second timeout gives ample room. The pre-run check is synchronous and limited to scene scripts + autoloads (typically < 20 scripts), so blocking time is negligible.

### Cache Busting
`ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)` is critical — without it, Godot returns cached (possibly stale) script objects that may not reflect recent file changes. `CACHE_MODE_IGNORE` is available in Godot 4.1+.

### Save Before Check
The pre-run check calls `EditorInterface.save_all_open_scenes()` before loading scripts from disk, ensuring the on-disk state matches the editor's in-memory state.

### Autoload Discovery
`ProjectSettings` stores autoloads as properties like `autoload/GameManager = "*res://autoloads/game_manager.gd"`. The helper iterates `ProjectSettings.get_property_list()` matching `autoload/*`, strips the `*` singleton marker, and handles both `.gd` (direct script) and `.tscn` (scene with root script) autoloads.

### Excluded Directories
The filesystem scanner skips: `addons/` (third-party), `.godot/` (import cache), `build/` and `export/` (build artifacts). These contain non-user code that the agent didn't write and can't fix.

### Logger Availability
The Logger class exists only in Godot 4.5+. A `ClassDB.class_exists("Logger")` check in `_ready()` determines availability. When unavailable, `check_errors` still works via `load() + can_instantiate()` alone — it just reports pass/fail per script without line-number detail. When available, it adds rich error messages with file, line, message, and error type.

### Scene Script Discovery Without Instantiation
The pre-run check uses `PackedScene.get_state()` / `SceneState` to read script references without instantiating the scene. This avoids running `_init()`, `@onready` assignments, and other side effects that could corrupt editor state or fail on broken scripts.
