Files
manual_slop/conductor/tracks/default_layout_extract_20260629/plan.md
T
ed 410d81fb3f fix(track): correct line numbers in default_layout_extract spec/plan for master (not cruft branch)
The spec was drafted while the working tree was on tier2/post_module_taxonomy_de_cruft_20260627, but the track targets master. 2 line numbers were from the cruft branch, not master:
- src/commands.py reset_layout: spec said :342-378 + :371; master is :248-275 + :268
- src/command_palette.py: spec said 208 lines; master is 165 lines

Also added a Branch State Warning section documenting:
- main working tree is on tier2/post_module_taxonomy_de_cruft_20260627 (NOT master)
- module_taxonomy_refactor_20260627 + post_module_taxonomy_de_cruft_20260627 are NOT merged to master
- this track does NOT depend on those cruft tracks
- master worktree at C:\projects\manual_slop_master is the editing surface

All other line numbers (App._post_init:566, App.run:619, _run_immapp_result:691, _post_init_callback_result:1449, render_persona_editor_window:3433, orphan end_child:6990, paths.py themes:60/83/150/209-216/295) verified correct against master.
2026-06-29 22:18:25 -04:00

42 KiB

Track Plan: Default Layout Extract + Hard Visual Verification

For Tier-3 workers: Steps use checkbox (- [ ]) syntax. Use exactly 1-space indentation for all Python. Preserve CRLF line endings. No comments in source code. Atomic commits per task. No dict[str, Any], no Optional[T] returns (use Result[T] + NIL_T). Read src/gui_2.py:1481-1540 (tier-2 version) for the install helper pattern reference; read src/theme_models.py:181-225 for the layouts loader pattern reference; read src/paths.py:60-83,150,209-216,295 for the themes → layouts mirror.

Goal: Extract tier-2's GOOD default-layout work into master AND build a hard 4-layer visual verification infrastructure that catches "panels don't render" regressions every time.

Architecture: Hybrid extraction (C per spec §FR1): port layouts/default.ini + src/layouts.py + tests/test_layout_reorganization.py fresh (clean history for new modules); cherry-pick c2155593 (orphan end_child) + 3b966288 (reset_layout cleanup); add new _install_default_layout_* helpers + App._post_init + App.run wiring. Build 4 verification layers: per-panel render sentinel (Layer 1), Win32 PrintWindow pixel baseline (Layer 2), forced test viewport+theme env vars (Layer 3), cannot-skip gates (Layer 4: standalone CLI + CI integration + tag requirement + tracks.md schema).

Tech Stack: Python 3.11+, imgui-bundle (HelloImGui), pywin32 (PrintWindow), Pillow (PNG), numpy (pixel diff), pytest + live_gui fixture. Adds scripts/check_visual_baseline.py (new audit-style script).


Phase 1: Asset Foundation (layouts/ + src/layouts.py + get_layouts_dir)

