# GodotIQ Phase 2 — Godot Addon + Bridge Tools

## Executive Summary

Phase 2 trasforma GodotIQ da analizzatore filesystem-only a **ponte bidirezionale tra AI agent e Godot editor/runtime**. L'addon GDScript pure (zero compilazione) usa WebSocket per comunicare col Python MCP server, e il debugger nativo di Godot per comunicare col gioco in esecuzione.

**Stato Phase 1**: 460 test, 13 tool filesystem-only, hardened su PFT (86 scripts, 41 scenes, 876 nodi, 28 autoloads).

**Obiettivo Phase 2**: 14 bridge tool + addon GDScript. Dopo Phase 2: 27 tool totali (13 + 14).

---

## Architettura Rivista (post-ricerca)

### Scoperta chiave: EngineDebugger

Il DESIGN.md originale prevedeva WebSocket ovunque. La ricerca sulle API Godot 4 rivela un canale IPC nativo **già esistente**: `EngineDebugger` + `EditorDebuggerPlugin`.

```
AI Agent ←(stdio)→ Python MCP Server ←(WebSocket:6007)→ GDScript EditorPlugin
                                                              ↕
                                                    (EngineDebugger - nativo, zero config)
                                                              ↕
                                                      Running Game (autoload)
```

**WebSocket** serve SOLO per `Python MCP ↔ Editor` (porta 6007).
**EngineDebugger** serve per `Editor ↔ Running Game` (canale nativo Godot, sempre attivo quando il gioco gira dall'editor, zero porte extra).

Questo elimina il "two-server pattern" di GDAI e semplifica l'addon da 4 file a 3 core file.

### Confronto architetturale

```
GDAI:     Agent ←stdio→ Python proxy ←HTTP:3571→ C++ GDExtension ←HTTP→ C++ RuntimeServer
GodotIQ:  Agent ←stdio→ Python MCP   ←WS:6007→   GDScript Plugin  ←Debugger→ GDScript Autoload
```

### I 3 componenti

```
godot-addon/addons/godotiq/
├── plugin.cfg                     # Metadata EditorPlugin
├── godotiq_plugin.gd              # EditorPlugin: lifecycle, abilita/disabilita, registra debugger
├── godotiq_server.gd              # WebSocket server (TCPServer + WebSocketPeer) nell'editor
├── godotiq_debugger.gd            # EditorDebuggerPlugin: cattura messaggi dal gioco
├── godotiq_runtime.gd             # Autoload: runtime nel gioco (screenshot, input, metriche)
└── godotiq_observer.gd            # Autoload helper: monitoraggio continuo (perf, state, errori)
```

> Nota: 5 file ma 3 ruoli concettuali: Editor (plugin+server+debugger), Runtime (runtime+observer).

### Flusso comunicazione dettagliato

```
1. MCP tool call (es. "godotiq_screenshot")
   → Python MCP server riceve via stdio

2. Python invia comando via WebSocket a porta 6007
   → {"id": "req_001", "method": "screenshot", "params": {"viewport": "game"}}

3. godotiq_server.gd riceve il messaggio nell'editor
   → Se è operazione editor-side (scene tree, proprietà): esegue direttamente
   → Se è operazione game-side (screenshot gioco, input sim): inoltra via EngineDebugger

4. Per operazioni game-side:
   → EditorDebuggerPlugin.send_message("godotiq:screenshot", [params])
   → EngineDebugger nel gioco cattura il messaggio
   → godotiq_runtime.gd esegue (cattura viewport, simula input)
   → EngineDebugger.send_message("godotiq:result", [data]) → torna all'editor

5. godotiq_debugger.gd riceve il risultato via _capture()
   → Lo passa a godotiq_server.gd
   → WebSocket risponde al Python MCP server
   → MCP risponde all'agent via stdio
```

### API Godot confermate dalla ricerca

**EditorInterface** (accessibile dall'EditorPlugin):
- `play_main_scene()`, `play_current_scene()`, `play_custom_scene(path)`
- `stop_playing_scene()`, `is_playing_scene()`, `get_playing_scene()`
- `get_edited_scene_root()` → Node root della scena nell'editor
- `get_editor_viewport_3d(idx)` → SubViewport 3D editor (0-3)
- `get_editor_viewport_2d()` → SubViewport 2D editor
- `open_scene_from_path(path)` → apre scena nell'editor
- `save_scene()`, `save_scene_as(path)`
- `get_open_scenes()` → PackedStringArray
- `get_selection()` → EditorSelection
- `get_editor_undo_redo()` → EditorUndoRedoManager

**EditorDebuggerPlugin** (editor-side):
- `_has_capture(prefix)` → return prefix == "godotiq"
- `_capture(message, data, session_id)` → riceve messaggi dal gioco
- `_setup_session(session_id)` → configurazione alla connessione
- `get_session(id).send_message(msg, data)` → invia messaggi al gioco
- Segnali: `started`, `stopped`, `breaked`, `continued`

**EngineDebugger** (game-side, nell'autoload):
- `register_message_capture("godotiq", callable)` → registra handler
- `send_message("godotiq:result", [data])` → invia all'editor
- Formato messaggi: `"prefix:action"` (es. `"godotiq:screenshot_result"`)

**WebSocket server** (Godot 4 pattern):
- `TCPServer.new()` + `TCPServer.listen(6007)`
- `WebSocketPeer.new()` + `WebSocketPeer.accept_stream(tcp_connection)`
- Polling in `_process()`: `peer.poll()` → `peer.get_ready_state()`
- `peer.send_text(json)` per text frames, `peer.get_packet()` per ricezione

**RenderingServer** (metriche performance):
- `get_rendering_info(RENDERING_INFO_TOTAL_OBJECTS_IN_FRAME)`
- `get_rendering_info(RENDERING_INFO_TOTAL_PRIMITIVES_IN_FRAME)`
- `get_rendering_info(RENDERING_INFO_TOTAL_DRAW_CALLS_IN_FRAME)`
- `get_rendering_info(RENDERING_INFO_TEXTURE_MEM_USED)`
- `get_rendering_info(RENDERING_INFO_BUFFER_MEM_USED)`
- `get_rendering_info(RENDERING_INFO_VIDEO_MEM_USED)`

**NavigationServer3D** (pathfinding):
- `map_get_path(map, from, to, optimize)` → PackedVector3Array
- `query_path(params, result)` → NavigationPathQueryResult3D
- `map_get_closest_point(map, point)` → Vector3
- `map_get_closest_point_to_segment(map, from, to)` → Vector3

**EditorUndoRedoManager** (undo/redo per node_ops):
- `create_action(name)` → inizia batch
- `add_do_method(callable)` / `add_undo_method(callable)`
- `add_do_property(obj, prop, value)` / `add_undo_property(obj, prop, value)`
- `commit_action()` → finalizza (undoable con Ctrl+Z)

---

## Protocollo WebSocket (Python ↔ Editor)

### Formato messaggi

```json
// Request (Python MCP → GDScript addon)
{
  "id": "req_001",
  "method": "screenshot",
  "params": {
    "viewport": "game",
    "scale": 0.5,
    "format": "webp"
  }
}

// Response (GDScript addon → Python MCP)
{
  "id": "req_001",
  "status": "ok",
  "result": {
    "image": "<base64>",
    "format": "webp",
    "size": [540, 960]
  }
}

// Error response
{
  "id": "req_001",
  "status": "error",
  "error": {
    "code": "GAME_NOT_RUNNING",
    "message": "Cannot take game screenshot: no scene is playing"
  }
}

// Push event (GDScript addon → Python MCP, unsolicited)
{
  "event": "runtime_error",
  "data": {
    "message": "Null reference in worker.gd:87",
    "timestamp": 3.2,
    "source": "scripts/entities/worker.gd:87"
  }
}

// Progress notification (durante operazioni lunghe)
{
  "event": "progress",
  "data": {
    "request_id": "req_001",
    "progress": 0.6,
    "message": "Waiting for game to start..."
  }
}

// Health check
{
  "id": "ping",
  "method": "ping"
}
→ {"id": "ping", "status": "ok", "result": {"editor_version": "4.6", "game_running": true}}
```

### Protocollo EngineDebugger (Editor ↔ Game)

```gdscript
# Editor → Game (via EditorDebuggerSession.send_message)
"godotiq:screenshot"       → [{"scale": 0.5, "format": "webp"}]
"godotiq:input"            → [{"commands": [...]}]
"godotiq:query_state"      → [{"nodes": ["Worker_1", "Printer_1"]}]
"godotiq:query_perf"       → [{}]
"godotiq:start_watch"      → [{"nodes": ["Worker_1"], "properties": ["state"]}]
"godotiq:stop_watch"       → [{}]
"godotiq:exec"             → [{"code": "func run():\n\treturn ..."}]
"godotiq:nav_query"        → [{"from": [0,0,0], "to": [5,0,-3]}]

# Game → Editor (via EngineDebugger.send_message)
"godotiq:screenshot_result"  → [base64_image, format, width, height]
"godotiq:input_result"       → [success, side_effects_json]
"godotiq:state_result"       → [nodes_state_json]
"godotiq:perf_result"        → [fps, draw_calls, triangles, vram]
"godotiq:watch_update"       → [node, property, old_value, new_value]
"godotiq:error"              → [message, source, stack_trace]
"godotiq:exec_result"        → [status, result_string, error_string]
"godotiq:nav_result"         → [path_points_json, distance, reachable]
```

---

## Tool Plan — 14 Bridge Tool + Feature Nuove

### Mapping DESIGN.md → Implementazione

| ID | Tool | DESIGN.md | Runtime? | Sprint |
|----|------|-----------|----------|--------|
| A01 | `godotiq_screenshot` | Sì | Game: sì, Editor: no | 4a |
| A02 | `godotiq_scene_tree` | Sì (runtime) | Sì | 4b |
| A03 | `godotiq_run` | Sì | Sì | 4a |
| A04 | `godotiq_input` | Sì | Sì | 4c |
| A05 | `godotiq_node_ops` | Sì | Editor-side | 4b |
| A06 | `godotiq_script_ops` | Sì | Editor-side | 4b |
| A07 | `godotiq_file_ops` | Sì | No (filesystem) | 4b |
| A08 | `godotiq_exec` | Sì | Both | 4c |
| NEW1 | `godotiq_perf_snapshot` | NO — NUOVO | Game: sì | 4a |
| NEW2 | `godotiq_state_inspect` | NO — NUOVO | Game: sì | 4c |
| NEW3 | `godotiq_nav_query` | Parziale (I01) | Game: sì | 4d |
| NEW4 | `godotiq_undo_history` | NO — NUOVO | Editor-side | 4d |
| NEW5 | `godotiq_editor_context` | NO — NUOVO | Editor-side | 4a |
| NEW6 | `godotiq_watch` | NO — NUOVO | Game: sì | 4d |

### Feature NUOVE (non nel DESIGN.md originale)

#### `NEW1` — `godotiq_perf_snapshot`
**Motivazione**: Il DESIGN.md mette le metriche di performance dentro `godotiq_screenshot` e `godotiq_run`, ma un tool dedicato è più utile. L'agent può chiedere "come sta andando il gioco?" senza fare screenshot. Inoltre, le metriche aggregate nel tempo (min/max/avg FPS su N frame) sono impossibili da ottenere con uno screenshot singolo.

```
INPUT:
{
  "duration_frames": 60,     // raccoglie dati per 60 frame (≈1s a 60fps)
  "include_memory": true
}

OUTPUT:
{
  "fps": {"current": 58, "min": 42, "max": 62, "avg": 55.3},
  "draw_calls": {"current": 34, "min": 30, "max": 52, "avg": 37.2},
  "triangles": {"current": 89000, "min": 75000, "max": 112000, "avg": 91000},
  "memory": {
    "texture_mb": 128.4,
    "buffer_mb": 45.2,
    "video_total_mb": 203.8
  },
  "physics": {
    "active_bodies": 23,
    "collision_pairs": 8
  },
  "scene": {
    "total_nodes": 876,
    "orphan_nodes": 0
  },
  "thresholds_violated": [
    {"metric": "draw_calls", "peak": 52, "limit": 50, "when_frame": 23}
  ]
}
```

#### `NEW2` — `godotiq_state_inspect`
**Motivazione**: L'agent ha bisogno di sapere lo stato runtime di nodi specifici SENZA fare screenshot. "In che stato è il Worker_1?" "Quanto cash ha il giocatore?" "Quanti ordini sono pending?" Queste domande richiedono introspezione diretta dei nodi/proprietà a runtime.

```
INPUT:
{
  "queries": [
    {"node": "/root/Main/Entities/Worker_1", "properties": ["state", "global_position", "current_task"]},
    {"node": "/root/EconomyManager", "properties": ["cash", "daily_income"]},
    {"node": "/root/OrderManager", "properties": ["pending_orders.size()", "active_orders.size()"]},
    {"autoload": "GameManager", "properties": ["current_day", "game_speed", "is_paused"]}
  ]
}

OUTPUT:
{
  "results": [
    {
      "node": "Worker_1",
      "found": true,
      "properties": {
        "state": "WALKING",
        "global_position": [1.2, 0, -0.5],
        "current_task": {"type": "FETCH_MATERIAL", "target": "MaterialShelf"}
      }
    },
    {
      "node": "EconomyManager",
      "found": true,
      "properties": {"cash": 1250.0, "daily_income": 320.5}
    },
    {
      "node": "OrderManager",
      "found": true,
      "properties": {"pending_orders.size()": 3, "active_orders.size()": 2}
    },
    {
      "node": "GameManager",
      "found": true,
      "properties": {"current_day": 15, "game_speed": 1.0, "is_paused": false}
    }
  ]
}
```

#### `NEW3` — `godotiq_nav_query`
**Motivazione**: Il DESIGN.md ha `I01 nav_audit` che è filesystem-only (analizza la .tscn). Ma il pathfinding reale richiede il `NavigationServer3D` a runtime. Questo tool fa query di pathfinding live: "Il worker può raggiungere la stampante P4?" "Quanto è lungo il percorso?" "Ci sono detour?"

```
INPUT:
{
  "from_node": "Worker_1",                    // usa global_position del nodo
  "to_node": "PrinterSlot_P4",               // oppure coordinate dirette
  // oppure:
  "from_position": [0, 0, 0],
  "to_position": [5, 0, -3],
  "optimize": true,
  "simplify": true
}

OUTPUT:
{
  "reachable": true,
  "distance": 8.2,
  "direct_distance": 3.1,
  "efficiency_ratio": 0.38,                   // direct/actual — basso = detour grande
  "path_points": [[0,0,0], [0.5,0,0], [0.5,0,-1], ...],
  "waypoint_count": 12,
  "estimated_walk_time": 4.5,                 // a velocità worker standard
  "bottleneck": {
    "segment": [3, 7],
    "reason": "Narrow corridor between Wall_East and Desk_Office (0.4m clearance)"
  }
}
```

#### `NEW4` — `godotiq_undo_history`
**Motivazione**: Quando l'agent fa modifiche via `node_ops`, ogni operazione è registrata con `EditorUndoRedoManager` e può essere annullata con Ctrl+Z. Ma l'agent non sa COSA ha fatto nelle ultime N operazioni. Questo tool dà visibilità sulla cronologia undo — essenziale per il debug di sessioni lunghe.

```
INPUT:
{
  "last_n": 10
}

OUTPUT:
{
  "history": [
    {"action": "Move StorageShelf to (1.8, 0, -0.5)", "undoable": true},
    {"action": "Scale Printer_New to (0.3, 0.3, 0.3)", "undoable": true},
    {"action": "Add child Printer_7 to Entities", "undoable": true}
  ],
  "total_actions": 3,
  "can_undo": true,
  "can_redo": false
}
```

#### `NEW5` — `godotiq_editor_context`
**Motivazione**: L'agent non sa cosa sta facendo l'utente nell'editor. Quale scena ha aperta? Quale nodo ha selezionato? Il gioco è in esecuzione? Questo tool dà un "snapshot" dell'editor in un singolo call — la prima cosa che l'agent chiede quando inizia a lavorare con il bridge attivo.

```
INPUT: {}

OUTPUT:
{
  "editor_version": "4.6.0",
  "current_scene": "res://scenes/main.tscn",
  "open_scenes": ["res://scenes/main.tscn", "res://scenes/ui/game_hud.tscn"],
  "selected_nodes": ["StorageShelf", "StorageShelf2"],
  "game_running": false,
  "playing_scene": null,
  "project_path": "/home/user/projects/print-farm-tycoon/",
  "addons_enabled": ["godotiq"],
  "main_screen": "3D",
  "undo_available": true,
  "redo_available": false
}
```

#### `NEW6` — `godotiq_watch`
**Motivazione**: Il DESIGN.md ha `watch_nodes` dentro `godotiq_run`, ma un sistema di watch persistente è molto più potente. L'agent attiva il watch su specifici nodi/proprietà, e il runtime autoload pusha aggiornamenti ogni volta che qualcosa cambia. L'agent può poi LEGGERE gli aggiornamenti accumulati quando vuole, senza polling continuo.

```
INPUT:
{
  "action": "start",           // "start", "stop", "read", "clear"
  "watches": [
    {"node": "Worker_1", "properties": ["state", "global_position"]},
    {"node": "EconomyManager", "properties": ["cash"]},
    {"node": "Printer_1", "properties": ["state"]}
  ],
  "sample_interval_ms": 500    // quanto spesso campionare posizione (0 = solo su cambio)
}

// Dopo un po':
INPUT: {"action": "read"}

OUTPUT:
{
  "events": [
    {"t": 1.2, "node": "Worker_1", "property": "state", "from": "IDLE", "to": "WALKING"},
    {"t": 1.2, "node": "Worker_1", "property": "global_position", "value": [0.1, 0, -0.1]},
    {"t": 2.5, "node": "Worker_1", "property": "global_position", "value": [0.8, 0, -0.5]},
    {"t": 2.5, "node": "EconomyManager", "property": "cash", "from": 1250, "to": 1295},
    {"t": 3.0, "node": "Worker_1", "property": "state", "from": "WALKING", "to": "CARRYING"},
    {"t": 4.8, "node": "Printer_1", "property": "state", "from": "IDLE", "to": "RESERVED"}
  ],
  "duration_seconds": 5.0,
  "watches_active": 3,
  "events_total": 6
}
```

---

## Sprint Breakdown

### Sprint 4a — Foundation + Connection (1.5 settimane)

**Obiettivo**: Addon funzionante, connessione WebSocket verificata, screenshot + run/stop + perf + editor context.

**File da creare:**

1. `godot-addon/addons/godotiq/plugin.cfg`
2. `godot-addon/addons/godotiq/godotiq_plugin.gd` — EditorPlugin
3. `godot-addon/addons/godotiq/godotiq_server.gd` — WebSocket server
4. `godot-addon/addons/godotiq/godotiq_debugger.gd` — EditorDebuggerPlugin
5. `godot-addon/addons/godotiq/godotiq_runtime.gd` — Autoload runtime
6. `src/godotiq/bridge/connection.py` — WebSocket client (Python side)
7. `src/godotiq/bridge/protocol.py` — Message encoding/decoding
8. `src/godotiq/tools/bridge/screenshot.py` — A01
9. `src/godotiq/tools/bridge/run.py` — A03
10. `src/godotiq/tools/bridge/perf_snapshot.py` — NEW1
11. `src/godotiq/tools/bridge/editor_context.py` — NEW5

**File da modificare:**
- `src/godotiq/server.py` (o `__main__.py`) — registra nuovi tool
- `.godotiq.json` — sezione `server.addon_port`

**Tool implementati**: 4 (A01 screenshot, A03 run, NEW1 perf_snapshot, NEW5 editor_context)

**Dettagli implementativi:**

**godotiq_plugin.gd** — Lifecycle:
```gdscript
@tool
extends EditorPlugin

var _server: Node  # godotiq_server.gd
var _debugger: EditorDebuggerPlugin  # godotiq_debugger.gd
const AUTOLOAD_NAME = "GodotIQRuntime"

func _enter_tree():
    _server = preload("godotiq_server.gd").new()
    _server.name = "GodotIQServer"
    add_child(_server)
    
    _debugger = preload("godotiq_debugger.gd").new()
    add_debugger_plugin(_debugger)
    _debugger.server = _server  # cross-reference
    
    add_autoload_singleton(AUTOLOAD_NAME, "res://addons/godotiq/godotiq_runtime.gd")

func _exit_tree():
    remove_autoload_singleton(AUTOLOAD_NAME)
    remove_debugger_plugin(_debugger)
    remove_child(_server)
    _server.queue_free()
```

**godotiq_server.gd** — WebSocket server core:
```gdscript
@tool
extends Node

var _tcp_server := TCPServer.new()
var _peers: Dictionary = {}  # peer_id → WebSocketPeer
var _next_peer_id: int = 0
var _port: int = 6007
var _pending_requests: Dictionary = {}  # request_id → {callback, timeout}

signal request_received(id: String, method: String, params: Dictionary)
signal game_message_received(message: String, data: Array)

func _ready():
    _port = _load_port_from_config()
    var err = _tcp_server.listen(_port)
    if err != OK:
        push_warning("GodotIQ: Cannot listen on port %d: %s" % [_port, error_string(err)])

func _process(_delta):
    # Accept new connections
    while _tcp_server.is_connection_available():
        var tcp = _tcp_server.take_connection()
        var ws = WebSocketPeer.new()
        ws.accept_stream(tcp)
        _peers[_next_peer_id] = ws
        _next_peer_id += 1
    
    # Poll all peers
    for peer_id in _peers.keys():
        var peer = _peers[peer_id]
        peer.poll()
        match peer.get_ready_state():
            WebSocketPeer.STATE_OPEN:
                while peer.get_available_packet_count():
                    var packet = peer.get_packet()
                    if peer.was_string_packet():
                        _handle_message(peer_id, packet.get_string_from_utf8())
            WebSocketPeer.STATE_CLOSED:
                _peers.erase(peer_id)

func _handle_message(peer_id: int, text: String):
    var json = JSON.parse_string(text)
    if json == null:
        _send_error(peer_id, "unknown", "PARSE_ERROR", "Invalid JSON")
        return
    var id = json.get("id", "unknown")
    var method = json.get("method", "")
    var params = json.get("params", {})
    request_received.emit(id, method, params)
    _dispatch(peer_id, id, method, params)

func send_response(peer_id: int, id: String, result: Dictionary):
    var msg = JSON.stringify({"id": id, "status": "ok", "result": result})
    if _peers.has(peer_id):
        _peers[peer_id].send_text(msg)

func send_event(event: String, data: Dictionary):
    var msg = JSON.stringify({"event": event, "data": data})
    for peer in _peers.values():
        if peer.get_ready_state() == WebSocketPeer.STATE_OPEN:
            peer.send_text(msg)
```

**godotiq_debugger.gd** — Ponte editor ↔ game:
```gdscript
@tool
extends EditorDebuggerPlugin

var server  # Referenza a godotiq_server.gd
var _session_id: int = -1

func _has_capture(prefix: String) -> bool:
    return prefix == "godotiq"

func _capture(message: String, data: Array, session_id: int) -> bool:
    # Messaggi dal gioco → inoltra al server → WebSocket → Python
    match message:
        "godotiq:screenshot_result":
            server.handle_game_response("screenshot", data)
        "godotiq:perf_result":
            server.handle_game_response("perf", data)
        "godotiq:state_result":
            server.handle_game_response("state", data)
        "godotiq:input_result":
            server.handle_game_response("input", data)
        "godotiq:error":
            server.send_event("runtime_error", {
                "message": data[0], "source": data[1]
            })
        "godotiq:watch_update":
            server.send_event("watch_update", {
                "node": data[0], "property": data[1],
                "from": data[2], "to": data[3], "time": data[4]
            })
        _:
            return false
    return true

func _setup_session(session_id: int):
    _session_id = session_id
    var session = get_session(session_id)
    session.started.connect(_on_game_started)
    session.stopped.connect(_on_game_stopped)

func send_to_game(message: String, data: Array):
    if _session_id >= 0:
        get_session(_session_id).send_message(message, data)

func _on_game_started():
    server.send_event("game_started", {})

func _on_game_stopped():
    server.send_event("game_stopped", {})
```

**godotiq_runtime.gd** — Autoload nel gioco:
```gdscript
extends Node

var _watches: Dictionary = {}  # node_path → {properties: [...], last_values: {...}}
var _watch_events: Array = []
var _sample_timer: float = 0.0
var _sample_interval: float = 0.5

func _ready():
    EngineDebugger.register_message_capture("godotiq", _on_message)

func _process(delta):
    _sample_timer += delta
    if _sample_timer >= _sample_interval and not _watches.is_empty():
        _sample_timer = 0.0
        _sample_watched_nodes()

func _on_message(message: String, data: Array) -> bool:
    match message:
        "godotiq:screenshot":
            _take_screenshot(data[0] if data.size() > 0 else {})
        "godotiq:query_perf":
            _send_perf_snapshot()
        "godotiq:query_state":
            _query_state(data[0] if data.size() > 0 else {})
        "godotiq:input":
            _simulate_input(data[0] if data.size() > 0 else {})
        "godotiq:start_watch":
            _start_watches(data[0] if data.size() > 0 else {})
        "godotiq:exec":
            _execute_code(data[0] if data.size() > 0 else {})
        "godotiq:nav_query":
            _nav_query(data[0] if data.size() > 0 else {})
        _:
            return false
    return true

func _take_screenshot(params: Dictionary):
    # Attende 1 frame per rendering completo
    await get_tree().process_frame
    var img = get_viewport().get_texture().get_image()
    var scale = params.get("scale", 0.5)
    if scale < 1.0:
        img.resize(int(img.get_width() * scale), int(img.get_height() * scale))
    var format = params.get("format", "webp")
    var buffer: PackedByteArray
    match format:
        "webp": buffer = img.save_webp_to_buffer()
        "png": buffer = img.save_png_to_buffer()
        "jpg": buffer = img.save_jpg_to_buffer(0.85)
    var b64 = Marshalls.raw_to_base64(buffer)
    EngineDebugger.send_message("godotiq:screenshot_result", [
        b64, format, img.get_width(), img.get_height()
    ])

func _send_perf_snapshot():
    var viewport_rid = get_viewport().get_viewport_rid()
    EngineDebugger.send_message("godotiq:perf_result", [JSON.stringify({
        "fps": Engine.get_frames_per_second(),
        "draw_calls": RenderingServer.get_rendering_info(
            RenderingServer.RENDERING_INFO_TOTAL_DRAW_CALLS_IN_FRAME),
        "triangles": RenderingServer.get_rendering_info(
            RenderingServer.RENDERING_INFO_TOTAL_PRIMITIVES_IN_FRAME),
        "objects": RenderingServer.get_rendering_info(
            RenderingServer.RENDERING_INFO_TOTAL_OBJECTS_IN_FRAME),
        "texture_mem": RenderingServer.get_rendering_info(
            RenderingServer.RENDERING_INFO_TEXTURE_MEM_USED),
        "buffer_mem": RenderingServer.get_rendering_info(
            RenderingServer.RENDERING_INFO_BUFFER_MEM_USED),
        "video_mem": RenderingServer.get_rendering_info(
            RenderingServer.RENDERING_INFO_VIDEO_MEM_USED),
        "total_nodes": get_tree().get_node_count(),
        "orphan_nodes": Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT),
        "physics_bodies": Performance.get_monitor(Performance.PHYSICS_3D_ACTIVE_OBJECTS),
        "physics_pairs": Performance.get_monitor(Performance.PHYSICS_3D_COLLISION_PAIRS),
    })])
```

**Python side** — `src/godotiq/bridge/connection.py`:
```python
import asyncio
import json
from typing import Any, Callable, Optional

class BridgeConnection:
    """WebSocket client that connects to the GodotIQ addon."""
    
    def __init__(self, host: str = "localhost", port: int = 6007):
        self.host = host
        self.port = port
        self._ws = None
        self._pending: dict[str, asyncio.Future] = {}
        self._event_handlers: dict[str, list[Callable]] = {}
        self._request_counter = 0
        self._connected = False
    
    async def connect(self, timeout: float = 5.0) -> bool:
        """Connect to the GodotIQ addon WebSocket server."""
        ...
    
    async def request(
        self, method: str, params: dict | None = None,
        timeout: float = 5.0
    ) -> dict:
        """Send a request and wait for response."""
        self._request_counter += 1
        req_id = f"req_{self._request_counter:04d}"
        msg = json.dumps({"id": req_id, "method": method, "params": params or {}})
        future = asyncio.get_event_loop().create_future()
        self._pending[req_id] = future
        await self._ws.send(msg)
        try:
            return await asyncio.wait_for(future, timeout)
        except asyncio.TimeoutError:
            self._pending.pop(req_id, None)
            raise TimeoutError(f"Bridge request '{method}' timed out after {timeout}s")
    
    def on_event(self, event: str, handler: Callable):
        """Register handler for push events from the addon."""
        self._event_handlers.setdefault(event, []).append(handler)
    
    @property
    def is_connected(self) -> bool:
        return self._connected
    
    @property
    def is_game_running(self) -> bool:
        """Cached state from game_started/game_stopped events."""
        return self._game_running
```

**Timeout strategy** (per-tool):
```python
TOOL_TIMEOUTS = {
    "ping": 2,
    "editor_context": 3,
    "screenshot": 30,           # viewport capture can be slow
    "screenshot_retry": 3,      # retry dopo 3s
    "screenshot_max_retries": 3,
    "run": 5,                   # il play_scene è veloce, monitoring è separato
    "stop": 3,
    "perf_snapshot": 5,
    "input": 65,                # input sequences possono essere lunghe
    "node_ops": 10,
    "script_ops": 10,
    "file_ops": 5,
    "exec": 10,
    "state_inspect": 5,
    "nav_query": 10,
    "watch_start": 3,
    "watch_read": 3,
}
```

**Criteri di verifica Sprint 4a**:
- [ ] `pytest tests/` → tutti i test Phase 1 passano ancora (regression)
- [ ] Addon attivabile in Godot senza errori nella console
- [ ] WebSocket ping/pong funzionante (Python → GDScript → Python)
- [ ] `godotiq_editor_context` restituisce info corrette
- [ ] `godotiq_run` avvia e ferma la scena main
- [ ] `godotiq_screenshot` cattura viewport del gioco (base64 webp)
- [ ] `godotiq_perf_snapshot` restituisce FPS, draw calls, triangles
- [ ] Screenshot retry funzionante (3 tentativi con delay 500ms)
- [ ] Progress notifications ogni 2s durante operazioni lunghe
- [ ] Gestione graceful della disconnessione (addon chiuso, Godot crashato)

---

### Sprint 4b — Editor Operations (1 settimana)

**Obiettivo**: Node ops con undo, script ops con patch, file ops, scene tree runtime.

**Tool implementati**: 4 (A02 scene_tree, A05 node_ops, A06 script_ops, A07 file_ops)

**Dettagli chiave:**

**A05 node_ops — Undo support**:
Ogni operazione passa per `EditorUndoRedoManager`, quindi l'utente può Ctrl+Z. Questo è CRITICO per la safety — se l'agent fa un errore, l'utente può annullare.

```gdscript
# In godotiq_server.gd, handler per node_ops:
func _handle_node_ops(operations: Array) -> Array:
    var undo_redo = EditorInterface.get_editor_undo_redo()
    var results = []
    
    undo_redo.create_action("GodotIQ: batch node operations")
    
    for op in operations:
        match op.get("op"):
            "move":
                var node = _find_node(op["node"])
                var old_pos = node.position
                var new_pos = Vector3(op["position"][0], op["position"][1], op["position"][2])
                undo_redo.add_do_property(node, "position", new_pos)
                undo_redo.add_undo_property(node, "position", old_pos)
                results.append({"op": "move", "node": op["node"], "status": "OK"})
            "scale":
                # simile...
            "set_property":
                var node = _find_node(op["node"])
                var old_val = node.get(op["property"])
                undo_redo.add_do_property(node, op["property"], op["value"])
                undo_redo.add_undo_property(node, op["property"], old_val)
                results.append({"op": "set_property", "status": "OK"})
    
    undo_redo.commit_action()
    return results
```

**A06 script_ops — Patch mode**:
Il find-and-replace di GDAI è il modo più sicuro di modificare script. L'agent non riscrive tutto il file, solo la parte che cambia.

```gdscript
func _handle_script_patch(path: String, patches: Array) -> Dictionary:
    var file = FileAccess.open(path, FileAccess.READ)
    var content = file.get_as_text()
    file.close()
    
    var applied = []
    var failed = []
    for patch in patches:
        var search = patch["search"]
        var replace = patch["replace"]
        if content.find(search) == -1:
            failed.append({"search": search, "error": "Not found in file"})
        else:
            content = content.replace(search, replace)
            applied.append({"search": search, "replace": replace})
    
    if failed.is_empty():
        file = FileAccess.open(path, FileAccess.WRITE)
        file.store_string(content)
        file.close()
    
    return {"applied": applied, "failed": failed, "written": failed.is_empty()}
```

**A02 scene_tree — Runtime version**:
Differenza dal Phase 1 `scene_map` (filesystem): questo legge il scene tree LIVE dal gioco in esecuzione, con stato corrente dei nodi (visibilità, proprietà runtime, animazioni in corso).

**Criteri di verifica Sprint 4b**:
- [ ] `godotiq_node_ops` muove/scala nodi + Ctrl+Z funziona
- [ ] `godotiq_script_ops` patch mode: find-and-replace corretto
- [ ] `godotiq_script_ops` validation pre-write (usa validate di Phase 1)
- [ ] `godotiq_file_ops` list/read/search funzionanti
- [ ] `godotiq_scene_tree` runtime restituisce albero dal gioco in esecuzione
- [ ] Tutti i tool gestiscono gracefully "gioco non in esecuzione"

---

### Sprint 4c — Input & Introspection (1 settimana)

**Obiettivo**: Input simulation, state inspection, exec escape hatch.

**Tool implementati**: 3 (A04 input, A08 exec, NEW2 state_inspect)

**A04 input — Sequential command pattern (da GDAI)**:
```gdscript
func _simulate_input(params: Dictionary):
    var commands = params.get("commands", [])
    var side_effects_before = _snapshot_autoloads() if params.get("track_side_effects", false) else {}
    
    for cmd in commands:
        if cmd.has("wait_ms"):
            await get_tree().create_timer(cmd["wait_ms"] / 1000.0).timeout
        elif cmd.has("actions"):
            for action in cmd["actions"]:
                Input.action_press(action)
            var hold = cmd.get("hold_ms", 70) / 1000.0
            await get_tree().create_timer(hold).timeout
            for action in cmd["actions"]:
                Input.action_release(action)
        elif cmd.has("target"):
            # Semantic targeting: trova il nodo UI e simula click
            var target_node = _find_ui_node(cmd["target"])
            if target_node:
                _simulate_click(target_node)
    
    var side_effects = {}
    if params.get("track_side_effects", false):
        side_effects = _diff_autoloads(side_effects_before, _snapshot_autoloads())
    
    # Wait for signal se specificato
    if params.has("wait_for"):
        # ... attendere signal o timeout
        pass
    
    EngineDebugger.send_message("godotiq:input_result", [
        JSON.stringify({"success": true, "side_effects": side_effects})
    ])
```

**A08 exec — Safety validation**:
```gdscript
func _execute_code(params: Dictionary):
    var code = params.get("code", "")
    
    # Safety check: deve iniziare con func run():
    if not code.strip_edges().begins_with("func run():"):
        EngineDebugger.send_message("godotiq:exec_result", [
            "BLOCKED", "", "Code must start with 'func run():'"
        ])
        return
    
    # Safety check: pattern bloccati
    var blocked = ["DirAccess.remove", "OS.execute", "OS.kill"]
    for pattern in blocked:
        if code.find(pattern) != -1:
            EngineDebugger.send_message("godotiq:exec_result", [
                "BLOCKED", "", "Blocked pattern: " + pattern
            ])
            return
    
    # Esegui come tool script
    var script = GDScript.new()
    script.source_code = "@tool\nextends RefCounted\n" + code
    var err = script.reload()
    if err != OK:
        EngineDebugger.send_message("godotiq:exec_result", [
            "SYNTAX_ERROR", "", "Compilation failed"
        ])
        return
    
    var obj = script.new()
    var result = obj.run()
    EngineDebugger.send_message("godotiq:exec_result", [
        "OK", str(result), ""
    ])
```

**Criteri di verifica Sprint 4c**:
- [ ] `godotiq_input` simula azioni (press/release/hold) correttamente
- [ ] Input simulation: una sola simulazione alla volta (constraint GDAI)
- [ ] `godotiq_input` semantic targeting: trova bottoni per nome nodo
- [ ] `godotiq_exec` blocca pattern pericolosi
- [ ] `godotiq_exec` timeout previene loop infiniti
- [ ] `godotiq_state_inspect` legge proprietà da nodi runtime
- [ ] `godotiq_state_inspect` supporta accesso autoload per nome

---

### Sprint 4d — Navigation, Watch, History + Polish (1 settimana)

**Obiettivo**: Nav query live, watch system, undo history, hardening, test su PFT.

**Tool implementati**: 3 (NEW3 nav_query, NEW4 undo_history, NEW6 watch)

**NEW3 nav_query — Live pathfinding**:
```gdscript
func _nav_query(params: Dictionary):
    var map = get_tree().root.get_world_3d().navigation_map
    var from = Vector3(params["from_position"][0], params["from_position"][1], params["from_position"][2])
    var to = Vector3(params["to_position"][0], params["to_position"][1], params["to_position"][2])
    
    # Verifica che from/to siano sulla navmesh
    var closest_from = NavigationServer3D.map_get_closest_point(map, from)
    var closest_to = NavigationServer3D.map_get_closest_point(map, to)
    
    var path = NavigationServer3D.map_get_path(map, closest_from, closest_to, true)
    
    var total_distance = 0.0
    for i in range(path.size() - 1):
        total_distance += path[i].distance_to(path[i + 1])
    
    var direct_distance = from.distance_to(to)
    
    EngineDebugger.send_message("godotiq:nav_result", [JSON.stringify({
        "reachable": path.size() > 1,
        "distance": total_distance,
        "direct_distance": direct_distance,
        "efficiency_ratio": direct_distance / max(total_distance, 0.001),
        "path_points": _v3_array_to_json(path),
        "waypoint_count": path.size(),
        "from_on_navmesh": from.distance_to(closest_from) < 0.1,
        "to_on_navmesh": to.distance_to(closest_to) < 0.1,
    })])
```

**Criteri di verifica Sprint 4d**:
- [ ] `godotiq_nav_query` calcola path corretto tra due punti
- [ ] `godotiq_nav_query` rileva punti fuori navmesh
- [ ] `godotiq_watch` start/read/stop/clear funzionanti
- [ ] Watch events accumulati correttamente nel tempo
- [ ] `godotiq_undo_history` restituisce azioni recenti
- [ ] Test completo su PFT: play → screenshot → input → state → perf → stop
- [ ] Gestione errori completa (gioco non running, nodo non trovato, timeout)
- [ ] Token optimization: risposte compatte per tutti i bridge tool
- [ ] Documentation: README setup, `.mcp.json` esempio

---

## Feature Predette — Cose che Serviranno Presto

### Prossime dopo Phase 2 (Phase 3 candidates)

1. **`godotiq_diff_scene`** — Confronta scene tree runtime vs filesystem. "Cosa è cambiato a runtime che non è stato salvato?" Utile per capire se le modifiche dell'agent hanno effetto.

2. **`godotiq_physics_query`** — Raycast e area queries a runtime. "C'è un muro tra A e B?" "Cosa c'è in questo punto?" Complementa nav_query con info fisiche.

3. **`godotiq_resource_inspect`** — Ispeziona risorse caricate (materiali, mesh, audio) a runtime. "Che shader usa questo modello?" "Quanto pesa questa texture?"

4. **`godotiq_signal_intercept`** — Intercetta signal specifici a runtime e logga quando vengono emessi. Complementa il signal_map di Phase 1 (statico) con dati live.

5. **`godotiq_console`** — Stream bidirezionale della console Godot. L'agent vede gli errori in tempo reale, può mandare comandi. Più leggero di `exec`.

6. **`godotiq_theme_inspect`** — Legge il tema UI corrente (colori, font, dimensioni) dal gioco a runtime. Utile per `ui_audit` e per generare CSS/stili coerenti.

7. **`godotiq_annotation_overlay`** — Disegna annotazioni temporanee nel viewport 3D (bounding box, frecce, etichette). L'agent può "puntare" a qualcosa per l'utente. Richiede DebugDraw nel runtime.

8. **`godotiq_replay`** — Registra e riproduce sequenze di input. L'agent può "registrare" un test manuale e poi rigiocarlo. Evoluzione di `godotiq_input`.

9. **`godotiq_breakpoint`** — Setta breakpoint condizionali via codice. "Fermati quando EconomyManager.cash < 0". Usa il debugger engine per pause intelligenti.

10. **`godotiq_hot_reload_watch`** — Monitora file GDScript modificati e trigga hot reload automatico. L'agent modifica uno script e il gioco si aggiorna senza restart.

---

## Decisioni Architetturali

### Perché WebSocket e non HTTP (come GDAI)?

| Aspetto | HTTP (GDAI) | WebSocket (noi) |
|---------|------------|-----------------|
| Push events | Impossibile — server deve aspettare request | Naturale — addon pusha errori, perf alerts |
| Screenshot | Retry loop con polling (fino a 30s) | Risposta diretta via messaggio |
| Input sim | Blocca 65s con polling | Stream risultati intermedi |
| Connessione | Nuova per ogni request | Persistente, riuso |
| Latenza | ~50-100ms per request overhead | ~1-5ms dopo connessione |
| Complessità | Più semplice (request/response) | Più complessa (state management) |

### Perché EngineDebugger e non secondo WebSocket?

| Aspetto | Secondo WebSocket | EngineDebugger |
|---------|-------------------|----------------|
| Config | Seconda porta da configurare | Zero config, sempre attivo |
| Sicurezza | Aperta al network | Solo editor↔game locale |
| Timing | Disponibile quando gioco è pronto | Disponibile dal momento in cui il gioco parte |
| Affidabilità | Potrebbe non connettersi in tempo | Garantito dal motore |
| Limitazione | Nessuna | Solo Array di Variant come payload |

### Perché NOT tutti i tool nel runtime?

I tool editor-side (node_ops, script_ops, file_ops, scene_tree) girano NELL'editor, non nel gioco. Motivi:
- `EditorUndoRedoManager` è disponibile solo nell'editor
- Modifiche ai nodi della scena editor non richiedono il gioco in esecuzione
- File operations sono I/O dell'editor, non del gioco
- Minimizza il codice nell'autoload runtime (meno rischio di interferire col gioco)

### Fallback quando addon non è attivo

I 13 tool di Phase 1 continuano a funzionare senza addon. I 14 bridge tool:
- `editor_context`: restituisce `{"addon_connected": false}` + info da filesystem
- `node_ops`, `script_ops`, `file_ops`: possono operare via filesystem (fallback degradato)
- Tutti gli altri: errore chiaro `"Addon not connected. Enable GodotIQ addon in Godot editor."`

---

## Rischi e Mitigazioni

| Rischio | Probabilità | Impatto | Mitigazione |
|---------|-------------|---------|-------------|
| EngineDebugger instabile o limitato | Media | Alto | Fallback a secondo WebSocket nel runtime se il debugger non basta |
| Latenza WebSocket troppo alta per screenshot | Bassa | Media | Ridurre risoluzione default (0.25x), formato webp |
| Conflitti autoload con progetto utente | Bassa | Alto | Nome univoco `GodotIQRuntime`, nessun segnale globale |
| Godot 4.2 vs 4.6 API differences | Media | Media | Testare su 4.3+ minimo, documentare versione minima |
| Payload EngineDebugger troppo grande (screenshot base64) | Media | Alto | Se Array di Variant non supporta stringhe lunghe, fallback a file temporaneo |
| Plugin non si attiva (errore GDScript) | Media | Alto | Validation script separato, error handling robusto in _enter_tree |
| Race condition WebSocket/Debugger | Media | Media | Request ID tracking, timeout per ogni request |

### Piano B per EngineDebugger

Se il canale EngineDebugger risulta troppo limitato (payload size, latenza, affidabilità), il fallback è:
- Runtime autoload apre ANCHE un WebSocket client verso porta 6007
- Il server WebSocket nell'editor gestisce sia la connessione Python che quella del gioco
- Questo aggiunge complessità ma risolve qualsiasi limitazione del debugger

---

## Test Strategy

### Test Python (pytest)

```
tests/
├── test_bridge/
│   ├── test_connection.py      # WebSocket client mock
│   ├── test_protocol.py        # Message encoding/decoding
│   ├── test_screenshot.py      # Tool input validation + response parsing
│   ├── test_run.py             # Tool input validation
│   ├── test_node_ops.py        # Operation validation
│   └── test_timeout.py         # Timeout + retry logic
```

I test Python NON testano la comunicazione reale con Godot (richiederebbe Godot aperto). Testano:
- Validazione input dei tool
- Parsing delle risposte
- Logica timeout e retry
- Fallback quando addon non connesso
- Serializzazione/deserializzazione protocollo

### Test Manuali Godot

Checklist per ogni sprint, eseguita manualmente su PFT:

```
□ Addon si attiva senza errori in Output
□ WebSocket accetta connessione (test con websocat o script Python)
□ Ping → Pong in <100ms
□ Screenshot editor funziona
□ Screenshot game funziona (con gioco in esecuzione)
□ Run/stop funziona
□ Node ops + undo funziona
□ Input simulation funziona
□ State inspect legge valori corretti
□ Perf snapshot restituisce metriche sensate
□ Nav query restituisce path valido
□ Watch accumula eventi correttamente
□ Errori runtime vengono pushati
□ Disconnessione graceful (chiudi Godot, chiudi Python)
□ Riconnessione dopo disconnessione
```

### Integration Test Script

Script Python che automatizza la checklist:

```python
# tests/integration/test_bridge_e2e.py
# Richiede: Godot aperto con PFT, addon attivo

async def test_full_workflow():
    conn = BridgeConnection()
    await conn.connect()
    
    # 1. Editor context
    ctx = await conn.request("editor_context")
    assert ctx["editor_version"].startswith("4.")
    
    # 2. Run game
    await conn.request("run", {"scene": "main"})
    await asyncio.sleep(2)  # attendi avvio
    
    # 3. Screenshot
    ss = await conn.request("screenshot", {"viewport": "game", "scale": 0.25})
    assert len(ss["image"]) > 1000  # base64 non vuoto
    
    # 4. Perf snapshot
    perf = await conn.request("perf_snapshot")
    assert perf["fps"] > 0
    
    # 5. State inspect
    state = await conn.request("state_inspect", {
        "queries": [{"autoload": "GameManager", "properties": ["current_day"]}]
    })
    assert state["results"][0]["found"]
    
    # 6. Stop game
    await conn.request("stop")
```

---

## Timeline Stimata

| Sprint | Durata | Tool | Totale cumulativo |
|--------|--------|------|-------------------|
| 4a: Foundation | 1.5 settimane | 4 (screenshot, run, perf, editor_context) | 17 |
| 4b: Editor Ops | 1 settimana | 4 (scene_tree, node_ops, script_ops, file_ops) | 21 |
| 4c: Input & Intro | 1 settimana | 3 (input, exec, state_inspect) | 24 |
| 4d: Nav + Polish | 1 settimana | 3 (nav_query, undo_history, watch) | 27 |
| **Totale** | **~4.5 settimane** | **14 bridge tool** | **27 tool** |

Dopo Phase 2: 27 tool (13 filesystem + 14 bridge). Mancano 4 tool per il target DESIGN.md di 31: E01 trace_flow, E02 diagnose, E03 test_flow, e H01-H03 (UI intelligence). Questi sono Phase 3.

---

## Dipendenze Python

Phase 1 usa solo `mcp` SDK. Phase 2 aggiunge:

```toml
[project]
dependencies = [
    "mcp>=1.0",
    "websockets>=12.0",  # WebSocket client asincrono
]
```

`websockets` è la libreria più matura per WebSocket asyncio in Python. Alternativa: `aiohttp` (più pesante ma include anche HTTP client se serve).

---

## `.mcp.json` Aggiornato

```json
{
  "mcpServers": {
    "godotiq": {
      "command": "python",
      "args": ["-m", "godotiq"],
      "env": {
        "GODOTIQ_PROJECT_ROOT": ".",
        "GODOTIQ_ADDON_PORT": "6007"
      }
    }
  }
}
```
