diff --git a/docs/superpowers/plans/2026-06-05-live-gui-state-sync.md b/docs/superpowers/plans/2026-06-05-live-gui-state-sync.md new file mode 100644 index 00000000..7841bd25 --- /dev/null +++ b/docs/superpowers/plans/2026-06-05-live-gui-state-sync.md @@ -0,0 +1,369 @@ +# Live-GUI State Sync 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:** Eliminate the App/Controller dual-state bug for the 8 confirmed sync-bug fields. Single source of truth: the Controller. App exposes Controller fields as properties. Restore `test_auto_switch_sim`, `test_workspace_profiles_restoration`, and likely `test_undo_redo_lifecycle`. + +**Architecture:** Add `@property` + `@X.setter` pairs on the `App` class for each sync-bug field. The getter reads `self.controller.X`; the setter writes `self.controller.X`. App-only fields (no Controller counterpart) remain as plain attributes. One regression test encodes the contract. + +**Tech Stack:** Python 3.11+, properties (descriptor protocol), pytest 9.0. + +--- + +## File Structure + +| File | Change | Purpose | +|---|---|---| +| `src/gui_2.py` | Modify (App class only) | Add 9 property pairs (8 sync-bug fields + `ui_ai_input`) | +| `tests/test_app_controller_state_sync.py` | Create | Regression test for the delegation contract | + +No new modules, no architectural refactor. + +--- + +## Task 1: Add the property pair for `ui_ai_input` + +**Files:** +- Modify: `src/gui_2.py` (App class, near other property definitions if any, or after `__init__`) + +- [ ] **Step 1.1: Pre-edit checkpoint** + +```powershell +cd C:\projects\manual_slop; git status --short +``` + +If `src/gui_2.py` has uncommitted changes, stop and ask the user. + +- [ ] **Step 1.2: Read the App class around `__init__` to find a good insertion point** + +Read `src/gui_2.py:130-200` to see how the App class is structured. The property should be at module/class level, ideally in a clearly delimited region. Check if there's an existing `#region: Properties` block or similar. + +- [ ] **Step 1.3: Add the `ui_ai_input` property pair** + +Find the existing `self.ui_ai_input = ...` line in `App.__init__` (search for it). After the `__init__` method ends, add: + +```python + @property + def ui_ai_input(self) -> str: + return self.controller.ui_ai_input + + @ui_ai_input.setter + def ui_ai_input(self, value: str) -> None: + self.controller.ui_ai_input = value +``` + +Use exactly 1-space indentation per project style. Use `manual-slop_py_update_definition` with the App class to add the property. + +- [ ] **Step 1.4: Verify the file still parses** + +```powershell +cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py', encoding='utf-8').read()); print('OK')" +``` + +Expected: `OK`. + +- [ ] **Step 1.5: Commit (interim checkpoint)** + +```powershell +cd C:\projects\manual_slop; git add src/gui_2.py +git -C C:\projects\manual_slop commit -m "fix(gui_2): add ui_ai_input property delegating to controller (sync fix #1 of 9)" +$h = git -C C:\projects\manual_slop log -1 --format='%H' +git -C C:\projects\manual_slop notes add -m "Add @property/@setter for ui_ai_input on the App class. Getter reads self.controller.ui_ai_input; setter writes self.controller.ui_ai_input. This is the first of 9 sync-bug property pairs (ui_ai_input + 7 panel_states + show_windows). The dual state was the root cause of test_undo_redo_lifecycle: snapshot read app.ui_ai_input but set_value wrote controller.ui_ai_input." $h +``` + +--- + +## Task 2: Add property pairs for `ui_separate_tier1` through `ui_separate_tier4` + +**Files:** +- Modify: `src/gui_2.py` (App class) + +- [ ] **Step 2.1: Add all 4 properties in a batch** + +After the `ui_ai_input` property, add: + +```python + @property + def ui_separate_tier1(self) -> bool: + return self.controller.ui_separate_tier1 + + @ui_separate_tier1.setter + def ui_separate_tier1(self, value: bool) -> None: + self.controller.ui_separate_tier1 = value + + @property + def ui_separate_tier2(self) -> bool: + return self.controller.ui_separate_tier2 + + @ui_separate_tier2.setter + def ui_separate_tier2(self, value: bool) -> None: + self.controller.ui_separate_tier2 = value + + @property + def ui_separate_tier3(self) -> bool: + return self.controller.ui_separate_tier3 + + @ui_separate_tier3.setter + def ui_separate_tier3(self, value: bool) -> None: + self.controller.ui_separate_tier3 = value + + @property + def ui_separate_tier4(self) -> bool: + return self.controller.ui_separate_tier4 + + @ui_separate_tier4.setter + def ui_separate_tier4(self, value: bool) -> None: + self.controller.ui_separate_tier4 = value +``` + +- [ ] **Step 2.2: Verify parse + commit** + +```powershell +cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py', encoding='utf-8').read()); print('OK')" +cd C:\projects\manual_slop; git add src/gui_2.py +git -C C:\projects\manual_slop commit -m "fix(gui_2): add ui_separate_tier1..4 property pairs (sync fix #2-5 of 9)" +$h = git -C C:\projects\manual_slop log -1 --format='%H' +git -C C:\projects\manual_slop notes add -m "Add 4 property pairs (ui_separate_tier1..4). These are the 4 fields that test_workspace_profiles_restoration and test_auto_switch_sim exercise. The save reads app.ui_separate_tier1, but set_value writes controller.ui_separate_tier1 -- the property bridges them." $h +``` + +--- + +## Task 3: Add property pairs for `ui_separate_task_dag` and `ui_separate_usage_analytics` + +**Files:** +- Modify: `src/gui_2.py` (App class) + +- [ ] **Step 3.1: Add both properties** + +```python + @property + def ui_separate_task_dag(self) -> bool: + return self.controller.ui_separate_task_dag + + @ui_separate_task_dag.setter + def ui_separate_task_dag(self, value: bool) -> None: + self.controller.ui_separate_task_dag = value + + @property + def ui_separate_usage_analytics(self) -> bool: + return self.controller.ui_separate_usage_analytics + + @ui_separate_usage_analytics.setter + def ui_separate_usage_analytics(self, value: bool) -> None: + self.controller.ui_separate_usage_analytics = value +``` + +- [ ] **Step 3.2: Verify + commit** + +```powershell +cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py', encoding='utf-8').read()); print('OK')" +cd C:\projects\manual_slop; git add src/gui_2.py +git -C C:\projects\manual_slop commit -m "fix(gui_2): add ui_separate_task_dag, ui_separate_usage_analytics property pairs (sync fix #6-7 of 9)" +$h = git -C C:\projects\manual_slop log -1 --format='%H' +git -C C:\projects\manual_slop notes add -m "Add 2 property pairs (ui_separate_task_dag, ui_separate_usage_analytics). These complete the 6 panel_states sync-bug fields. All ui_separate_X fields with Controller settable counterparts are now properties." $h +``` + +--- + +## Task 4: Add property pair for `show_windows` + +**Files:** +- Modify: `src/gui_2.py` (App class) + +- [ ] **Step 4.1: Add the property (dict type)** + +```python + @property + def show_windows(self) -> dict: + return self.controller.show_windows + + @show_windows.setter + def show_windows(self, value: dict) -> None: + self.controller.show_windows = value +``` + +- [ ] **Step 4.2: Verify + commit** + +```powershell +cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py', encoding='utf-8').read()); print('OK')" +cd C:\projects\manual_slop; git add src/gui_2.py +git -C C:\projects\manual_slop commit -m "fix(gui_2): add show_windows property pair (sync fix #8 of 9)" +$h = git -C C:\projects\manual_slop log -1 --format='%H' +git -C C:\projects\manual_slop notes add -m "Add show_windows property (dict). In-place mutations (app.show_windows['X'] = True) work because the property returns the same dict reference as the controller. Replacements (app.show_windows = new_dict) go through the setter." $h +``` + +--- + +## Task 5: Write the regression test + +**Files:** +- Create: `tests/test_app_controller_state_sync.py` + +- [ ] **Step 5.1: Pre-edit checkpoint** + +```powershell +cd C:\projects\manual_slop; git status --short +``` + +- [ ] **Step 5.2: Read the App's `__init__` to find the minimum setup needed for property access** + +Read `src/gui_2.py:130-180` to see App's `__init__`. We need to instantiate an App (or use `__new__` to skip `__init__`) and set up the minimum state for property access. + +- [ ] **Step 5.3: Write the test file** + +```python +import pytest +from src import app_controller, gui_2 + + +def _make_minimal_app(): + app = gui_2.App.__new__(gui_2.App) + app.controller = app_controller.AppController() + app.controller._app = app + return app + + +def test_ui_ai_input_property_delegates_to_controller(): + app = _make_minimal_app() + app.controller.ui_ai_input = "Hello" + assert app.ui_ai_input == "Hello" + app.ui_ai_input = "World" + assert app.controller.ui_ai_input == "World" + + +def test_ui_separate_tier1_property_delegates_to_controller(): + app = _make_minimal_app() + app.controller.ui_separate_tier1 = True + assert app.ui_separate_tier1 is True + app.ui_separate_tier1 = False + assert app.controller.ui_separate_tier1 is False + + +def test_ui_separate_tier2_through_tier4_properties_delegate(): + app = _make_minimal_app() + for attr in ("ui_separate_tier2", "ui_separate_tier3", "ui_separate_tier4"): + setattr(app.controller, attr, True) + assert getattr(app, attr) is True + setattr(app, attr, False) + assert getattr(app.controller, attr) is False + + +def test_ui_separate_task_dag_and_usage_analytics_properties_delegate(): + app = _make_minimal_app() + for attr in ("ui_separate_task_dag", "ui_separate_usage_analytics"): + setattr(app.controller, attr, True) + assert getattr(app, attr) is True + setattr(app, attr, False) + assert getattr(app.controller, attr) is False + + +def test_show_windows_property_delegates_to_controller(): + app = _make_minimal_app() + app.controller.show_windows = {"A": True, "B": False} + assert app.show_windows == {"A": True, "B": False} + app.show_windows = {"C": True} + assert app.controller.show_windows == {"C": True} + + +def test_show_windows_inplace_mutation_visible_to_controller(): + app = _make_minimal_app() + app.controller.show_windows = {"A": False} + app.show_windows["A"] = True + assert app.controller.show_windows["A"] is True + + +def test_app_only_panel_states_remain_plain_attributes(): + app = _make_minimal_app() + for attr in ("ui_separate_context_preview", "ui_separate_message_panel", + "ui_separate_response_panel", "ui_separate_tool_calls_panel", + "ui_separate_external_tools", "ui_discussion_split_h"): + assert not hasattr(type(app), attr), \ + f"{attr} should NOT be a property (no controller counterpart)" +``` + +Use exactly 1-space indentation. + +- [ ] **Step 5.4: Run the test** + +```powershell +cd C:\projects\manual_slop; uv run pytest tests/test_app_controller_state_sync.py -v --timeout=15 +``` + +Expected: 7 passed. + +- [ ] **Step 5.5: Commit** + +```powershell +cd C:\projects\manual_slop; git add tests/test_app_controller_state_sync.py +git -C C:\projects\manual_slop commit -m "test(app_controller): add state sync property regression tests" +$h = git -C C:\projects\manual_slop log -1 --format='%H' +git -C C:\projects\manual_slop notes add -m "7 tests for the App->Controller state delegation contract. Covers ui_ai_input, ui_separate_tier1..4, ui_separate_task_dag, ui_separate_usage_analytics, show_windows (with both replacement and in-place mutation semantics). Also asserts that App-only fields (ui_separate_context_preview, etc.) are NOT properties." $h +``` + +--- + +## Task 6: Run the originally-failing tests to verify the fix + +**Files:** (no file changes; verification only) + +- [ ] **Step 6.1: Run the 3 originally-failing tests** + +```powershell +cd C:\projects\manual_slop; uv run pytest tests/test_auto_switch_sim.py tests/test_workspace_profiles_sim.py tests/test_undo_redo_sim.py -v --timeout=60 +``` + +Expected: all pass (or at minimum: the 2 profile tests pass; undo_redo may still fail if it's a flake unrelated to sync). + +- [ ] **Step 6.2: If `test_undo_redo_sim` still fails, run it in isolation** + +```powershell +cd C:\projects\manual_slop; uv run pytest tests/test_undo_redo_sim.py::test_undo_redo_lifecycle -v --timeout=60 +``` + +If it passes in isolation, it's a flake. Document in the commit note and move on. + +- [ ] **Step 6.3: Commit verification result** + +```powershell +cd C:\projects\manual_slop; git -c core.autocrlf=false commit --allow-empty -m "verify: state sync fix unblocks test_auto_switch_sim + test_workspace_profiles_restoration" +$h = git -C C:\projects\manual_slop log -1 --format='%H' +git -C C:\projects\manual_slop notes add -m "Verified: test_auto_switch_sim and test_workspace_profiles_restoration now pass. test_undo_redo_lifecycle [passes in isolation / still fails - see other notes]. The App/Controller state sync bug is resolved via the property approach." $h +``` + +--- + +## Task 7: Update tracks.md and conductor/index.md + +**Files:** +- Modify: `conductor/tracks.md` (mark v2 sub-track complete or partial) +- Modify: `conductor/index.md` (move v2 sub-track to recently-shipped or note next steps) + +- [ ] **Step 7.1: Update tracks.md** + +Find the live_gui_test_hardening_v2 entry and add a sub-task completion note. Or move to a dedicated entry. + +- [ ] **Step 7.2: Update index.md** + +- [ ] **Step 7.3: Commit** + +```powershell +cd C:\projects\manual_slop; git add conductor/tracks.md conductor/index.md +git -C C:\projects\manual_slop commit -m "conductor: live_gui_state_sync sub-track complete" +``` + +--- + +## Self-Review + +- **Spec coverage:** All 8 sync-bug fields + `ui_ai_input` (9 total) have property pairs (Tasks 1-4). The regression test (Task 5) covers the delegation contract. Verification (Task 6) runs the originally-failing tests. +- **Placeholders:** None. +- **Type consistency:** `bool` for `ui_separate_*`, `str` for `ui_ai_input`, `dict` for `show_windows` — matches the existing Controller type hints. +- **Risk:** Mid — 9 property pairs added to a 5532-line class. Per-field atomic commits with regression tests mitigate. + +--- + +## Execution Handoff + +This plan is sized for **inline execution** (single agent, no subagents, per the user's stated preference). Execute Tasks 1-7 in order; each task ends with an atomic commit + git note. + +After all tasks, the user runs `uv run python scripts/run_tests_batched.py` to confirm 100% pass on the 273-file suite. diff --git a/docs/superpowers/specs/2026-06-05-live-gui-state-sync-design.md b/docs/superpowers/specs/2026-06-05-live-gui-state-sync-design.md new file mode 100644 index 00000000..d2f22ba7 --- /dev/null +++ b/docs/superpowers/specs/2026-06-05-live-gui-state-sync-design.md @@ -0,0 +1,172 @@ +# Live-GUI State Sync — Design + +**Date:** 2026-06-05 +**Status:** Draft +**Track:** live_gui_state_sync_20260605 (sub-project of v2) + +## Problem Statement + +`App` (`src/gui_2.py`) and `AppController` (`src/app_controller.py`) maintain **parallel state** for the same logical fields. `set_value` writes to the **Controller**, but several code paths read from the **App**, returning stale or wrong values. + +### Concrete failures (from 2026-06-05 batched test run, batches 7, 46, 65, 68) + +1. **`test_auto_switch_sim::test_auto_switch_sim`** — sets `ui_separate_tier1=True` and `show_windows['Diagnostics']=True`, saves `Tier3Profile`, sets to False, triggers tier-3 auto-switch. Expects `show_windows['Diagnostics']=True` restored. **Fails: profile captures from App but is set on Controller.** + +2. **`test_workspace_profiles_restoration::test_workspace_profiles_restoration`** — sets `ui_separate_tier1=True`, saves `test_restore`, sets to False, loads. Expects True. **Fails: same root cause.** + +3. **`test_undo_redo_lifecycle::test_undo_redo_lifecycle`** (NEW regression) — sets `ai_input="Initial Input"`, modifies to `"Modified Input"`, clicks `btn_undo`. Expects `ai_input="Initial Input"`. **Fails: snapshot reads `app.ui_ai_input` but `set_value` writes to `controller.ui_ai_input`.** + +### Audit of duplicated fields + +Static analysis of the 71 settable fields in `AppController._settable_fields` vs the 12 `panel_states` keys captured in `App._capture_workspace_profile`, plus the `show_windows` dict and snapshot fields: + +| Field | In `_settable_fields` (Controller)? | Read by App code? | Sync bug? | +|---|---|---|---| +| `show_windows` | yes | `_capture_workspace_profile` (line 627), `_apply_workspace_profile` (line 633) | **YES** | +| `ui_separate_task_dag` | yes | `_capture_workspace_profile` (line 615) | **YES** | +| `ui_separate_usage_analytics` | yes | `_capture_workspace_profile` (line 616) | **YES** | +| `ui_separate_tier1` | yes | `_capture_workspace_profile` (line 617) | **YES** | +| `ui_separate_tier2` | yes | `_capture_workspace_profile` (line 618) | **YES** | +| `ui_separate_tier3` | yes | `_capture_workspace_profile` (line 619) | **YES** | +| `ui_separate_tier4` | yes | `_capture_workspace_profile` (line 620) | **YES** | +| `ui_ai_input` | yes (`ai_input -> ui_ai_input`) | `_take_snapshot` (line 551), `_apply_snapshot` (line 569) | **YES** | +| `ui_separate_context_preview` | no (NOT in settable_fields) | `_capture_workspace_profile` (line 611) | no — App-only | +| `ui_separate_message_panel` | no | `_capture_workspace_profile` (line 612) | no — App-only | +| `ui_separate_response_panel` | no | `_capture_workspace_profile` (line 613) | no — App-only | +| `ui_separate_tool_calls_panel` | no | `_capture_workspace_profile` (line 614) | no — App-only | +| `ui_separate_external_tools` | no | `_capture_workspace_profile` (line 621) | no — App-only | +| `ui_discussion_split_h` | no | `_capture_workspace_profile` (line 622) | no — App-only | + +**8 confirmed sync bugs.** Plus `ui_ai_input` (snapshot) is a 9th. + +## Root Cause + +`App.__init__` creates a separate `AppController` instance and later sets `self.controller._app = self` (bidirectional link). The two objects each declare their own `self.ui_separate_tier1 = False` (App) and `self.ui_separate_tier1 = False` (Controller) in their respective `__init__`s. They are independent Python attributes. + +`set_value` (`src/api_hooks.py`, line 614) calls `setattr(controller, attr_name, value)` — writes to Controller. But `_capture_workspace_profile` reads `self.ui_separate_tier1` where `self` is the App — never updated. + +## Design + +### Goal + +Eliminate the dual state. **Single source of truth: the Controller.** The App becomes a thin "view" layer that exposes Controller fields as Python properties. `set_value` continues to write to the Controller. All reads (from save, snapshot, render) transparently read from the Controller. + +### Approach: Properties on App that delegate to Controller + +Add `@property` definitions on the `App` class for each field that has a Controller counterpart. The getter returns `self.controller.X`. The setter (where App code writes, e.g. snapshot restore) also delegates to `self.controller.X`. + +**Hypothetical example for `ui_separate_tier1`:** + +```python +# In App class (src/gui_2.py) + +@property +def ui_separate_tier1(self) -> bool: + return self.controller.ui_separate_tier1 + +@ui_separate_tier1.setter +def ui_separate_tier1(self, value: bool) -> None: + self.controller.ui_separate_tier1 = value +``` + +This makes `app.ui_separate_tier1` and `controller.ui_separate_tier1` the same value, regardless of which path writes. The only writes are via the property setter (or `set_value` via the Controller directly), and all reads go through the getter. + +### Why this approach + +- **Minimal blast radius**: The App class only adds properties; no method bodies change. Methods that read `self.X` continue to work — they just get the Controller's value via the property. +- **Bidirectional**: Setter support is critical for `_apply_snapshot` and `_apply_workspace_profile` which set App fields directly (`self.ui_ai_input = snapshot.ai_input`). They go through the property setter, which writes to the Controller. +- **No double-write footgun**: A "sync on set_value" alternative requires remembering to write to BOTH objects. A property approach is a single point of truth. +- **Easy to migrate incrementally**: Each field is one property pair. Can be added one at a time with a regression test for each. + +### Alternatives considered + +- **A2: Merge App and Controller into one class.** Rejected: would be a 5532-line → 4000-line merge with high risk. The Controller already lives in a separate file; the App delegates to it via `self.controller.X`. Merging would lose the existing boundary. +- **A3: Sync on every set_value (write to both).** Rejected: requires touching every writer; easy to miss a site. Property approach is one place per field. +- **A4: Pass Controller as a method argument everywhere.** Rejected: invasive; requires changing method signatures throughout `gui_2.py` and `app_controller.py`. + +## File Changes + +### Modify: `src/gui_2.py` (App class) + +Add `@property` + `@X.setter` for each of the 8 sync-bug fields, plus `ui_ai_input`: + +```python +@property +def ui_separate_tier1(self) -> bool: + return self.controller.ui_separate_tier1 + +@ui_separate_tier1.setter +def ui_separate_tier1(self, value: bool) -> None: + self.controller.ui_separate_tier1 = value +``` + +Fields to add properties for: +- `ui_ai_input` (snapshot bug) +- `ui_separate_task_dag` +- `ui_separate_usage_analytics` +- `ui_separate_tier1` through `ui_separate_tier4` +- `show_windows` (special: dict, not bool) + +For `show_windows`, the property needs care — `set_value` may pass a new dict; the property should do `self.controller.show_windows = value` to allow full replacement, but for in-place updates (`self.show_windows["X"] = True`), the property getter returns the Controller's dict reference (so in-place mutations work) and the property setter can either replace or do nothing (since the dict is shared). + +```python +@property +def show_windows(self) -> Dict[str, bool]: + return self.controller.show_windows + +@show_windows.setter +def show_windows(self, value: Dict[str, bool]) -> None: + self.controller.show_windows = value +``` + +**Do NOT** add properties for fields that are App-only (no Controller counterpart): `ui_separate_context_preview`, `ui_separate_message_panel`, `ui_separate_response_panel`, `ui_separate_tool_calls_panel`, `ui_separate_external_tools`, `ui_discussion_split_h`, etc. — they remain as plain App attributes. + +### Add: `tests/test_app_controller_state_sync.py` (new) + +A new unit test that encodes the contract: **for every field in `_settable_fields` that is also referenced as `self.X` in the App class's `_capture_workspace_profile` and `_take_snapshot`/`_apply_snapshot`, writes to `app.X` and `controller.X` must be observed by both.** + +```python +def test_ui_separate_tier1_setter_delegates_to_controller(): + """The App's ui_separate_tier1 property is a delegate to the Controller. + Writes through app.ui_separate_tier1 = X are visible at controller.ui_separate_tier1, + and writes through set_value (which goes to controller) are visible at app.ui_separate_tier1.""" + from src import app_controller, gui_2 + from src.app_controller import AppController + # Don't fully init App (too heavy); use lightweight setup + app = gui_2.App.__new__(gui_2.App) + app.controller = AppController() + app._app = app # back-ref + # set_value goes to controller + app.controller.ui_separate_tier1 = True + assert app.ui_separate_tier1 is True # reads through property + # direct set through app's property + app.ui_separate_tier1 = False + assert app.controller.ui_separate_tier1 is False # write visible at controller +``` + +This is a regression test for the contract. + +### Test impact + +After the fix, these tests should pass: +- `test_auto_switch_sim::test_auto_switch_sim` (writes to `app.show_windows` and `app.ui_separate_tier1` are observed by save) +- `test_workspace_profiles_sim::test_workspace_profiles_restoration` (same) +- `test_undo_redo_lifecycle::test_undo_redo_lifecycle` (snapshot reads from `app.ui_ai_input` get the Controller's value) + +If `test_undo_redo_lifecycle` is **also** a flake or a regression from the user's recent cleanup commit `873edf42`, the property fix may not be sufficient. In that case, the test will continue to fail and need its own investigation track. + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| Existing App code does `del app.ui_X` to reset state | Low | Low | property setter can be a no-op for `del` (raises AttributeError); review call sites | +| App class is 5532 lines — risk of regression | High | Medium | Per-field property addition; one regression test per field; ship in a single atomic commit | +| User's recent cleanup commit `873edf42` may have added or removed attribute references | Medium | Low | Run targeted regression test after each property addition | +| New properties shadow existing class attributes | Low | High | Use `dir(app)` to verify no shadow before commit | + +## Out of Scope + +- **prior_session test mock setup** — separate track (`prior_session_test_harden_20260605`). +- **wait-for-ready test pattern** — separate track (`wait_for_ready_test_pattern_20260605`). +- **Other App/Controller sync bugs not in the 8 listed** — audit will continue; if more found, queue as v3 sub-track. +- **Refactoring App and Controller into one class** — deferred; property approach is sufficient for now.