# Hot Reloader Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Implement selective, state-preserving hot-reload for `src/gui_2.py` with delegation pattern refactor, manual trigger via `Ctrl+Alt+R` and GUI button, and visual error tint feedback on failure.

**Architecture:** Separating code into **delegators** (thin `App` wrappers in `gui_2.py`) and **delegation targets** (actual render logic as module-level functions). State preservation via `copy.deepcopy` capture/restore around `importlib.reload`. Error tint overlay on reload failure using ImGui draw list.

**Tech Stack:** Python `importlib.reload`, `copy.deepcopy`, Dear PyGui / ImGui draw lists, `traceback`

---

## File Structure

| File | Responsibility |
|------|----------------|
| `src/hot_reloader.py` | New — `HotModule` dataclass and `HotReloader` class with registry, capture/restore, reload API |
| `src/gui_2.py` | Refactor render methods from `App._render_xxx` to module-level `render_xxx(app)` |
| `src/app_controller.py` | Delegation wrappers — `App._render_xxx` becomes `import src.gui_2 as g2; g2.render_xxx(self)` |
| `tests/test_hot_reloader.py` | Unit tests for `HotModule`, `HotReloader` registry and reload |
| `tests/test_hot_reload_integration.py` | Integration tests via `live_gui` fixture |

---

## Task 1: Create `src/hot_reloader.py` — HotModule Dataclass and HotReloader Core

**Files:**
- Create: `src/hot_reloader.py`
- Test: `tests/test_hot_reloader.py`

- [ ] **Step 1: Write failing test for HotModule dataclass**

```python
# tests/test_hot_reloader.py
import pytest
from dataclasses import dataclass
from src.hot_reloader import HotModule, HotReloader

def test_hot_module_dataclass_fields():
    hm = HotModule(
        name="test_module",
        file_path="/path/to/test_module.py",
        state_keys=["attr1", "attr2"],
        delegation_targets=["method_a", "method_b"],
    )
    assert hm.name == "test_module"
    assert hm.file_path == "/path/to/test_module.py"
    assert hm.state_keys == ["attr1", "attr2"]
    assert hm.delegation_targets == ["method_a", "method_b"]

def test_hot_reloader_register_and_get():
    HotReloader.HOT_MODULES.clear()
    hm = HotModule(name="test_mod", file_path="/fake/path.py", state_keys=[], delegation_targets=[])
    HotReloader.register(hm)
    assert "test_mod" in HotReloader.HOT_MODULES
    assert HotReloader.HOT_MODULES["test_mod"] is hm

def test_hot_reloader_register_duplicate_raises():
    HotReloader.HOT_MODULES.clear()
    hm1 = HotModule(name="dup", file_path="/a.py", state_keys=[], delegation_targets=[])
    HotReloader.register(hm1)
    with pytest.raises(ValueError, match="already registered"):
        HotReloader.register(hm1)

def test_hot_reloader_is_error_state():
    HotReloader.HOT_MODULES.clear()
    HotReloader.last_error = None
    HotReloader.is_error_state = False
    assert HotReloader.is_error_state is False
```

- [ ] **Step 2: Run test to verify it fails**

Run: `uv run pytest tests/test_hot_reloader.py -v`
Expected: FAIL — `src/hot_reloader.py` does not exist

- [ ] **Step 3: Write minimal HotModule dataclass and HotReloader skeleton**

```python
# src/hot_reloader.py
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import copy
import traceback

@dataclass
class HotModule:
    name: str
    file_path: str
    state_keys: list[str] = field(default_factory=list)
    delegation_targets: list[str] = field(default_factory=list)

class HotReloader:
    HOT_MODULES: dict[str, HotModule] = {}
    last_error: str | None = None
    is_error_state: bool = False

    @classmethod
    def register(cls, module: HotModule) -> None:
        if module.name in cls.HOT_MODULES:
            raise ValueError(f"Module {module.name} already registered")
        cls.HOT_MODULES[module.name] = module

    @classmethod
    def capture_state(cls, app: Any, state_keys: list[str]) -> dict[str, Any]:
        return {key: copy.deepcopy(getattr(app, key, None)) for key in state_keys if hasattr(app, key)}

    @classmethod
    def restore_state(cls, app: Any, state: dict[str, Any]) -> None:
        for key, value in state.items():
            setattr(app, key, value)
```

