# Sprint 5b Spec — Editor Exec + Save Scene (Synthesized)

## Objective

Implement 2 capabilities that close the agent's editing loop:

1. **Editor-side exec** — Execute arbitrary GDScript in the editor context with full access to `EditorInterface`, `get_tree()`, scene nodes. Currently exec only works game-side (RefCounted sandbox).
2. **Save scene** — Save the current scene in-place via `EditorInterface.save_scene()`.

After Sprint 5b: **29 MCP tools** total (27 existing + save_scene + exec upgrade = enhancement not new tool count).

---

## Scope Changes from Original Spec

Based on research and interview:

1. **save_scene simplified**: No `path` parameter. `save_scene_as(path)` does not exist in Godot 4. Only `EditorInterface.save_scene()` (in-place, no params). Save As via PackedScene deferred to future sprint.
2. **Prompt update included**: Update `godot_development.md` to document exec context parameter and save_scene tool.
3. **Godot bug #87046 mitigated**: The `@tool` annotation in dynamic source code is the fix. The spec already includes `"@tool\nextends Node\n\n" + code`. Primary approach (script.new()) should work. File-based fallback retained as insurance.

---

## Technical Decisions

### Editor Exec

- **Single tool**: `godotiq_exec` with `context` parameter (`"editor"` default, `"game"` alternative)
- **Dispatch**: New `"exec_editor"` entry in server dispatch table. Existing `"exec"` unchanged (game-side).
- **Safety**: Block `OS.execute`, `OS.kill`, `OS.shell_open`, `DirAccess.remove`. Allow `FileAccess.open` entirely.
- **Execution model**: `GDScript.new()` + `source_code = "@tool\nextends Node\n\n" + code` + `reload()` + `script.new()` + `add_child()` → gives `get_tree()` access.
- **Fallback**: If `script.new()` returns null (Godot bug), write to temp file + `load()` + instantiate. Synchronous — may need retry.
- **Async**: Keep handler synchronous. No `await` in fallback.

### Save Scene

- **In-place only**: `EditorInterface.save_scene()` returns Error code
- **No path parameter**: `save_scene_as()` doesn't exist in Godot 4
- **Response**: `{saved: true/false, scene_path, scene_name, error}`

### Registration

- Modify existing `godotiq_exec` registration to add `context` parameter
- Add new `godotiq_save_scene` registration following bridge pattern

---

## Files

### CREATE:
| File | Purpose |
|------|---------|
| `src/godotiq/tools/bridge/save_scene.py` | Python MCP tool |
| `tests/test_bridge/test_save_scene.py` | Tests (3-4 cases) |
| `tests/test_bridge/test_exec_editor.py` | Tests (7 cases) |

### MODIFY:
| File | Change |
|------|--------|
| `godot-addon/addons/godotiq/godotiq_server.gd` | Add `exec_editor` + `save_scene` dispatch, handlers |
| `src/godotiq/tools/bridge/exec_code.py` | Add `context` param, route to `exec_editor` or `exec` |
| `src/godotiq/tools/bridge/__init__.py` | Update exec registration, add save_scene registration |
| `src/godotiq/prompts/godot_development.md` | Document exec context + save_scene |

### DO NOT TOUCH:
- Phase 1 files (parsers, resolvers, session, code, spatial, animation, assets, memory tools)
- `godotiq_plugin.gd`, `godotiq_runtime.gd`, `godotiq_debugger.gd`
- Existing test files

---

## Codebase Context (from research)

### Current exec_code.py
- Signature: `async def exec_code(code="", timeout_ms=5000) -> dict`
- Bridge call: `bridge.request("exec", params={...}, timeout=10.0)`
- No `context` parameter yet

### Bridge __init__.py Pattern
- `@mcp.tool(name="...", annotations={...})` with try/except wrapper
- Error types: `BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError`
- Import: `from godotiq.tools.bridge.X import Y as _Y`

### Server Dispatch (14 current entries)
- Editor-side: ping, editor_context, scene_tree, node_ops, run, stop, undo_history
- Game-side (forward): screenshot, perf_snapshot, input, exec, state_inspect, nav_query, watch
- After Sprint 5b: 16 entries (+exec_editor, +save_scene)

### Response Methods
- `send_response(peer_id, id, result_dict)` → `{"id": ..., "status": "ok", "result": {...}}`
- `_send_error(peer_id, id, code, message)` → `{"id": ..., "status": "error", "error": {...}}`

---

## Verification

1. `pytest tests/ -v` → 622+ existing + ~11 new = 633+
2. Import smoke tests for exec_code and save_scene
3. GDScript audit: no ternaries, no snapf, no f-strings
4. Dispatch has both `"exec"` AND `"exec_editor"`
5. Live Godot testing: editor exec basic, get_tree(), EditorInterface, save_scene
6. Prompt updated with new tool docs