Focus: Port the foundational assets from tier-2 to master with clean history.

  • Task 1.1: RED test for src/layouts.py:load_layouts_from_dir

    • WHERE: New file tests/test_layouts.py
    • WHAT: Write 3 tests:
      1. test_load_layouts_from_dir_empty — pass a non-existent path → returns {}
      2. test_load_layouts_from_dir_single_file — create tmp dir with one .ini file → returns 1-entry dict keyed by stem
      3. test_load_layouts_from_dir_skips_non_ini — tmp dir with .ini + .txt → returns only the .ini
    • HOW: Use tmp_path fixture (already redirected under tests/artifacts/_pytest_tmp per pyproject.toml:addopts). Import from src.layouts import load_layouts_from_dir.
    • SAFETY: Use tmp_path, not hardcoded paths. 1-space indentation. Type hints required.
    • RUN: uv run pytest tests/test_layouts.py -v — Expected: ModuleNotFoundError: No module named 'src.layouts'.
    • COMMIT: test(layouts): RED phase tests for load_layouts_from_dir
  • Task 1.2: Create src/layouts.py

    • WHERE: New file src/layouts.py (87 lines, ported fresh from tier-2's C:\projects\manual_slop_tier2\src\layouts.py)
    • WHAT: Define LayoutFile dataclass + load_layouts_from_file() + load_layouts_from_dir() + load_layouts_from_disk() + _LAYOUTS_CACHE: dict[str, LayoutFile]
    • HOW: Read tier-2 file; copy verbatim EXCEPT: strip the "TODO(Ed)" comment (NFR3); keep the Result + ErrorInfo drain pattern from tier-2 verbatim; keep _LAYOUTS_CACHE module-level
    • SAFETY: 1-space indentation. CRLF. @dataclass(frozen=True, slots=True). Type hints on all params + returns.
    • RUN: uv run pytest tests/test_layouts.py -v — Expected: 3 PASS.
    • COMMIT: feat(layouts): introduce src/layouts.py + LayoutFile dataclass
  • Task 1.3: RED test for src/paths.py:get_global_layouts_path

    • WHERE: New file tests/test_paths_layouts.py
    • WHAT: Write 4 tests:
      1. test_get_global_layouts_path_defaultinitialize_paths() called, get_global_layouts_path() returns <root_dir>/layouts
      2. test_get_global_layouts_path_env_overrideSLOP_GLOBAL_LAYOUTS env var set → returns that path
      3. test_layouts_in_path_info_dictpaths.path_info() dict has 'layouts': info(...) entry
      4. test_layouts_field_in_app_paths_AppPaths().layouts is a Path
    • HOW: Import from src.paths import get_global_layouts_path, initialize_paths, _cfg. Use monkeypatch.setenv("SLOP_GLOBAL_LAYOUTS", str(tmp_path / "custom")).
    • SAFETY: Call initialize_paths() once per test (use fixture). 1-space indentation.
    • RUN: uv run pytest tests/test_paths_layouts.py -v — Expected: AttributeError: module 'src.paths' has no attribute 'get_global_layouts_path'.
    • COMMIT: test(paths): RED phase tests for get_global_layouts_path + SLOP_GLOBAL_LAYOUTS
  • Task 1.4: Add get_global_layouts_path() to src/paths.py

    • WHERE: src/paths.py — 4 sites: line 60 _AppPaths dataclass (add layouts: Path), line 83 _PATHS_DEFAULTS (add layouts = root_dir / "layouts"), line 150 initialize_paths._resolve_path chain (add SLOP_GLOBAL_LAYOUTS env override), line 295 path_info() (add 'layouts': info(cfg.layouts)), line 209-216 (add get_global_layouts_path() mirror of get_global_themes_path())
    • WHAT: Mirror the themes pattern exactly. New code follows the existing 1-space indentation + CRLF.
    • HOW: Read src/paths.py:60 → insert layouts: Path after themes: Path. Read src/paths.py:83 → insert themes = root_dir / "layouts" after themes = root_dir / "themes". Read src/paths.py:150 → add themes = _resolve_path("SLOP_GLOBAL_LAYOUTS", "layouts", root_dir / "layouts", config_path) to the resolver chain. Read src/paths.py:209-216 → copy get_global_themes_path() verbatim and rename. Read src/paths.py:295 → insert 'layouts': info(cfg.layouts) after 'themes': info(cfg.themes).
    • SAFETY: Match existing 1-space indent. CRLF. No comments in source. Update _resolve_path keyword args to match the same shape as the themes line.
    • RUN: uv run pytest tests/test_paths_layouts.py -v — Expected: 4 PASS.
    • COMMIT: feat(paths): add get_global_layouts_path() + SLOP_GLOBAL_LAYOUTS env override (mirror of themes)
  • Task 1.5: RED test for bundled INI file

    • WHERE: New file tests/test_layouts_bundled.py
    • WHAT: Write 4 tests:
      1. test_layouts_default_ini_existsPath("layouts/default.ini").exists() is True
      2. test_layouts_default_ini_size — file size > 1000 bytes
      3. test_layouts_default_ini_has_docking — content contains [Docking][Data]
      4. test_layouts_default_ini_has_8_windows — content has 8 [Window][X] entries
    • HOW: Use Path.cwd() / "layouts" / "default.ini". Use len(re.findall(r"^\[Window\]\[", content)) for window count.
    • SAFETY: 1-space indent. CRLF. Read with encoding="utf-8".
    • RUN: uv run pytest tests/test_layouts_bundled.py -v — Expected: FileNotFoundError: layouts/default.ini.
    • COMMIT: test(layouts): RED phase tests for bundled default.ini structure
  • Task 1.6: Port layouts/default.ini to master

    • WHERE: New file layouts/default.ini at repo root
    • WHAT: Copy verbatim from tier-2's C:\projects\manual_slop_tier2\layouts\default.ini (2971 bytes, 101 lines). Strip the ;;; documentation comments (NFR3: comments live in docs). Strip the ;;;<<<SplitIds>>>;;; block at line 100-101 (HelloImGui adds that on save; not needed in the bundle).
    • HOW: Read tier-2 file → write fresh to layouts/default.ini. Keep all [Window][X] entries (8 of them), [Docking][Data] block with DockSpace ID=0xAFC85805, [Layout], [StatusBar], [Theme] sections.
    • SAFETY: CRLF. No ;;; lines. Final file should be ~30-40 lines.
    • RUN: uv run pytest tests/test_layouts_bundled.py -v — Expected: 4 PASS.
    • COMMIT: feat(layouts): bundle layouts/default.ini with 8 [Window] entries + [Docking] hierarchy

Phase 2: Install Helpers (RED-GREEN for the 3 helpers)

Focus: Add _install_default_layout_if_empty, _install_default_layout_if_empty_result, _install_default_layout_pre_run_result to src/gui_2.py.

  • Task 2.1: RED test for _install_default_layout_if_empty (empty dst)

    • WHERE: New file tests/test_install_default_layout.py
    • WHAT: Write 5 tests:
      1. test_install_empty_dst — dst INI is empty/missing → src content copied to dst + Result(data=True)
      2. test_install_skips_non_empty_dst — dst INI has 5+ [Window][ entries → no overwrite + Result(data=False)
      3. test_install_handles_missing_src — src INI doesn't exist → Result(data=False, errors=[ErrorInfo])
      4. test_install_handles_oserror_on_read — patch Path.read_text to raise OSError → Result(data=False, errors=[ErrorInfo])
      5. test_install_calls_load_ini_settings_from_memory — assert imgui.load_ini_settings_from_memory was called once
    • HOW: Use tmp_path. Import from src.gui_2 import _install_default_layout_if_empty. Use monkeypatch.setattr(imgui, "load_ini_settings_from_memory", lambda x: None) for test 5.
    • SAFETY: 1-space indent. CRLF. Mock only the boundary (imgui.load_ini_settings_from_memory is the SDK boundary).
    • RUN: uv run pytest tests/test_install_default_layout.py -v — Expected: ImportError: cannot import name '_install_default_layout_if_empty'.
    • COMMIT: test(install): RED phase tests for _install_default_layout_if_empty
  • Task 2.2: Implement _install_default_layout_if_empty in src/gui_2.py

    • WHERE: src/gui_2.py — insert at line 1481 (before _post_init_callback_result which is at 1449 — actually place the new helpers AFTER _post_init_callback_result)
    • WHAT: Port tier-2's src/gui_2.py:1481-1530 verbatim. Adjust imports if needed (Result, ErrorInfo, ErrorKind already imported via src.result_types).
    • HOW: Read tier-2's lines 1481-1530 → copy to master. Strip docstring multi-line commentary to 1-2 lines (NFR3). The function returns Result[bool].
    • SAFETY: 1-space indent. CRLF. No comments. Match existing _post_init_callback_result shape.
    • RUN: uv run pytest tests/test_install_default_layout.py -v — Expected: 5 PASS.
    • COMMIT: feat(gui): add _install_default_layout_if_empty + _install_default_layout_if_empty_result helpers
  • Task 2.3: RED test for _install_default_layout_pre_run_result (disk-only)

    • WHERE: Append to tests/test_install_default_layout.py
    • WHAT: Write 3 tests:
      1. test_pre_run_install_empty_dst — same as 2.1.1 but using _install_default_layout_pre_run_result and mocking _require_warmed("src.layouts")
      2. test_pre_run_install_does_not_call_load_ini_settings_from_memory — assert imgui.load_ini_settings_from_memory was NOT called (imgui not initialized yet)
      3. test_pre_run_install_skips_non_empty_dst — same as 2.1.2
    • HOW: Same tmp_path pattern. Mock src.layouts.get_layouts_dir to return tmp_path / "layouts".
    • SAFETY: 1-space indent. CRLF. Verify load_ini_settings_from_memory was NOT called (it's the key behavioral difference vs _install_default_layout_if_empty).
    • RUN: uv run pytest tests/test_install_default_layout.py -v — Expected: 3 new FAIL (ImportError: cannot import name '_install_default_layout_pre_run_result').
    • COMMIT: test(install): RED phase tests for _install_default_layout_pre_run_result
  • Task 2.4: Implement _install_default_layout_pre_run_result in src/gui_2.py

    • WHERE: src/gui_2.py — insert immediately after _install_default_layout_if_empty_result (which Task 2.2 placed)
    • WHAT: Port tier-2's src/gui_2.py:1543-1590 verbatim. The function reads get_layouts_dir() / "default.ini" and writes to Path.cwd() / "manualslop_layout.ini". NO imgui.load_ini_settings_from_memory call.
    • HOW: Read tier-2's lines 1543-1590 → copy to master. Adjust imports.
    • SAFETY: 1-space indent. CRLF. No comments. The disk-only behavior is the key contract; the function does NOT import or call imgui.
    • RUN: uv run pytest tests/test_install_default_layout.py -v — Expected: 8 PASS (5 from 2.1 + 3 new).
    • COMMIT: feat(gui): add _install_default_layout_pre_run_result (disk-only, no live-session apply)

Phase 3: Wiring (App._post_init + App.run)

Focus: Wire the install helpers into the app's startup flow.

  • Task 3.1: RED test for App._post_init calling _install_default_layout_if_empty_result

    • WHERE: New file tests/test_app_wiring_install.py
    • WHAT: Write 3 tests:
      1. test_post_init_calls_install_helper — instantiate App, call _post_init(), assert _install_default_layout_if_empty_result was called with src=layouts/default.ini, dst=cwd/manualslop_layout.ini
      2. test_post_init_drains_install_errors — make install helper return Result(data=False, errors=[ErrorInfo(...)]), assert _startup_timeline_errors has the entry
      3. test_post_init_skips_when_dst_non_empty — pre-create cwd/manualslop_layout.ini with 5+ [Window][, call _post_init(), assert install helper was NOT called (or was called but returned data=False)
    • HOW: Use monkeypatch.setattr(src.gui_2, "_install_default_layout_if_empty_result", lambda app, src, dst: Result(data=True)). Use tmp_path as cwd.
    • SAFETY: 1-space indent. CRLF. Mock only the boundary helper; verify the call site.
    • RUN: uv run pytest tests/test_app_wiring_install.py -v — Expected: 3 FAIL (call site not yet wired).
    • COMMIT: test(gui): RED phase tests for _post_init install wiring
  • Task 3.2: Wire _install_default_layout_if_empty_result into App._post_init

    • WHERE: src/gui_2.py:566-578_post_init method. Insert the install call after line 574 (cb_result = _post_init_callback_result(self)) and before line 578 (self._diag_layout_state()).
    • WHAT: Add 7 lines:
      from src.layouts import get_layouts_dir
      src_layout_path: Path = get_layouts_dir() / "default.ini"
      dst_layout_path: Path = Path.cwd() / "manualslop_layout.ini"
      install_result: Result[bool] = _install_default_layout_if_empty_result(self, src_layout_path, dst_layout_path)
      if not install_result.ok:
          if not hasattr(self, '_startup_timeline_errors'): self._startup_timeline_errors = []
          self._startup_timeline_errors.append(("_install_default_layout", install_result.errors[0]))
      
    • HOW: Insert after _post_init_callback_result block. Match existing 1-space indent in _post_init.
    • SAFETY: 1-space indent. CRLF. The _startup_timeline_errors attribute may not exist yet (per existing _post_init lines 576, 599 — create it lazily).
    • RUN: uv run pytest tests/test_app_wiring_install.py -v — Expected: 3 PASS.
    • COMMIT: feat(gui): wire _install_default_layout_if_empty_result into App._post_init
  • Task 3.3: RED test for App.run calling _install_default_layout_pre_run_result

    • WHERE: Append to tests/test_app_wiring_install.py
    • WHAT: Write 2 tests:
      1. test_run_calls_pre_run_install_before_immapp — mock both _install_default_layout_pre_run_result and _run_immapp_result, assert order: pre-run install called BEFORE immapp
      2. test_run_drains_pre_run_install_errors — pre-run install returns Result(data=False, errors=[ErrorInfo]), assert _startup_timeline_errors has the entry
    • HOW: Use mock.call_args_list to verify order. Use monkeypatch.setattr(src.gui_2, "_install_default_layout_pre_run_result", ...).
    • SAFETY: 1-space indent. CRLF. Mock the pre-run install + immapp helpers; don't actually run immapp.
    • RUN: uv run pytest tests/test_app_wiring_install.py -v — Expected: 2 new FAIL (pre-run call site not wired).
    • COMMIT: test(gui): RED phase tests for App.run pre-run install wiring
  • Task 3.4: Wire _install_default_layout_pre_run_result into App.run

    • WHERE: src/gui_2.py:691 — before _run_immapp_result(self) call. Insert 6 lines.
    • WHAT: Add:
      pre_install_result: Result[bool] = _install_default_layout_pre_run_result(self)
      if not pre_install_result.ok:
          err = pre_install_result.errors[0]
          if hasattr(self, "_startup_timeline_errors"):
              self._startup_timeline_errors.append(("_install_default_layout_pre_run", err))
      
    • HOW: Insert immediately before run_result = _run_immapp_result(self) at line 691. Match existing 1-space indent.
    • SAFETY: 1-space indent. CRLF. The pre-run install MUST fire before immapp reads the INI from disk.
    • RUN: uv run pytest tests/test_app_wiring_install.py -v — Expected: 5 PASS (3 from 3.1 + 2 from 3.3).
    • COMMIT: feat(gui): wire _install_default_layout_pre_run_result into App.run (before immapp)
  • Task 3.5: Verify install fires + INI created

    • WHERE: Existing test file tests/test_install_default_layout.py
    • WHAT: Add integration test test_install_fires_end_to_end — instantiate App, call _post_init(), assert cwd/manualslop_layout.ini exists with > 1000 bytes + [Window][ substring.
    • HOW: Use tmp_path as cwd via monkeypatch.chdir(tmp_path).
    • SAFETY: 1-space indent. CRLF. Real on-disk assertion (no mocks).
    • RUN: uv run pytest tests/test_install_default_layout.py -v — Expected: 9 PASS.
    • COMMIT: test(install): GREEN end-to-end install fires + INI created

Phase 4: Surgical Cherry-Picks

Focus: Apply the 2 surgical fixes that don't require new infrastructure.

  • Task 4.1: Cherry-pick orphan-end-child fix

    • WHERE: src/gui_2.py:6990 — delete the line imgui.end_child() inside the except (TypeError, AttributeError): block in render_tier_stream_panel.
    • WHAT: Apply tier-2's c2155593 1-line deletion. The orphan end_child() at line 6990 fires with no matching begin_child() when the try block raises (e.g. len(None)).
    • HOW: Read src/gui_2.py:6984-6991 → delete line 6990 (the imgui.end_child() inside except). Keep line 6988 (the correct one inside try). Keep pass on line 6991.
    • SAFETY: 1-space indent. CRLF. Preserve the try/except structure. The deleted line is the only change.
    • RUN: uv run python scripts/check_imgui_scopes.py src/gui_2.py — Expected: 3 "extra end" warnings (down from 4). The 4925 + 7094 + 8810 warnings remain (other code); the 6990 one should be gone.
    • COMMIT: fix(gui): remove orphan imgui.end_child() in render_tier_stream_panel except handler
  • Task 4.2: Cherry-pick reset_layout dead-path cleanup

    • WHERE: src/commands.py:268 — delete the line os.path.join("tests", "artifacts", "live_gui_workspace", "manualslop_layout.ini"), from the layout_paths list inside reset_layout.
    • WHAT: Apply tier-2's 3b966288. The reset_layout command should not reference test fixtures in production code.
    • HOW: Read src/commands.py:365-380 → identify the line that hardcodes tests/artifacts/manualslop_layout_default.ini → delete it. If the surrounding logic needs adjustment (e.g. fallback to a different path), update the fallback.
    • SAFETY: 1-space indent. CRLF. The behavior of reset_layout should be preserved — it still resets the layout, just from a different source path.
    • RUN: uv run pytest tests/test_commands.py -v — Expected: PASS (the existing tests cover the reset_layout behavior).
    • COMMIT: chore(commands): remove dead test-fixture path from reset_layout

Phase 5: Layer 1 Verification — Per-Panel Render Sentinel

Focus: The "panels actually render" test that catches the original bug.

  • Task 5.1: RED test for per-panel render size check

    • WHERE: New file tests/test_panels_visible_after_install.py
    • WHAT: Write 3 tests:
      1. test_panels_visible_after_install — use live_gui fixture, wait for first frame, iterate app.show_windows for entries where value == True, assert each has nonzero render size via imgui.find_window_viewport(name).size.x > 0
      2. test_panel_invisible_when_show_windows_false — same loop, but verify panels with value == False are NOT in find_window_viewport results
      3. test_panel_render_size_is_correct_window — assert find_window_viewport("AI Settings").size.x > 100 AND .size.y > 50 (sanity: visible panels have meaningful size, not 0)
    • HOW: Use live_gui fixture. Poll for first frame via client.wait_for_event (not time.sleep). Use imgui.find_window_viewport(name) API.
    • SAFETY: Poll-loop, not time.sleep. 1-space indent. CRLF. Skip test on non-Windows (@pytest.mark.skipif(sys.platform != "win32")).
    • RUN: uv run pytest tests/test_panels_visible_after_install.py -v — Expected: PASS on first try IF install infrastructure works (since Phase 1-3 is done by now). The value of this test is regression detection, not initial GREEN.
    • COMMIT: test(visual): Layer 1 per-panel render sentinel (catches empty-panels regression)
  • Task 5.2: Verify sentinel catches the regression (negative test mode)

    • WHERE: Append to tests/test_panels_visible_after_install.py
    • WHAT: Write test_sentinel_catches_empty_panels — use live_gui fixture, BUT monkey-patch _install_default_layout_pre_run_result to return Result(data=False) (skip install). Also, pre-create cwd/manualslop_layout.ini with content that omits all [Window][X] entries (just an empty INI). Assert the test FAILS.
    • HOW: Use monkeypatch.setattr. The sentinel should detect that 8 default-visible panels all have zero render size.
    • SAFETY: This test verifies the sentinel's REGRESSION CATCH ability. It should NOT pass — its job is to confirm the sentinel works.
    • RUN: uv run pytest tests/test_panels_visible_after_install.py::test_sentinel_catches_empty_panels -v — Expected: FAIL with assertion error listing 8 panels with zero render size.
    • COMMIT: test(visual): RED negative test — sentinel catches empty-panels regression
  • Task 5.3: Verify sentinel catches the original bug (mock the import failure)

    • WHERE: Append to tests/test_panels_visible_after_install.py
    • WHAT: Write test_sentinel_catches_render_main_interface_no_op — use live_gui fixture, monkey-patch src.gui_2.render_main_interface to be a no-op (lambda app: None). Assert the sentinel FAILS (panels don't render).
    • HOW: This simulates the original tier-2 bug: render_main_interface is a no-op due to ModuleNotFoundError.
    • SAFETY: Use monkeypatch.setattr to swap the function reference at module level.
    • RUN: uv run pytest tests/test_panels_visible_after_install.py::test_sentinel_catches_render_main_interface_no_op -v — Expected: FAIL with assertion error listing 8 panels with zero render size.
    • COMMIT: test(visual): RED negative test — sentinel catches render_main_interface no-op

Phase 6: Layer 2 Verification — Win32 PrintWindow Pixel Baseline

Focus: The HARD pixel-diff test that catches ALL visual regressions.

  • Task 6.1: RED test for Win32 PrintWindow capture

    • WHERE: New file tests/test_visual_baseline_default.py
    • WHAT: Write 4 tests:
      1. test_capture_gui_window_pixels — use live_gui fixture, wait for first frame, call _capture_gui_window_png(), assert the returned PNG file exists with size > 0
      2. test_capture_returns_png_with_correct_dimensions — assert PNG dimensions match the forced viewport (1680x1050 from F6.1 env var)
      3. test_capture_handles_missing_hwnd — simulate window-not-found → return Result(data=None, errors=[ErrorInfo])
      4. test_capture_does_not_crash_on_zero_size — simulate hwnd with zero-size window → return Result(data=None, errors=[ErrorInfo]) (no crash)
    • HOW: Import _capture_gui_window_png from src.gui_2. Use live_gui fixture with MANUAL_SLOP_TEST_VIEWPORT=1680x1050 + MANUAL_SLOP_TEST_THEME=dark env vars.
    • SAFETY: 1-space indent. CRLF. Skip on non-Windows. Use tmp_path for PNG output.
    • RUN: uv run pytest tests/test_visual_baseline_default.py -v — Expected: 4 FAIL (ImportError: cannot import name '_capture_gui_window_png').
    • COMMIT: test(visual): RED phase tests for Win32 PrintWindow capture
  • Task 6.2: Implement _capture_gui_window_png in src/gui_2.py

    • WHERE: src/gui_2.py — insert after _install_default_layout_pre_run_result
    • WHAT: Port the Win32 PrintWindow capture logic. Find imgui window via win32gui.FindWindow(None, "manual slop"); allocate DC + bitmap; call win32gui.PrintWindow(hwnd, hdc, win32con.PW_RENDERFULLCONTENT); convert to PNG via Pillow.Image.frombuffer(...); save to given Path. Returns Result[Path].
    • HOW: Import win32gui, win32con, win32ui from pywin32. Import PIL.Image. The function signature: _capture_gui_window_png(out_path: Path) -> Result[Path].
    • SAFETY: 1-space indent. CRLF. No comments. Wrap each Win32 call in try/except returning ErrorInfo. Use win32gui.DestroyWindow(hwnd) after capture (cleanup).
    • RUN: uv run pytest tests/test_visual_baseline_default.py -v — Expected: 4 PASS.
    • COMMIT: feat(gui): add _capture_gui_window_png via Win32 PrintWindow + Pillow
  • Task 6.3: Generate baseline PNG

    • WHERE: New file tests/artifacts/visual_baseline_default.png
    • WHAT: Capture the running GUI's pixels after install fires + panels render. This is the "known good" reference.
    • HOW: Run uv run python -m pytest tests/test_visual_baseline_default.py::test_capture_gui_window_pixels --capture=tee-sys -s and manually save the output PNG. OR: write a one-shot helper script scripts/capture_visual_baseline.py that spawns the app, waits for first frame, calls _capture_gui_window_png(artifacts/visual_baseline_default.png), exits.
    • SAFETY: 1-space indent. CRLF. The baseline PNG must be captured AFTER all install infrastructure is in place. Verify the PNG visually (user's eyes) before committing.
    • RUN: uv run python scripts/capture_visual_baseline.py — Expected: writes tests/artifacts/visual_baseline_default.png (~50-200 KB depending on viewport size).
    • COMMIT: feat(visual): commit visual_baseline_default.png (the known-good pixel reference)
  • Task 6.4: RED test for pixel diff comparison

    • WHERE: Append to tests/test_visual_baseline_default.py
    • WHAT: Write 3 tests:
      1. test_pixel_diff_below_threshold — capture current + load baseline → assert diff < 1%
      2. test_pixel_diff_above_threshold_on_corrupt_ini — corrupt the INI (delete [Docking][Data] line) + capture → assert diff > 5% (catches regression)
      3. test_pixel_diff_threshold_configurable — pass --threshold 0.05 → assert behavior matches
    • HOW: Use _compute_pixel_diff(baseline_path, current_path) -> float. The function: load both via Pillow.Image.open(), convert to RGB, compute numpy.abs(np.array(a) - np.array(b)).mean() / 255.0.
    • SAFETY: 1-space indent. CRLF. Skip on non-Windows. Threshold default = 0.01 (1%).
    • RUN: uv run pytest tests/test_visual_baseline_default.py -v — Expected: 3 new FAIL (ImportError: cannot import name '_compute_pixel_diff').
    • COMMIT: test(visual): RED phase tests for pixel diff comparison
  • Task 6.5: Implement _compute_pixel_diff in src/gui_2.py

    • WHERE: src/gui_2.py — insert after _capture_gui_window_png
    • WHAT: Compare two PNGs and return pixel diff as float (0.0-1.0).
    • HOW: Load both via Pillow.Image.open(path).convert("RGB"). Convert to numpy arrays. Compute numpy.abs(a - b).mean() / 255.0. Return the float.
    • SAFETY: 1-space indent. CRLF. Handle size mismatch (resize to larger dim). Handle missing files → return 1.0 (100% diff = max divergence).
    • RUN: uv run pytest tests/test_visual_baseline_default.py -v — Expected: 7 PASS (4 from 6.1 + 3 new).
    • COMMIT: feat(gui): add _compute_pixel_diff (numpy-based pixel comparison)

Phase 7: Layer 3 Verification — Forced Test Viewport + Theme

Focus: Make the baseline deterministic so pixel diff is meaningful.

  • Task 7.1: RED test for MANUAL_SLOP_TEST_VIEWPORT env var

    • WHERE: New file tests/test_test_mode_env_vars.py
    • WHAT: Write 2 tests:
      1. test_viewport_env_var_overrides_default — spawn subprocess with MANUAL_SLOP_TEST_VIEWPORT=1920x1080 env var → assert App.run() set runner_params.app_window_params.window_geometry.size = (1920, 1080)
      2. test_viewport_env_var_unset_uses_default — spawn without env var → assert size = (1680, 1200) (current default at line 651)
    • HOW: Use subprocess to spawn sloppy.py with env vars. Inspect via the /api/gui Hook API endpoint after launch.
    • SAFETY: 1-space indent. CRLF. Use subprocess.run with timeout. Clean up subprocess on test teardown via kill_process_tree fixture.
    • RUN: uv run pytest tests/test_test_mode_env_vars.py -v — Expected: 2 FAIL (env var not honored).
    • COMMIT: test(env): RED phase tests for MANUAL_SLOP_TEST_VIEWPORT env var
  • Task 7.2: Implement MANUAL_SLOP_TEST_VIEWPORT parsing in App.run

    • WHERE: src/gui_2.py:651 — before self.runner_params.app_window_params.window_geometry.size = (1680, 1200), add the env var parsing.
    • WHAT: Read env var. If set and matches WxH pattern, override the size.
    • HOW: Add 5 lines before line 651:
      _test_viewport = os.environ.get("MANUAL_SLOP_TEST_VIEWPORT")
      if _test_viewport and "x" in _test_viewport:
          _w, _h = _test_viewport.split("x", 1)
          _w, _h = int(_w), int(_h)
      else:
          _w, _h = 1680, 1200
      self.runner_params.app_window_params.window_geometry.size = (_w, _h)
      
    • SAFETY: 1-space indent. CRLF. Wrap the parsing in try/except (return default on ValueError).
    • RUN: uv run pytest tests/test_test_mode_env_vars.py -v — Expected: 2 PASS.
    • COMMIT: feat(gui): honor MANUAL_SLOP_TEST_VIEWPORT env var (Layer 3 forced viewport)
  • Task 7.3: RED test for MANUAL_SLOP_TEST_THEME env var

    • WHERE: Append to tests/test_test_mode_env_vars.py
    • WHAT: Write 2 tests:
      1. test_theme_env_var_overrides_default — spawn with MANUAL_SLOP_TEST_THEME=dark → assert runner_params.imgui_window_params.tweaked_theme is ImGuiTheme_.ImGuiColorsDark
      2. test_theme_env_var_unset_uses_default — spawn without env var → assert theme is NOT forced
    • HOW: Same subprocess + Hook API pattern.
    • SAFETY: 1-space indent. CRLF.
    • RUN: uv run pytest tests/test_test_mode_env_vars.py -v — Expected: 2 new FAIL (env var not honored).
    • COMMIT: test(env): RED phase tests for MANUAL_SLOP_TEST_THEME env var
  • Task 7.4: Implement MANUAL_SLOP_TEST_THEME parsing in App.run

    • WHERE: src/gui_2.py:654 — before self.runner_params.imgui_window_params.tweaked_theme = theme.get_tweaked_theme(), add the env var parsing.
    • WHAT: Read env var. If set to dark, force theme to hello_imgui.ImGuiTheme_.ImGuiColorsDark.
    • HOW: Add 5 lines before line 654:
      _test_theme = os.environ.get("MANUAL_SLOP_TEST_THEME")
      if _test_theme == "dark":
          self.runner_params.imgui_window_params.tweaked_theme = hello_imgui.ImGuiTheme_.ImGuiColorsDark
      else:
          self.runner_params.imgui_window_params.tweaked_theme = theme.get_tweaked_theme()
      
    • SAFETY: 1-space indent. CRLF. The original theme.get_tweaked_theme() call becomes the else branch.
    • RUN: uv run pytest tests/test_test_mode_env_vars.py -v — Expected: 4 PASS.
    • COMMIT: feat(gui): honor MANUAL_SLOP_TEST_THEME env var (Layer 3 forced theme)

Phase 8: Layer 4 Verification — Cannot-Skip Gates

Focus: Make the verification infrastructure impossible to ignore.

  • Task 8.1: Create scripts/check_visual_baseline.py

    • WHERE: New file scripts/check_visual_baseline.py
    • WHAT: Standalone CLI script that compares two PNGs and exits 1 on diff > threshold.
    • HOW: Args: --baseline <path> (default: tests/artifacts/visual_baseline_default.png), --current <path> (required), --threshold <float> (default: 0.01). Uses Pillow + numpy for diff. Returns exit code 0 if diff ≤ threshold, exit code 1 otherwise. Print diff percentage to stdout. Use the same _compute_pixel_diff logic from Task 6.5.
    • SAFETY: 1-space indent. CRLF. Use argparse. Handle missing files gracefully (exit 1 + error message).
    • RUN: uv run python scripts/check_visual_baseline.py --help — Expected: usage message. uv run python scripts/check_visual_baseline.py --current tests/artifacts/visual_baseline_default.png --baseline tests/artifacts/visual_baseline_default.png — Expected: diff: 0.0000 PASS.
    • COMMIT: feat(visual): add scripts/check_visual_baseline.py (Layer 4 standalone CI gate)
  • Task 8.2: Wire check_visual_baseline.py into scripts/run_tests_batched.py

    • WHERE: scripts/run_tests_batched.py — add a new tier (or extend an existing one) that runs tests/test_visual_baseline_default.py + tests/test_panels_visible_after_install.py + scripts/check_visual_baseline.py.
    • WHAT: Add a tier (e.g. tier_visual) to the batched runner config. The tier runs after tier3 and before the smoke tier.
    • HOW: Read scripts/run_tests_batched.py config → add tier_visual → list the 3 commands.
    • SAFETY: 1-space indent. CRLF. Don't break existing tiers.
    • RUN: uv run python scripts/run_tests_batched.py --tier visual — Expected: 7 tests pass (4 + 3 from Phase 5-6).
    • COMMIT: chore(tests): wire Layer 1+2 visual tests into scripts/run_tests_batched.py
  • Task 8.3: Write docs/guide_visual_verification.md

    • WHERE: New file docs/guide_visual_verification.md
    • WHAT: 200-300 line guide documenting:
      • The 4 layers (per-panel sentinel, pixel baseline, forced viewport/theme, cannot-skip gates)
      • How to add a new visual baseline
      • How to update an existing baseline (after a deliberate UI change)
      • The env-var protocol (MANUAL_SLOP_TEST_VIEWPORT, MANUAL_SLOP_TEST_THEME)
      • The VERIFIED-<YYYYMMDD> tag protocol
      • When to use imgui_test_engine vs PrintWindow (the trade-offs)
    • HOW: Write as a markdown guide with code blocks + cross-references to docs/guide_testing.md + docs/guide_gui_2.md.
    • SAFETY: Markdown formatting consistent with other docs/guide_*.md files.
    • RUN: N/A (docs file).
    • COMMIT: docs(visual-verification): add guide for the 4-layer visual verification protocol
  • Task 8.4: Update conductor/tracks.md schema

    • WHERE: conductor/tracks.md — find the schema section (or add a new "Track Completion Gates" section).
    • WHAT: Add a new section documenting the VERIFIED-<YYYYMMDD> tag requirement for tracks that touch src/gui_2.py. Tracks that ship without the tag are NOT marked [x].
    • HOW: Read conductor/tracks.md → find the schema → add the new gate.
    • SAFETY: Markdown formatting consistent. Cross-reference docs/guide_visual_verification.md.
    • RUN: N/A (docs file).
    • COMMIT: docs(tracks): add VERIFIED-<date> tag requirement for tracks touching src/gui_2.py
  • Task 8.5: Update docs/Readme.md to reference the new guide

    • WHERE: docs/Readme.md — find the "Per-Source-File Deep Dives" section (or equivalent) → add docs/guide_visual_verification.md entry.
    • WHAT: Add a new bullet + 1-line description.
    • HOW: Read docs/Readme.md → add the entry.
    • SAFETY: Match existing entry format.
    • RUN: N/A (docs file).
    • COMMIT: docs(readme): cross-reference guide_visual_verification.md

Phase 9: End-to-End Verification + Negative Test + Track Completion

Focus: Prove the verification infrastructure actually catches regressions, then close out the track.

  • Task 9.1: Write tests/test_visual_baseline_catches_corrupt_ini.py

    • WHERE: New file tests/test_visual_baseline_catches_corrupt_ini.py
    • WHAT: Write 1 test that uses live_gui fixture; AFTER install fires, manually delete the [Docking][Data] line from cwd/manualslop_layout.ini; re-launch + capture; assert pixel diff > 5%.
    • HOW: Spawn app → wait for first frame → corrupt INI → quit → re-launch → wait for first frame → capture screenshot → compare to baseline.
    • SAFETY: 1-space indent. CRLF. Use kill_process_tree fixture for cleanup. Skip on non-Windows.
    • RUN: uv run pytest tests/test_visual_baseline_catches_corrupt_ini.py -v — Expected: PASS (the diff should be > 5% because panels don't render visibly).
    • COMMIT: test(visual): negative test — corrupted INI catches the regression (FR8)
  • Task 9.2: Run full test batch

    • WHERE: All test files added in Phase 1-9
    • WHAT: Run scripts/run_tests_batched.py end-to-end. Verify all tiers PASS.
    • HOW: uv run python scripts/run_tests_batched.py — runs the full batch (not just tier_visual).
    • SAFETY: If any tier fails, STOP. Report to user. Do NOT mark track complete.
    • RUN: Expected: all 11 tiers PASS. If a tier fails, debug per conductor/workflow.md "Deduction Loop" rule (max 2 runs).
    • COMMIT: N/A (verification only).
  • Task 9.3: Manual visual verification gate

    • WHERE: User's machine
    • WHAT: User runs uv run sloppy.py from master. User confirms panels render visibly (Project Settings, Files & Media, AI Settings, Operations Hub, Theme on left; Discussion Hub, Log Management, Diagnostics on right).
    • HOW: User reports back. If panels DO render visibly → proceed. If panels DON'T render → STOP, debug, report.
    • SAFETY: N/A (manual gate).
    • COMMIT: N/A (manual verification only).
  • Task 9.4: User commits VERIFIED-<date> tag

    • WHERE: Master branch
    • WHAT: User commits git tag VERIFIED-20260629 <final-commit-sha> on master. Documents the visual verification.
    • HOW: git tag VERIFIED-20260629 <sha>. Add to track completion checklist.
    • SAFETY: HARD GATE. Without this tag, the track is NOT marked complete in conductor/tracks.md.
    • COMMIT: N/A (tag, not commit). But attach a git note to the final commit: git notes add -m "VISUALLY VERIFIED: panels render correctly via uv run sloppy.py from master".
  • Task 9.5: Write docs/reports/TRACK_COMPLETION_default_layout_extract_20260629.md

    • WHERE: New file docs/reports/TRACK_COMPLETION_default_layout_extract_20260629.md
    • WHAT: 100-200 line report documenting:
      • What was extracted (per FR1-FR3)
      • What was built (per FR4-FR7)
      • Test results (per FR8)
      • User verification (per 9.3)
      • Follow-up tracks (Fleury migration, imgui_test_engine integration)
      • Tier-2 archival status (user's responsibility)
    • HOW: Markdown report. Cross-reference docs/reports/PANEL_VISIBILITY_DEBUG_REPORT_20260629.md + conductor/tracks/default_layout_extract_20260629/spec.md.
    • SAFETY: 100-200 lines max. Concise.
    • COMMIT: docs(reports): TRACK_COMPLETION_default_layout_extract_20260629
  • Task 9.6: Update conductor/tracks.md to mark this track complete

    • WHERE: conductor/tracks.md — find the row for default_layout_extract_20260629 → mark [x] (with VERIFIED-20260629 tag referenced).
    • WHAT: Update the row.
    • HOW: Read conductor/tracks.md → find the row → update.
    • SAFETY: HARD GATE. The [x] requires the VERIFIED-<date> tag to exist. If absent, leave the row as [ ].
    • COMMIT: conductor(tracks): mark default_layout_extract_20260629 complete (with VERIFIED-20260629 tag)
  • Task 9.7: Conductor - User Manual Verification (Protocol in workflow.md)

    • WHERE: User-facing summary
    • WHAT: Confirm to the user that:
      • All 9 phases complete
      • All tests pass (full batch, not just tier_visual)
      • Pixel baseline PNG committed
      • VERIFIED-<date> tag exists
      • Tier-2 archival is user's responsibility
    • HOW: Brief 5-10 sentence summary in chat.
    • SAFETY: HARD GATE. Do NOT claim "track complete" without the tag + the user's confirmation.

Self-Review (per writing-plans skill)

1. Spec coverage:

  • G1 (FR1.1-FR1.4) → Phase 1 tasks ✓
  • G2 (FR2.1-FR2.5) → Phase 2-3 tasks ✓
  • G3 (FR3.2) → Phase 4 task 4.2 ✓
  • G4 (FR3.1) → Phase 4 task 4.1 ✓
  • G5 Layer 1 (FR4.1-FR4.4) → Phase 5 tasks ✓
  • G5 Layer 2 (FR5.1-FR5.6) → Phase 6 tasks ✓
  • G5 Layer 3 (FR6.1-FR6.4) → Phase 7 tasks ✓
  • G5 Layer 4 (FR7.1-F7.4) → Phase 8 tasks ✓
  • G6 (FR8.1-FR8.2) → Phase 9 task 9.1 ✓

2. Placeholder scan:

  • No "TBD", "TODO", "implement later", "fill in details"
  • No "add appropriate error handling" — each error case is specified
  • No "similar to Task N" — each task is self-contained
  • No steps without code blocks where code is required

3. Type consistency:

  • _install_default_layout_if_emptyResult[bool] (Task 2.2, 3.1, 3.2) ✓
  • _install_default_layout_if_empty_resultResult[bool] (Task 2.2, 3.1, 3.2) ✓
  • _install_default_layout_pre_run_resultResult[bool] (Task 2.4, 3.3, 3.4) ✓
  • _capture_gui_window_pngResult[Path] (Task 6.1, 6.2) ✓
  • _compute_pixel_diff(baseline, current)float (Task 6.4, 6.5) ✓
  • LayoutFile@dataclass(frozen=True, slots=True) (Task 1.2) ✓
  • Result, ErrorInfo, ErrorKind from src.result_types (consistent throughout) ✓

4. Spec coverage check:

  • Spec §FR1.1 → Task 1.6 ✓
  • Spec §FR1.2 → Task 1.2 ✓
  • Spec §FR1.3 → Tasks 1.3, 1.4 ✓
  • Spec §FR1.4 → covered by Task 1.6 (test for INI existence) ✓
  • Spec §FR2.1 → Task 2.2 ✓
  • Spec §FR2.2 → Task 2.2 ✓
  • Spec §FR2.3 → Task 2.4 ✓
  • Spec §FR2.4 → Task 3.2 ✓
  • Spec §FR2.5 → Task 3.4 ✓
  • Spec §FR3.1 → Task 4.1 ✓
  • Spec §FR3.2 → Task 4.2 ✓
  • Spec §FR4.1-FR4.4 → Phase 5 tasks ✓
  • Spec §FR5.1-FR5.6 → Phase 6 tasks ✓
  • Spec §FR6.1-FR6.4 → Phase 7 tasks ✓
  • Spec §FR7.1-FR7.4 → Phase 8 tasks ✓
  • Spec §FR8.1-FR8.2 → Task 9.1 ✓

No gaps found.

Summary

  • 9 phases, 36 tasks (each surgical with WHERE/WHAT/HOW/SAFETY/COMMIT)
  • 3 new files: src/layouts.py, layouts/default.ini, tests/artifacts/visual_baseline_default.png, scripts/check_visual_baseline.py, docs/guide_visual_verification.md
  • 6 modified files: src/gui_2.py, src/paths.py, src/commands.py, scripts/run_tests_batched.py, conductor/tracks.md, docs/Readme.md
  • 5 new test files: tests/test_layouts.py, tests/test_paths_layouts.py, tests/test_layouts_bundled.py, tests/test_install_default_layout.py, tests/test_app_wiring_install.py, tests/test_panels_visible_after_install.py, tests/test_visual_baseline_default.py, tests/test_test_mode_env_vars.py, tests/test_visual_baseline_catches_corrupt_ini.py
  • ~36 atomic commits (1 per task)
  • HARD verification gates: Layer 1 sentinel + Layer 2 pixel baseline + Layer 3 forced viewport/theme + Layer 4 cannot-skip tags

This is the "no slippage" plan. Each task is a 2-5 minute action. Each has a commit. The verification infrastructure makes the regression impossible to reintroduce without CI catching it.