- [ ] **Step 4: Run test to verify it passes**

Run: `uv run pytest tests/test_hot_reloader.py -v`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/hot_reloader.py tests/test_hot_reloader.py
git commit -m "feat(hot-reload): Add HotModule dataclass and HotReloader registry"
```

---

## Task 2: Implement `HotReloader.reload()` and `HotReloader.reload_all()`

**Files:**
- Modify: `src/hot_reloader.py`
- Test: `tests/test_hot_reloader.py`

- [ ] **Step 1: Write failing test for reload method**

```python
# tests/test_hot_reloader.py (add these tests)
import importlib
from unittest.mock import MagicMock, patch

def test_reload_unknown_module_returns_false():
    HotReloader.HOT_MODULES.clear()
    HotReloader.register(HotModule(name="nonexistent_mod", file_path="/nonexistent.py", state_keys=[], delegation_targets=[]))
    app = MagicMock()
    result = HotReloader.reload("nonexistent_mod", app)
    assert result is False
    assert HotReloader.is_error_state is True
    assert HotReloader.last_error is not None

def test_reload_success_clears_error_state():
    HotReloader.HOT_MODULES.clear()
    # Create a minimal test module that can be reloaded
    import sys
    import types
    test_mod = types.ModuleType("src._test_reload_mod")
    sys.modules["src._test_reload_mod"] = test_mod
    HotReloader.register(HotModule(name="src._test_reload_mod", file_path=str(Path(__file__).parent.parent / "src" / "_test_reload_mod.py"), state_keys=[], delegation_targets=[]))
    app = MagicMock()
    result = HotReloader.reload("src._test_reload_mod", app)
    assert result is True
    assert HotReloader.is_error_state is False
    assert HotReloader.last_error is None
    del sys.modules["src._test_reload_mod"]

def test_reload_captures_and_restores_state_on_failure():
    HotReloader.HOT_MODULES.clear()
    HotReloader.register(HotModule(name="bad_mod", file_path="/bad.py", state_keys=["_test_attr"], delegation_targets=[]))
    app = MagicMock()
    app._test_attr = "preserved_value"
    result = HotReloader.reload("bad_mod", app)
    assert result is False
    assert HotReloader.is_error_state is True
    # State should be restored even on failure

def test_reload_all():
    HotReloader.HOT_MODULES.clear()
    # Register two modules and verify reload_all calls both
```

- [ ] **Step 2: Run test to verify it fails**

Run: `uv run pytest tests/test_hot_reloader.py -v`
Expected: FAIL — `reload` method not implemented

- [ ] **Step 3: Write `reload()` and `reload_all()` implementations**

```python
# src/hot_reloader.py (add after existing methods)
@classmethod
def reload(cls, module_name: str, app: Any) -> bool:
    if module_name not in cls.HOT_MODULES:
        cls.last_error = f"Module {module_name} not registered"
        cls.is_error_state = True
        return False

    hm = cls.HOT_MODULES[module_name]
    state = cls.capture_state(app, hm.state_keys)

    try:
        import importlib
        import sys
        if module_name in sys.modules:
            old_module = sys.modules[module_name]
            importlib.reload(old_module)
        else:
            importlib.import_module(module_name)
        cls.last_error = None
        cls.is_error_state = False
        return True
    except Exception:
        cls.restore_state(app, state)
        cls.last_error = traceback.format_exc()
        cls.is_error_state = True
        return False

@classmethod
def reload_all(cls, app: Any) -> bool:
    success = True
    for name in cls.HOT_MODULES:
        if not cls.reload(name, app):
            success = False
    return success
```

- [ ] **Step 4: Run test to verify it passes**

Run: `uv run pytest tests/test_hot_reloader.py -v`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/hot_reloader.py tests/test_hot_reloader.py
git commit -m "feat(hot-reload): Implement HotReloader.reload and reload_all"
```

---

## Task 3: Register `src.gui_2` with state_keys and delegation_targets

**Files:**
- Modify: `src/app_controller.py` — add `HotModule` registration
- Test: `tests/test_hot_reloader.py`

- [ ] **Step 1: Write test for src.gui_2 registration**

```python
def test_gui_2_module_registered():
    from src.hot_reloader import HotReloader
    assert "src.gui_2" in HotReloader.HOT_MODULES
    hm = HotReloader.HOT_MODULES["src.gui_2"]
    assert len(hm.state_keys) > 0
    assert len(hm.delegation_targets) > 0
    assert "_render_main_interface" in hm.delegation_targets
```

- [ ] **Step 2: Run test to verify it fails**

Run: `uv run pytest tests/test_hot_reloader.py::test_gui_2_module_registered -v`
Expected: FAIL — registration not yet added

- [ ] **Step 3: Add src.gui_2 HotModule registration in app_controller.py**

Find the end of `AppController.__init__` or add a `_register_hot_modules()` function called at end of `__init__`:

```python
# In src/app_controller.py, near end of AppController.__init__ or as standalone:
from src.hot_reloader import HotReloader, HotModule
from pathlib import Path

def _register_hot_modules() -> None:
    HotReloader.register(HotModule(
        name="src.gui_2",
        file_path=str(Path(__file__).parent / "gui_2.py"),
        state_keys=[
            "_active_discussion", "_disc_entries", "_disc_roles",
            "show_windows", "ui_discussion_split_h", "active_tickets",
            "show_preset_manager_window", "show_tool_preset_manager_window",
            "show_persona_editor_window", "_pending_patch",
            "_tool_log", "_comms_log",
        ],
        delegation_targets=[
            "_render_main_interface", "_render_discussion_hub",
            "_render_discussion_panel", "_render_discussion_selector",
            "_render_discussion_entries", "_render_discussion_entry",
            "_render_files_and_media", "_render_files_panel",
            "_render_screenshots_panel", "_render_context_composition_panel",
            "_render_ai_settings_hub", "_render_provider_panel",
            "_render_persona_selector_panel", "_render_agent_tools_panel",
            "_render_rag_panel", "_render_system_prompts_panel",
            "_render_preset_manager_content", "_render_persona_editor_window",
            "_render_tool_preset_manager_content",
            "_render_operations_hub", "_render_tool_calls_panel",
            "_render_comms_history_panel", "_render_mma_dashboard",
            "_render_mma_modals", "_render_ticket_queue",
            "_render_task_dag_panel", "_render_approve_script_modal",
            "_render_patch_modal",
        ],
    ))
```

Call `_register_hot_modules()` at the end of `AppController.__init__` after all state is initialized.

- [ ] **Step 4: Run test to verify it passes**

Run: `uv run pytest tests/test_hot_reloader.py::test_gui_2_module_registered -v`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/app_controller.py
git commit -m "feat(hot-reload): Register src.gui_2 with state_keys and delegation_targets"
```

---

## Task 4: Phase 1 — Refactor gui_2 render methods to module-level functions

**Files:**
- Modify: `src/gui_2.py` — extract `App._render_xxx` to `render_xxx(app)`
- Modify: `src/app_controller.py` — delegation wrappers
- Test: `tests/test_hot_reloader.py`

**Refactor Pattern** (for each render method):

```python
# BEFORE (in App class):
def _render_main_interface(self) -> None:
    if self.perf_profiling_enabled:
        self.perf_monitor.start_component("_render_main_interface")
    with imscope.window("Main Interface", self.show_windows.get("main_interface", True)) as (exp, opened):
        if not exp: return
        # ... all render logic

# AFTER — App method becomes thin delegation wrapper:
def _render_main_interface(self) -> None:
    import src.gui_2 as g2
    g2.render_main_interface(self)

# NEW module-level function:
def render_main_interface(app: App) -> None:
    if app.perf_profiling_enabled:
        app.perf_monitor.start_component("_render_main_interface")
    with imscope.window("Main Interface", app.show_windows.get("main_interface", True)) as (exp, opened):
        if not exp: return
        # ... same render logic, replacing self with app
```

**Methods to refactor** (in order):
1. `_render_main_interface` (lines 769-857)
2. `_render_discussion_hub` (lines 3235-3245)
3. `_render_discussion_panel` (lines 3395-3415)
4. `_render_discussion_selector` (lines 3430-3473)
5. `_render_discussion_entries` (lines 3247-3259)
6. `_render_discussion_entry` (lines 3261-3304)
7. `_render_files_and_media` (lines 2529-2636)
8. `_render_files_panel` (lines 2638-2716)
9. `_render_screenshots_panel` (lines 2718-2750)
10. `_render_context_composition_panel` (lines 2752-2765)
11. `_render_ai_settings_hub` (lines 1754-1759)
12. `_render_provider_panel` (lines 2377-2441)
13. `_render_persona_selector_panel` (lines 2443-2523)
14. `_render_agent_tools_panel` (lines 1850-1913)
15. `_render_rag_panel` (lines 1761-1796)
16. `_render_system_prompts_panel` (lines 1798-1848)
17. `_render_preset_manager_content` (lines 1915-2002)
18. `_render_persona_editor_window` (lines 2205-2375)
19. `_render_tool_preset_manager_content` (lines 2014-2193)
20. `_render_operations_hub` (lines 3750-3788)
21. `_render_tool_calls_panel` (lines 3790-3853)
22. `_render_comms_history_panel` (lines 3855-3971)
23. `_render_mma_dashboard` (lines 4664-4695)
24. `_render_mma_modals` (lines 4711-4785)
25. `_render_ticket_queue` (lines 4365-4478)
26. `_render_task_dag_panel` (lines 4480-4621)
27. `_render_approve_script_modal` (lines 4269-4310)
28. `_render_patch_modal` (lines 4135-4177)

- [ ] **Step 1: Write test for module-level function pattern**

```python
def test_gui2_has_module_level_render_functions():
    import src.gui_2 as g2
    # After refactor, these should exist as module-level functions
    assert hasattr(g2, 'render_main_interface')
    assert hasattr(g2, 'render_discussion_hub')
    assert hasattr(g2, 'render_discussion_panel')
    # ... etc
    # Each should accept app: App as first parameter
    import inspect
    sig = inspect.signature(g2.render_main_interface)
    assert 'app' in sig.parameters
```

- [ ] **Step 2: Run test to verify it fails**

Run: `uv run pytest tests/test_hot_reloader.py::test_gui2_has_module_level_render_functions -v`
Expected: FAIL — module-level functions don't exist yet

- [ ] **Step 3: Refactor `_render_main_interface` first as pilot**

Extract the method body, replace `self` with `app`, create module-level `render_main_interface(app: App) -> None`, and convert `App._render_main_interface` to delegation wrapper.

- [ ] **Step 4: Run test after each method refactor**

Run: `uv run pytest tests/test_hot_reloader.py::test_gui2_has_module_level_render_functions -v`
Continue until all module-level functions exist.

- [ ] **Step 5: Commit after completing all methods**

```bash
git add src/gui_2.py src/app_controller.py
git commit -m "refactor(hot-reload): Extract gui_2 render methods to module-level functions"
```

---

## Task 5: Add `Ctrl+Alt+R` trigger and GUI Hot Reload button

**Files:**
- Modify: `src/gui_2.py:App.run` — capture keyboard shortcut
- Modify: `src/gui_2.py:_render_mma_dashboard` — add Hot Reload button
- Modify: `src/gui_2.py` — add `_trigger_hot_reload` and `_hot_reload_error` state
- Test: `tests/test_hot_reload_integration.py`

- [ ] **Step 1: Write test for hot reload trigger**

```python
def test_ctrl_alt_r_triggers_hot_reload(live_gui):
    client = ApiHookClient(port=8999)
    # Verify initial state
    initial_state = client.get_value("_hot_reload_error")
    # Note: Direct keyboard simulation via hooks not yet implemented
    # Test will verify button-based trigger
```

- [ ] **Step 2: Write test for Hot Reload button in MMA dashboard**

```python
def test_hot_reload_button_in_mma_dashboard(live_gui):
    client = ApiHookClient(port=8999)
    # Verify _render_mma_dashboard has a Hot Reload button
    # via exposed component state or via behavior
```

- [ ] **Step 3: Add `_hot_reload_error` state to App.__init__**

In `src/gui_2.py:App.__init__`, add:

```python
self._hot_reload_error = False
```

- [ ] **Step 4: Add `_trigger_hot_reload()` method to App**

```python
def _trigger_hot_reload(self) -> None:
    from src.hot_reloader import HotReloader
    success = HotReloader.reload_all(self)
    self._hot_reload_error = not success

def _render_hot_reload_button(self) -> None:
    imgui.push_style_color(imgui.Col_.button, imgui.get_color_u32_rgba(0.2, 0.2, 0.2, 1.0))
    if imgui.button("Hot Reload"):
        self._trigger_hot_reload()
    imgui.pop_style_color()
    if self._hot_reload_error:
        imgui.same_line()
        imgui.text_colored(imgui.get_color_u32_rgba(1.0, 0.3, 0.2, 1.0), "Reload failed")
```

- [ ] **Step 5: Add keyboard capture in `App.run` main loop**

In `src/gui_2.py:App.run`, inside the main `while running:` loop:

```python
# Check for Ctrl+Alt+R
io = imgui.get_io()
if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.KEY_R):
    self._trigger_hot_reload()
```

- [ ] **Step 6: Integrate button into `_render_mma_global_controls` or `_render_mma_dashboard`**

Add `self._render_hot_reload_button()` call in `_render_mma_dashboard` or a nearby section.

- [ ] **Step 7: Run tests**

Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=60`
Expected: PASS

- [ ] **Step 8: Commit**

```bash
git add src/gui_2.py src/app_controller.py
git commit -m "feat(hot-reload): Add Ctrl+Alt+R trigger and GUI Hot Reload button"
```

---

## Task 6: Implement visual error tint on reload failure

**Files:**
- Modify: `src/gui_2.py` — add `_render_error_tint()` and call from `render_main_interface`
- Test: `tests/test_hot_reload_integration.py`

- [ ] **Step 1: Write test for error tint overlay**

```python
def test_error_tint_renders_on_failure(live_gui):
    # Inject a failure state and verify tint appears
    pass
```

- [ ] **Step 2: Add `_render_error_tint(app: App)` function**

```python
def _render_error_tint(app: App) -> None:
    if not getattr(app, '_hot_reload_error', False):
        return
    # NERV theme error tint
    tint_color = imgui.get_color_u32_rgba(1.0, 0.28, 0.25, 0.15)
    draw_list = imgui.get_background_draw_list()
    viewport = imgui.get_main_viewport()
    draw_list.add_rect_filled(
        viewport.pos,
        viewport.pos + viewport.size,
        tint_color,
    )
```

- [ ] **Step 3: Call `_render_error_tint(app)` at end of `render_main_interface`**

Add at the very start of `render_main_interface` since it's an overlay:

```python
def render_main_interface(app: App) -> None:
    _render_error_tint(app)
    if app.perf_profiling_enabled:
        # ...
```

- [ ] **Step 4: Run test**

Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=60`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/gui_2.py
git commit -m "feat(hot-reload): Add visual error tint on reload failure"
```

---

## Task 7: End-to-End Integration Test

**Files:**
- Create: `tests/test_hot_reload_integration.py`
- Test: `tests/test_hot_reload_integration.py`

- [ ] **Step 1: Write e2e tests using live_gui fixture**

```python
import pytest
from api_hook_client import ApiHookClient
from simulation.sim_base import BaseSimulation, run_sim

class HotReloadSimulation(BaseSimulation):
    def run(self) -> None:
        # 1. Verify hot reload button is accessible
        # 2. Trigger hot reload via API hook
        # 3. Verify GUI still renders after reload
        # 4. Verify error tint clears on success
        pass

def test_hot_reload_e2e(live_gui):
    client = ApiHookClient(port=8999)
    # Trigger hot reload
    response = client.post("/api/gui", {"action": "hot_reload"})
    # Verify GUI still responds
    state = client.get("/api/gui_state")
    assert state is not None

def test_hot_reload_preserves_state(live_gui):
    client = ApiHookClient(port=8999)
    # Set some state
    # Trigger reload
    # Verify state preserved
```

- [ ] **Step 2: Run e2e tests**

Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=120`
Expected: PASS

- [ ] **Step 3: Commit**

```bash
git add tests/test_hot_reload_integration.py
git commit -m "test(hot-reload): Add e2e integration test"
```

---

## Success Criteria Verification

1. Pressing `Ctrl+Alt+R` reloads `src.gui_2` without losing App state — tested via manual trigger + state verification
2. GUI "Hot Reload" button triggers same reload — tested via button click in live_gui
3. Failed reload shows error tint, preserves last-good state — tested via mock failure injection
4. New hot modules can be added via `HotReloader.register()` without modifying core — tested in unit tests
5. No performance impact when not reloading (< 1ms overhead per frame) — verified via perf profiling

---

## Out of Scope (Per Spec)

- Automatic file watching (manual trigger only)
- Hot-reloading of C extensions or native code
- Cross-platform reload support (Windows focus)