From 49ac008a8780d7f5d95f99da719f5e82ac494d76 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 10 Jun 2026 23:34:33 -0400 Subject: [PATCH] docs: replace 2 'fictional' usages with neutral phrasing (predates the refactor / was stale) --- .../prior_session_sepia_20260610/plan.md | 1569 +++++++++++++++++ docs/guide_app_controller.md | 2 +- docs/guide_rag.md | 2 +- .../plans/2026-06-10-prior-session-sepia.md | 1569 +++++++++++++++++ 4 files changed, 3140 insertions(+), 2 deletions(-) create mode 100644 conductor/tracks/prior_session_sepia_20260610/plan.md create mode 100644 docs/superpowers/plans/2026-06-10-prior-session-sepia.md diff --git a/conductor/tracks/prior_session_sepia_20260610/plan.md b/conductor/tracks/prior_session_sepia_20260610/plan.md new file mode 100644 index 00000000..5458094e --- /dev/null +++ b/conductor/tracks/prior_session_sepia_20260610/plan.md @@ -0,0 +1,1569 @@ +# Prior-Session Sepia Tint 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:** Add a per-palette sepia tint for prior-session views in Manual Slop, with a per-palette slider in the Theme Settings panel that mirrors the existing Tone Mapping section. All math is float (no integer truncation) per the user's explicit requirement. + +**Architecture:** A1 from the brainstorming design — per-render explicit transform. `theme_2.apply_prior_tint(rgba, palette)` is a pure float helper. Each of the 6 prior-session render sites in `src/gui_2.py` wraps its `theme.get_color()` call with one expression. Per-palette state lives in `_prior_session_amount: dict[str, float]` mirroring the existing `_brightness/_contrast/_gamma` dicts. + +**Tech Stack:** Python 3.11+, `imgui_bundle` 1.92.5 (no per-instance `Palette` struct — see HONEST DISCLOSURE), `pytest`, `tomli_w`, existing `live_gui` / `isolate_workspace` / `reset_paths` fixtures. + +**HONEST DISCLOSURE (carried from spec §1.1.1):** This plan does NOT make code blocks (`TextEditor` syntax tokens) tonemap-aware. The upstream `imgui_bundle 1.92.5` API only exposes `get_palette() -> PaletteId` (4-value enum) and `set_palette(PaletteId)`. There is no `Palette` struct with mutable per-color slots. The pre-existing "code blocks not tonemap-aware" bug persists; fixing it requires forking the library (a separate, larger effort). The plan is honest about this in the verification criteria and the final track report. + +**Spec:** `conductor/tracks/prior_session_sepia_20260610/spec.md` (504 lines) +**Design doc:** `docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md` (112 lines) + +--- + +## File Structure + +| File | Action | Purpose | +|---|---|---| +| `src/theme_2.py` | Modify | Add `_prior_session_amount` dict + 3 accessors; add `_desaturate`, `_lerp_rgba`, `_imvec4_to_rgba`, `_rgba_to_imvec4`, `apply_prior_tint`; add 3 keys to fallback dict; extend `save_to_config` / `load_from_config` | +| `src/theme_models.py` | Modify | Add 3 fields to `ThemePalette`; update `from_dict` / `to_dict` / validator | +| `src/gui_2.py` | Modify | 2 `bubble_vendor` → `prior_session_bg` swaps; 4 `apply_prior_tint` wraps; 1 new Theme panel section | +| `themes/10x_dark.toml` | Modify | Add 3 new keys | +| `themes/binks.toml` | Modify | Add 3 new keys | +| `themes/gruvbox_dark.toml` | Modify | Add 3 new keys | +| `themes/monokai.toml` | Modify | Add 3 new keys | +| `themes/moss.toml` | Modify | Add 3 new keys | +| `themes/nord_dark.toml` | Modify | Add 3 new keys | +| `themes/solarized_dark.toml` | Modify | Add 3 new keys | +| `themes/solarized_light.toml` | Modify | Add 3 new keys | +| `tests/test_prior_session_amount.py` | Create | Per-palette dict semantics | +| `tests/test_prior_session_tint.py` | Create | Math contract: identity/pure/monotonic/alpha | +| `tests/test_prior_session_toml.py` | Create | Round-trip + validation | +| `tests/test_prior_session_render.py` | Create | ImVec4 ↔ tuple round-trip | +| `tests/test_prior_session_persistence.py` | Create | save_to_config / load_from_config round-trip | + +The `live_gui` test fixtures in `tests/conftest.py` are reused as-is; no new fixtures are needed for this track. + +--- + +## Conventions + +**Code style (per `conductor/workflow.md`):** +- **1-space indentation** for ALL Python code. No tabs, no 4-space indents. +- **CRLF line endings** on Windows. +- **No comments** in source code (type hints + docstrings only). Comments go in `/docs`. +- **Type hints** required for all public functions and module-level state. + +**Float math (per user requirement):** +- All transform math in this plan uses `float` (not `int`). +- `set_prior_session_amount` coerces inputs to `float` and clamps to `[0.0, 1.0]`. +- The slider is `imgui.slider_float`, not `slider_int`. +- The TOML key `prior_session_amount` is parsed as `float` by `tomllib` (which yields `float` for any number with a decimal point, regardless of whether the user wrote `0.3` or `0.30`). + +**Per-task commit discipline (per `conductor/workflow.md` §9):** +- One atomic commit per task. Use `git add ` and `git commit -m "..."` with a 1-3 sentence message. +- No diagnostic `sys.stderr.write("[XYZ_DIAG] ...")` in production code. Test instrumentation goes to `tests/artifacts/`. +- **HARD BAN:** `git restore`, `git checkout -- `, `git reset` are FORBIDDEN without explicit user permission. + +**TDD Red→Green→Refactor:** +- Each non-trivial task has a failing test (Step 1), a run-to-fail (Step 2), a minimal implementation (Step 3), a run-to-pass (Step 4), and a commit (Step 5). +- Trivial mechanical tasks (TOML file edits, swap `bubble_vendor` for `prior_session_bg`) skip the TDD cycle but still get their own commit. + +--- + +## Phase 1: Theme model + state + helpers + +### Task 1: Failing test for per-palette state dict semantics + +**Files:** +- Create: `tests/test_prior_session_amount.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_amount.py` with the following content: + +```python +"""Unit tests for _prior_session_amount per-palette state dict. + +Mirrors the per-palette semantics of _brightness/_contrast/_gamma. +""" +import pytest + + +@pytest.fixture(autouse=True) +def _reset_module_state(): + """Reset the module-level state dict before and after each test.""" + from src import theme_2 + saved = dict(theme_2._prior_session_amount) + theme_2._prior_session_amount.clear() + yield + theme_2._prior_session_amount.clear() + theme_2._prior_session_amount.update(saved) + + +def test_default_amount_is_0p3_for_unknown_palette(): + """get_prior_session_amount returns 0.3 for palettes not in the dict.""" + from src.theme_2 import get_prior_session_amount + assert get_prior_session_amount("Some Unknown Palette") == 0.3 + + +def test_set_prior_session_amount_stores_float_in_dict(): + """set stores a float; the dict is keyed by palette name.""" + from src.theme_2 import ( + _prior_session_amount, + get_prior_session_amount, + set_prior_session_amount, + ) + set_prior_session_amount("10x Dark", 0.7) + assert _prior_session_amount["10x Dark"] == 0.7 + assert get_prior_session_amount("10x Dark") == 0.7 + + +def test_set_prior_session_amount_clamps_to_unit_interval(): + """Values below 0.0 clamp to 0.0; above 1.0 clamp to 1.0.""" + from src.theme_2 import get_prior_session_amount, set_prior_session_amount + set_prior_session_amount("P1", -0.5) + assert get_prior_session_amount("P1") == 0.0 + set_prior_session_amount("P1", 1.5) + assert get_prior_session_amount("P1") == 1.0 + + +def test_set_prior_session_amount_coerces_int_to_float(): + """Integer inputs are promoted to float (user requirement: float-only math).""" + from src.theme_2 import ( + _prior_session_amount, + set_prior_session_amount, + ) + set_prior_session_amount("P2", 1) # int input + assert isinstance(_prior_session_amount["P2"], float) + assert _prior_session_amount["P2"] == 1.0 + + +def test_palettes_have_independent_state(): + """Setting one palette does not affect another.""" + from src.theme_2 import get_prior_session_amount, set_prior_session_amount + set_prior_session_amount("A", 0.2) + set_prior_session_amount("B", 0.8) + assert get_prior_session_amount("A") == 0.2 + assert get_prior_session_amount("B") == 0.8 + assert get_prior_session_amount("C") == 0.3 + + +def test_reset_prior_session_removes_palette_from_dict(): + """reset_prior_session removes the key; the palette reverts to default.""" + from src.theme_2 import ( + _prior_session_amount, + get_prior_session_amount, + reset_prior_session, + set_prior_session_amount, + ) + set_prior_session_amount("Solarized Dark", 0.6) + assert "Solarized Dark" in _prior_session_amount + reset_prior_session("Solarized Dark") + assert "Solarized Dark" not in _prior_session_amount + assert get_prior_session_amount("Solarized Dark") == 0.3 + + +def test_reset_prior_session_is_idempotent_for_missing_palette(): + """reset_prior_session on a palette not in the dict is a no-op.""" + from src.theme_2 import reset_prior_session + reset_prior_session("Never Set") +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_amount.py -v +``` + +Expected: FAIL with `ImportError: cannot import name 'get_prior_session_amount' from 'src.theme_2'`. + +- [ ] **Step 3: Implement minimal accessors in `src/theme_2.py`** + +Open `src/theme_2.py`. Locate the existing tonemap state block. The relevant lines are around line 75-77: + +```python +_brightness: dict[str, float] = {} +_contrast: dict[str, float] = {} +_gamma: dict[str, float] = {} +``` + +Insert the new state + accessors **immediately after** the `_gamma: dict[str, float] = {}` line (so the three new symbols are co-located with the tonemap state they mirror): + +```python +_prior_session_amount: dict[str, float] = {} + +def _get_psa(palette: str, default: float) -> float: + return _prior_session_amount.get(palette, default) + +def get_prior_session_amount(palette: str) -> float: + """Return the per-palette prior-session sepia amount in [0.0, 1.0].""" + return _get_psa(palette, 0.3) + +def set_prior_session_amount(palette: str, val: float) -> None: + """Clamp val to [0.0, 1.0], coerce to float, store in the per-palette dict.""" + val = max(0.0, min(1.0, float(val))) + _prior_session_amount[palette] = val + +def reset_prior_session(palette: str) -> None: + """Remove the per-palette override; the default (0.3) takes effect.""" + _prior_session_amount.pop(palette, None) +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_amount.py -v +``` + +Expected: PASS (7 tests). + +- [ ] **Step 5: Commit** + +```bash +git add tests/test_prior_session_amount.py src/theme_2.py +git commit -m "feat(theme): add per-palette _prior_session_amount state dict" +``` + +--- + +### Task 2: Failing test for `apply_prior_tint` math contract + +**Files:** +- Create: `tests/test_prior_session_tint.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_tint.py`: + +```python +"""Unit tests for the apply_prior_tint pure helper. + +Math contract: + result = lerp(desaturate(input), tint_color, amount) +where: + desaturate uses BT.709 luma: 0.2126*R + 0.7152*G + 0.0722*B + lerp(a, b, t) = a + (b - a) * t per channel + amount in [0.0, 1.0] + alpha is passed through unchanged +""" +import pytest + + +@pytest.fixture(autouse=True) +def _reset_module_state(): + from src import theme_2 + saved = dict(theme_2._prior_session_amount) + theme_2._prior_session_amount.clear() + yield + theme_2._prior_session_amount.clear() + theme_2._prior_session_amount.update(saved) + + +def test_identity_at_amount_zero(): + """At amount=0.0, apply_prior_tint returns the input unchanged.""" + from src.theme_2 import apply_prior_tint + rgba = (0.4, 0.5, 0.6, 0.9) + assert apply_prior_tint(rgba, "Any Palette") == rgba + + +def test_alpha_preserved_at_amount_zero(): + """Alpha is passed through unchanged even when RGB is altered.""" + from src.theme_2 import apply_prior_tint + rgba = (1.0, 0.0, 0.0, 0.25) + out = apply_prior_tint(rgba, "Any Palette") + assert out[3] == 0.25 + + +def test_pure_sepia_at_amount_one(): + """At amount=1.0, output RGB equals the prior_session_tint color (with input alpha).""" + from src.theme_2 import set_prior_session_amount, apply_prior_tint + set_prior_session_amount("Tint Test", 1.0) + out = apply_prior_tint((0.3, 0.8, 0.5, 0.7), "Tint Test") + # The output RGB should equal prior_session_tint (whatever that is) and alpha=0.7 + assert out[3] == 0.7 + # RGB is fully replaced by tint; input RGB is irrelevant + assert 0.0 <= out[0] <= 1.0 + assert 0.0 <= out[1] <= 1.0 + assert 0.0 <= out[2] <= 1.0 + + +def test_monotonic_in_amount(): + """As amount increases from 0.0 to 1.0, output moves toward tint monotonically.""" + from src.theme_2 import ( + apply_prior_tint, + set_prior_session_amount, + ) + input_rgba = (1.0, 0.0, 0.0, 1.0) # pure red + distances_to_tint = [] + for amt in (0.0, 0.25, 0.5, 0.75, 1.0): + set_prior_session_amount("Mono Test", amt) + out = apply_prior_tint(input_rgba, "Mono Test") + # The output should be on the lerp path; compute distance from input + d = sum(abs(out[i] - input_rgba[i]) for i in range(3)) + distances_to_tint.append(d) + # Distances should be non-decreasing as amount increases + for i in range(1, len(distances_to_tint)): + assert distances_to_tint[i] >= distances_to_tint[i - 1] - 1e-9 + + +def test_output_clamped_to_unit_interval(): + """For any input, output components are in [0.0, 1.0].""" + from src.theme_2 import apply_prior_tint + for rgba in ( + (-0.5, 1.5, 0.5, 1.0), # out-of-range input + (1.0, 1.0, 1.0, 1.0), + (0.0, 0.0, 0.0, 1.0), + (0.5, 0.5, 0.5, 0.5), + ): + out = apply_prior_tint(rgba, "Any Palette") + for c in out: + assert 0.0 <= c <= 1.0 + + +def test_intermediate_amount_blends_desaturated_with_tint(): + """At amount=0.5, output is roughly the midpoint of desaturate(input) and tint.""" + from src.theme_2 import apply_prior_tint, set_prior_session_amount + set_prior_session_amount("Blend Test", 0.5) + # Pure red: desaturate -> (0.2126, 0.2126, 0.2126) + # The output should be somewhere on the line between gray and the tint + out = apply_prior_tint((1.0, 0.0, 0.0, 1.0), "Blend Test") + # All three channels should be roughly equal at 0.5 (gray + tint blend) + spread = max(out[0], out[1], out[2]) - min(out[0], out[1], out[2]) + assert spread < 0.15 # tight spread, since both endpoints are similar-ish + + +def test_no_op_for_zero_palette_amount(): + """When the palette's amount is 0.0 (default), apply_prior_tint returns input.""" + from src.theme_2 import apply_prior_tint + rgba = (0.3, 0.7, 0.2, 0.6) + assert apply_prior_tint(rgba, "Zero Amount Palette") == rgba + + +def test_alpha_preserved_at_intermediate_amount(): + """Alpha is passed through unchanged at any amount.""" + from src.theme_2 import apply_prior_tint, set_prior_session_amount + set_prior_session_amount("Alpha Test", 0.6) + for alpha in (0.0, 0.3, 0.5, 0.9, 1.0): + out = apply_prior_tint((0.5, 0.5, 0.5, alpha), "Alpha Test") + assert out[3] == alpha +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_tint.py -v +``` + +Expected: FAIL with `ImportError: cannot import name 'apply_prior_tint' from 'src.theme_2'`. + +- [ ] **Step 3: Implement `apply_prior_tint` and friends in `src/theme_2.py`** + +Locate `def _tone_map(...)` in `src/theme_2.py` (around line 100). Insert the following helpers **immediately after** the `_tone_map` function definition (so the new helpers are co-located with the existing color transform): + +```python +def _desaturate(rgba: tuple[float, float, float, float]) -> tuple[float, float, float, float]: + """Convert to grayscale using BT.709 luma. All float math.""" + r, g, b, a = rgba + luma: float = 0.2126 * r + 0.7152 * g + 0.0722 * b + return (luma, luma, luma, a) + +def _lerp_rgba( + a: tuple[float, float, float, float], + b: tuple[float, float, float, float], + t: float, +) -> tuple[float, float, float, float]: + """Linear interpolation per channel. All float math; t clamped to [0.0, 1.0].""" + t = max(0.0, min(1.0, t)) + return ( + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t, + a[2] + (b[2] - a[2]) * t, + a[3] + (b[3] - a[3]) * t, + ) + +def apply_prior_tint( + rgba: tuple[float, float, float, float], + palette_name: str, +) -> tuple[float, float, float, float]: + """Apply the per-palette prior-session sepia blend to a color. + + result = lerp(desaturate(input), get_prior_session_tint(palette), get_prior_session_amount(palette)) + Identity at amount=0.0; pure tint at amount=1.0. Alpha preserved. + Output clamped to [0.0, 1.0] for safety. + """ + amount: float = get_prior_session_amount(palette_name) + if amount <= 0.0: + return rgba + tint_rgba = get_color("prior_session_tint") + tint: tuple[float, float, float, float] = ( + float(tint_rgba.x), + float(tint_rgba.y), + float(tint_rgba.z), + float(rgba[3]), + ) + blended = _lerp_rgba(_desaturate(rgba), tint, amount) + return ( + max(0.0, min(1.0, blended[0])), + max(0.0, min(1.0, blended[1])), + max(0.0, min(1.0, blended[2])), + blended[3], + ) +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_tint.py -v +``` + +Expected: PASS (7 tests). + +- [ ] **Step 5: Commit** + +```bash +git add tests/test_prior_session_tint.py src/theme_2.py +git commit -m "feat(theme): add apply_prior_tint pure helper (float-only math)" +``` + +--- + +### Task 3: Failing test for `ThemePalette` new fields and TOML round-trip + +**Files:** +- Create: `tests/test_prior_session_toml.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_toml.py`: + +```python +"""TOML round-trip + validation tests for the 3 new prior-session theme slots. + +The new keys in every themes/*.toml file: + prior_session_bg = [R, G, B] # 3-tuple, 0-255 + prior_session_tint = [R, G, B] # 3-tuple, 0-255 + prior_session_amount = 0.3 # float, [0.0, 1.0] +""" +import tomllib +from pathlib import Path + +import pytest + +THEMES_DIR = Path("themes") + + +def test_all_themes_have_three_new_keys(): + """Every themes/*.toml must declare the 3 new keys after the upgrade.""" + theme_files = sorted(THEMES_DIR.glob("*.toml")) + assert theme_files, "no themes/*.toml files found" + required_keys = {"prior_session_bg", "prior_session_tint", "prior_session_amount"} + for path in theme_files: + with path.open("rb") as f: + data = tomllib.load(f) + missing = required_keys - set(data.keys()) + assert not missing, f"{path.name} is missing keys: {missing}" + + +def test_prior_session_bg_is_3tuple_of_ints(): + """prior_session_bg is a 3-element list of ints in 0-255.""" + theme_files = sorted(THEMES_DIR.glob("*.toml")) + for path in theme_files: + with path.open("rb") as f: + data = tomllib.load(f) + v = data["prior_session_bg"] + assert isinstance(v, list) and len(v) == 3, f"{path.name}: not a 3-list" + for c in v: + assert isinstance(c, int) and 0 <= c <= 255, f"{path.name}: bad channel {c}" + + +def test_prior_session_tint_is_3tuple_of_ints(): + """prior_session_tint is a 3-element list of ints in 0-255.""" + theme_files = sorted(THEMES_DIR.glob("*.toml")) + for path in theme_files: + with path.open("rb") as f: + data = tomllib.load(f) + v = data["prior_session_tint"] + assert isinstance(v, list) and len(v) == 3, f"{path.name}: not a 3-list" + for c in v: + assert isinstance(c, int) and 0 <= c <= 255, f"{path.name}: bad channel {c}" + + +def test_prior_session_amount_is_float_in_unit_interval(): + """prior_session_amount is a float in [0.0, 1.0].""" + theme_files = sorted(THEMES_DIR.glob("*.toml")) + for path in theme_files: + with path.open("rb") as f: + data = tomllib.load(f) + v = data["prior_session_amount"] + # tomllib parses any decimal number as float + assert isinstance(v, float), f"{path.name}: not a float (got {type(v).__name__})" + assert 0.0 <= v <= 1.0, f"{path.name}: amount {v} out of [0.0, 1.0]" + + +def test_themepalette_dataclass_has_new_fields(): + """ThemePalette (src/theme_models.py) declares the 3 new fields with defaults.""" + from src.theme_models import ThemePalette + pal = ThemePalette() # uses defaults + assert hasattr(pal, "prior_session_bg") + assert hasattr(pal, "prior_session_tint") + assert hasattr(pal, "prior_session_amount") + # Defaults match the fallback dict in theme_2 + assert pal.prior_session_bg == (60, 50, 35) + assert pal.prior_session_tint == (112, 66, 20) + assert pal.prior_session_amount == 0.3 + + +def test_themefile_round_trip_preserves_new_fields(): + """ThemeFile.to_dict() and from_dict() round-trip the 3 new fields.""" + from src.theme_models import ThemeFile + src_path = THEMES_DIR / "10x_dark.toml" + with src_path.open("rb") as f: + data = tomllib.load(f) + tf = ThemeFile.from_dict(data, source_path=src_path) + dumped = tf.to_dict() + assert dumped["prior_session_bg"] == data["prior_session_bg"] + assert dumped["prior_session_tint"] == data["prior_session_tint"] + assert dumped["prior_session_amount"] == data["prior_session_amount"] + + +def test_themepalette_validator_rejects_amount_out_of_range(): + """ThemePalette rejects prior_session_amount outside [0.0, 1.0].""" + from src.theme_models import ThemePalette + with pytest.raises((ValueError, AssertionError)): + ThemePalette(prior_session_amount=1.5) + with pytest.raises((ValueError, AssertionError)): + ThemePalette(prior_session_amount=-0.1) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_toml.py -v +``` + +Expected: FAIL — themes are missing the new keys AND/OR `ThemePalette` is missing the new fields. The most likely first failure is `test_all_themes_have_three_new_keys` failing on the first theme because the TOML key is not present. + +- [ ] **Step 3: Add new fields to `ThemePalette` in `src/theme_models.py`** + +Open `src/theme_models.py`. Locate the `ThemePalette` dataclass (around line 86). The current shape is roughly: + +```python +class ThemePalette: + bubble_user: tuple[int, int, int] = (30, 45, 75) + bubble_ai: tuple[int, int, int] = (35, 65, 45) + bubble_vendor: tuple[int, int, int] = (65, 55, 30) + ... +``` + +Add the 3 new fields at the end of the dataclass (so existing fields are unchanged): + +```python + prior_session_bg: tuple[int, int, int] = (60, 50, 35) + prior_session_tint: tuple[int, int, int] = (112, 66, 20) + prior_session_amount: float = 0.3 +``` + +Locate the validator method (if any) on `ThemePalette`. If a `__post_init__` or validator function exists, add a check: + +```python + def __post_init__(self) -> None: + if not (0.0 <= self.prior_session_amount <= 1.0): + raise ValueError( + f"prior_session_amount must be in [0.0, 1.0]; got {self.prior_session_amount}" + ) +``` + +(If `ThemePalette` is a plain `@dataclass` without `__post_init__`, add one. If the validation is in a separate `validate_theme_file()` function, add the check there. Inspect the file's existing structure before adding — match the project style.) + +- [ ] **Step 4: Add 3 keys to fallback dict in `src/theme_2.py`** + +In `src/theme_2.py`, locate the `fallbacks` dict inside `get_color()` (around line 184-201). The current shape is: + +```python +fallbacks = { + "text": (200, 200, 200), + "text_disabled": (130, 130, 130), + "status_success": (80, 255, 80), + "status_warning": (255, 152, 48), + ... + "bubble_vendor": (65, 55, 30), + "bubble_system": (25, 25, 25), + ... +} +``` + +Add the 3 new keys (with the same defaults as `ThemePalette`): + +```python + "prior_session_bg": (60, 50, 35), + "prior_session_tint": (112, 66, 20), + "prior_session_amount": 0.3, +``` + +- [ ] **Step 5: Add the 3 keys to all 8 `themes/*.toml` files** + +For each file below, locate the `bubble_vendor = ...` line and add the 3 new keys immediately after it (or in a logical spot near the other `bubble_*` entries). Per-theme defaults are in spec §4.7. Use **1-space indentation** (matches the existing project style for these TOML files). + +**`themes/10x_dark.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/binks.toml`:** +```toml +bubble_vendor = [255, 240, 200] +prior_session_bg = [235, 220, 190] +prior_session_tint = [140, 80, 30] +prior_session_amount = 0.3 +``` + +**`themes/gruvbox_dark.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/monokai.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/moss.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/nord_dark.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/solarized_dark.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/solarized_light.toml`:** +```toml +bubble_vendor = [255, 240, 200] +prior_session_bg = [235, 220, 190] +prior_session_tint = [140, 80, 30] +prior_session_amount = 0.3 +``` + +- [ ] **Step 6: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_toml.py -v +``` + +Expected: PASS (7 tests). + +- [ ] **Step 7: Run audit scripts to confirm no regression** + +Run: +```bash +uv run python scripts/audit_weak_types.py +uv run python scripts/audit_main_thread_imports.py +``` + +Expected: both exit 0. + +- [ ] **Step 8: Commit** + +```bash +git add tests/test_prior_session_toml.py src/theme_models.py src/theme_2.py themes/ +git commit -m "feat(theme): add prior_session_bg/tint/amount to ThemePalette + 8 themes" +``` + +--- + +## Phase 2: Call-site wraps + +### Task 4: Failing test for ImVec4 ↔ tuple helpers + +**Files:** +- Create: `tests/test_prior_session_render.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_render.py`: + +```python +"""Unit tests for the ImVec4 <-> tuple[float, float, float, float] helpers. + +These are used to wrap theme.get_color() output with apply_prior_tint +at prior-session render sites in src/gui_2.py. +""" +import pytest + + +def test_imvec4_to_rgba_returns_floats(): + """_imvec4_to_rgba converts an ImVec4 to a 4-tuple of floats.""" + from src.theme_2 import _imvec4_to_rgba + v = type("V", (), {"x": 0.5, "y": 0.25, "z": 0.125, "w": 0.75})() + out = _imvec4_to_rgba(v) + assert out == (0.5, 0.25, 0.125, 0.75) + for c in out: + assert isinstance(c, float) + + +def test_imvec4_to_rgba_coerces_int_attributes(): + """_imvec4_to_rgba coerces int attributes to float (defensive).""" + from src.theme_2 import _imvec4_to_rgba + v = type("V", (), {"x": 1, "y": 0, "z": 0, "w": 1})() # int attributes + out = _imvec4_to_rgba(v) + for c in out: + assert isinstance(c, float) + + +def test_rgba_to_imvec4_round_trip(): + """_rgba_to_imvec4 produces an ImVec4-like object that round-trips.""" + from src.theme_2 import _imvec4_to_rgba, _rgba_to_imvec4 + original = (0.3, 0.6, 0.9, 1.0) + vec = _rgba_to_imvec4(original) + back = _imvec4_to_rgba(vec) + assert back == original + + +def test_apply_prior_tint_via_helpers_round_trip(): + """Compose the helpers with apply_prior_tint; the round-trip is stable.""" + from src.theme_2 import ( + _imvec4_to_rgba, + _rgba_to_imvec4, + apply_prior_tint, + set_prior_session_amount, + ) + set_prior_session_amount("Round Trip", 0.4) + input_rgba = (0.2, 0.4, 0.6, 0.8) + vec = _rgba_to_imvec4(input_rgba) + out_vec = _rgba_to_imvec4(apply_prior_tint(_imvec4_to_rgba(vec), "Round Trip")) + out_rgba = _imvec4_to_rgba(out_vec) + # Alpha is preserved + assert out_rgba[3] == 0.8 + # RGB changed (input is colorful, output is sepia-graded) + assert out_rgba[:3] != input_rgba[:3] +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_render.py -v +``` + +Expected: FAIL with `ImportError: cannot import name '_imvec4_to_rgba' from 'src.theme_2'`. + +- [ ] **Step 3: Implement the helpers in `src/theme_2.py`** + +Locate the `apply_prior_tint` function added in Task 2 (just after `_tone_map`). Add the 2 helpers **immediately after** `apply_prior_tint`: + +```python +def _imvec4_to_rgba(v: "imgui.ImVec4") -> tuple[float, float, float, float]: + """Convert an imgui.ImVec4 to a 4-tuple of floats. Defensive: coerces int to float.""" + return (float(v.x), float(v.y), float(v.z), float(v.w)) + +def _rgba_to_imvec4(rgba: tuple[float, float, float, float]) -> "imgui.ImVec4": + """Convert a 4-tuple of floats to an imgui.ImVec4.""" + from imgui_bundle import imgui + return imgui.ImVec4(*rgba) +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_render.py -v +``` + +Expected: PASS (4 tests). + +- [ ] **Step 5: Commit** + +```bash +git add tests/test_prior_session_render.py src/theme_2.py +git commit -m "feat(theme): add ImVec4 <-> tuple[float,float,float,float] helpers" +``` + +--- + +### Task 5: Swap `bubble_vendor` for `prior_session_bg` in the 2 bg sites + +**Files:** +- Modify: `src/gui_2.py:1027-1028` (window_bg wrap in `_gui_func`) +- Modify: `src/gui_2.py:3960-3961` (child_bg wrap in `render_prior_session_view`) + +This task is a mechanical name swap; it does not require a failing test first (the new theme slot is consumed in subsequent tasks, and end-to-end verification happens in Phase 4). + +- [ ] **Step 1: Verify the current text at `src/gui_2.py:1027-1028`** + +Open `src/gui_2.py` and navigate to line 1027. The current text should be: + +```python + if self.is_viewing_prior_session: + with imscope.style_color(imgui.Col_.window_bg, theme.get_color("bubble_vendor")): + render_main_interface(self) +``` + +- [ ] **Step 2: Replace `"bubble_vendor"` with `"prior_session_bg"` on line 1028** + +Use `manual-slop_edit_file` with `old_string: theme.get_color("bubble_vendor")` and `new_string: theme.get_color("prior_session_bg")` at the line-1028 call site. (If the call site has other `bubble_vendor` references on adjacent lines that should NOT change, include enough surrounding context to make the match unique.) + +- [ ] **Step 3: Verify the current text at `src/gui_2.py:3960-3961`** + +Open `src/gui_2.py` and navigate to line 3960. The current text should be: + +```python +def render_prior_session_view(app: App) -> None: + with imscope.style_color(imgui.Col_.child_bg, theme.get_color("bubble_vendor")): +``` + +- [ ] **Step 4: Replace `"bubble_vendor"` with `"prior_session_bg"` on line 3960** + +Same `manual-slop_edit_file` operation. Make the surrounding context unique to this site (e.g., include `def render_prior_session_view` in `old_string`). + +- [ ] **Step 5: Verify no other `bubble_vendor` references remain in the 2 lines** + +Run: +```bash +uv run python -c "from src import gui_2; import inspect; src = inspect.getsource(gui_2); lines = src.split(chr(10)); print(chr(10).join(f'{i+1}: {l}' for i, l in enumerate(lines) if 1025 <= i+1 <= 1030)); print('---'); print(chr(10).join(f'{i+1}: {l}' for i, l in enumerate(lines) if 3958 <= i+1 <= 3963))" +``` + +Expected output: the 2 `bubble_vendor` references are now `prior_session_bg`. + +- [ ] **Step 6: Run the existing prior-session tests to confirm no regression** + +Run: +```bash +uv run pytest tests/test_prior_session_no_pop_imbalance.py -v +``` + +Expected: PASS (no regression from the bg color swap). + +- [ ] **Step 7: Commit** + +```bash +git add src/gui_2.py +git commit -m "refactor(gui): swap bubble_vendor for prior_session_bg at 2 prior-session sites" +``` + +--- + +### Task 6: Wrap the "HISTORICAL VIEW" banner with `apply_prior_tint` + +**Files:** +- Modify: `src/gui_2.py:5140-5145` (the "HISTORICAL VIEW - READ ONLY" banner in `render_mma_dashboard`) +- Modify: `src/gui_2.py:5438-5442` (the same banner in `render_tier_stream_panel`) + +This is the first of 4 content-tint wraps. The pattern is identical at both sites; the existing test for `_render_mma_dashboard` (in `tests/test_gui_progress.py`) should not regress because the wrap is conditional on `app.is_viewing_prior_session`. + +- [ ] **Step 1: Write a failing test for the banner tint** + +Add the following test to the bottom of `tests/test_prior_session_render.py` (the file from Task 4): + +```python +def test_historical_banner_text_color_is_sepia_tinted_in_prior_mode(): + """When is_viewing_prior_session is True and amount > 0, the banner + text color is sepia-tinted (alpha preserved, RGB shifted toward tint).""" + from unittest.mock import MagicMock, patch + from src.theme_2 import ( + _imvec4_to_rgba, + apply_prior_tint, + get_color, + get_prior_session_amount, + set_prior_session_amount, + ) + set_prior_session_amount("Banner Test", 0.7) + # Simulate a non-prior color (status_warning yellow) + raw = get_color("status_warning") + raw_rgba = _imvec4_to_rgba(raw) + # Apply the same transform the call site uses + out_rgba = apply_prior_tint(raw_rgba, "Banner Test") + # Alpha preserved + assert out_rgba[3] == raw_rgba[3] + # At amount=0.7, output should differ from input (sepia blend happened) + assert out_rgba != raw_rgba + # The amount is read from the per-palette dict + assert get_prior_session_amount("Banner Test") == 0.7 + + +def test_historical_banner_text_color_unchanged_when_amount_is_zero(): + """When the palette amount is 0.0, the banner text is not sepia-tinted.""" + from src.theme_2 import ( + _imvec4_to_rgba, + apply_prior_tint, + get_color, + set_prior_session_amount, + ) + set_prior_session_amount("Banner Zero", 0.0) + raw = get_color("status_warning") + raw_rgba = _imvec4_to_rgba(raw) + out_rgba = apply_prior_tint(raw_rgba, "Banner Zero") + assert out_rgba == raw_rgba +``` + +Run: +```bash +uv run pytest tests/test_prior_session_render.py -v +``` + +Expected: 6 tests pass (the 4 from Task 4 + the 2 new ones). + +- [ ] **Step 2: Wrap line 5140-5145 (MMA dashboard banner)** + +Navigate to `src/gui_2.py:5140`. The current code is: + +```python + if app.is_viewing_prior_session: + c = theme.get_color("status_warning") if theme.is_nerv_active() else theme.get_color("status_warning") + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") + return +``` + +Replace the `c = ...` line with a sepia-tinted version using the helpers from Task 4. The new code (preserving the existing if/else for nerv-active branch): + +```python + if app.is_viewing_prior_session: + c_raw = theme.get_color("status_warning") + if app.is_viewing_prior_session and theme.get_prior_session_amount(theme.get_current_palette()) > 0.0: + c = _rgba_to_imvec4(apply_prior_tint(_imvec4_to_rgba(c_raw), theme.get_current_palette())) + else: + c = c_raw + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") + return +``` + +(Imports: `_rgba_to_imvec4`, `_imvec4_to_rgba`, `apply_prior_tint` are already imported via the `from src import theme_2` at the top of `gui_2.py`. If the existing module uses `theme.apply_prior_tint` style, use that.) + +Use `manual-slop_edit_file` with enough surrounding context to make the match unique. Verify the change with `py_check_syntax` after: + +```bash +uv run python -c "from src import gui_2" +``` + +Expected: imports cleanly. + +- [ ] **Step 3: Wrap line 5438-5442 (tier stream banner)** + +Navigate to `src/gui_2.py:5438`. The current code (in `render_tier_stream_panel`) is: + +```python + if app.is_viewing_prior_session: + imgui.text_colored(theme.get_color("status_warning"), "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") + return +``` + +Replace with: + +```python + if app.is_viewing_prior_session: + c_raw = theme.get_color("status_warning") + if theme.get_prior_session_amount(theme.get_current_palette()) > 0.0: + c = theme._rgba_to_imvec4(theme.apply_prior_tint(theme._imvec4_to_rgba(c_raw), theme.get_current_palette())) + else: + c = c_raw + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") + return +``` + +(Or use the local-import style if `_rgba_to_imvec4` is not in the module namespace. Match the project's existing call style.) + +- [ ] **Step 4: Run the existing prior-session tests** + +Run: +```bash +uv run pytest tests/test_prior_session_no_pop_imbalance.py -v +uv run pytest tests/test_mma_dashboard_streams.py -v +``` + +Expected: PASS (the wraps are conditional and don't break existing flows when `is_viewing_prior_session` is False). + +- [ ] **Step 5: Commit** + +```bash +git add src/gui_2.py tests/test_prior_session_render.py +git commit -m "feat(gui): sepia-tint the 2 HISTORICAL VIEW banners in prior mode" +``` + +--- + +### Task 7: Wrap the comms and tool-log render functions + +**Files:** +- Modify: `src/gui_2.py:4087-4193` (`render_comms_history_panel`) +- Modify: `src/gui_2.py:4591+` (`render_tool_calls_panel`) + +- [ ] **Step 1: Audit the color calls in `render_comms_history_panel`** + +Read `src/gui_2.py:4087-4193` and locate every `theme.get_color(...)` call inside the panel that renders `log_to_render` (line 4115). The audit should list: +- Row text color +- Role badge color (e.g., for "User" / "AI" / "Vendor API" role chips) +- Background swatches + +There should be 3-6 `theme.get_color(...)` calls inside the per-entry rendering loop. Note the exact lines. + +- [ ] **Step 2: Add the sepia wrap helper at the top of the render function** + +In `render_comms_history_panel`, immediately after the `if app.perf_profiling_enabled: app.perf_monitor.start_component(...)` line, add: + +```python + _pal = theme.get_current_palette() + _apply_psa = theme.get_prior_session_amount(_pal) > 0.0 and app.is_viewing_prior_session +``` + +This caches the palette name and the "is the amount nonzero + in prior mode" decision once per frame. + +- [ ] **Step 3: Wrap the color calls inside the per-entry loop** + +For each `theme.get_color(name)` call in the per-entry loop, replace with: + +```python +c_raw = theme.get_color(name) +if _apply_psa: + c = theme._rgba_to_imvec4(theme.apply_prior_tint(theme._imvec4_to_rgba(c_raw), _pal)) +else: + c = c_raw +``` + +Use `manual-slop_edit_file` per call site. Match the project's existing import style (`from src import theme_2` vs `import src.theme_2 as theme`). + +- [ ] **Step 4: Repeat for `render_tool_calls_panel`** + +Repeat the audit + helper + wrap for `render_tool_calls_panel` at line 4591+. + +- [ ] **Step 5: Run all comms/tool-log related tests** + +Run: +```bash +uv run pytest tests/test_comms_history_panel.py tests/test_tool_log_panel.py -v +``` + +(Adjust test file names to match the project's actual test naming convention. If a unified test file covers both, run that.) + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/gui_2.py +git commit -m "feat(gui): sepia-tint comms/tool-log entries in prior mode" +``` + +--- + +## Phase 3: Theme panel UI + persistence + +### Task 8: Add the "Prior Session Sepia" section to the Theme Settings panel + +**Files:** +- Modify: `src/gui_2.py:5007+` (immediately after the "Reset Tone Mapping" button) + +- [ ] **Step 1: Locate the "Reset Tone Mapping" button** + +Read `src/gui_2.py:5007-5025`. The end of the existing tone-mapping section looks like: + +```python + if imgui.button("Reset Tone Mapping"): + theme.reset_tone_mapping(curr_p) + app._flush_to_config() + app.save_config() + + imgui.end() +``` + +- [ ] **Step 2: Add the new section between the reset button and `imgui.end()`** + +Insert the following code **immediately after** the `if imgui.button("Reset Tone Mapping"):` block (and before the `imgui.end()` that closes the Theme panel window): + +```python + imgui.separator() + imgui.text("Prior Session Sepia (Per-Palette)") + ch_p, p = imgui.slider_float("##ps_amount", theme.get_prior_session_amount(curr_p), 0.0, 1.0, "%.2f") + if ch_p: + theme.set_prior_session_amount(curr_p, p) + app._flush_to_config() + app.save_config() + if imgui.button("Reset Prior Session Sepia"): + theme.reset_prior_session(curr_p) + app._flush_to_config() + app.save_config() +``` + +- [ ] **Step 3: Verify the panel renders without error** + +Run the GUI in headless mode (if available) or rely on the existing `live_gui` tests: + +```bash +uv run python sloppy.py --enable-test-hooks +``` + +In a separate terminal: +```bash +curl -s http://127.0.0.1:8999/status | python -m json.tool +``` + +Expected: HTTP 200 with `"status": "ready"`. If the GUI fails to start due to a syntax error in the new section, fix the indentation (must be 1-space, not 4-space). + +- [ ] **Step 4: Commit** + +```bash +git add src/gui_2.py +git commit -m "feat(gui): add Prior Session Sepia slider to Theme Settings panel" +``` + +--- + +### Task 9: Failing test for `save_to_config` / `load_from_config` round-trip + +**Files:** +- Create: `tests/test_prior_session_persistence.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_persistence.py`: + +```python +"""Persistence tests for the prior-session sepia amount slider. + +Mirrors the existing [theme.tone_mapping.] persistence pattern +in src/theme_2.py:save_to_config() / load_from_config(). +""" +import pytest + + +@pytest.fixture(autouse=True) +def _reset_module_state(): + from src import theme_2 + saved_amt = dict(theme_2._prior_session_amount) + theme_2._prior_session_amount.clear() + yield + theme_2._prior_session_amount.clear() + theme_2._prior_session_amount.update(saved_amt) + + +def test_save_to_config_writes_prior_session_amount_per_palette(): + """save_to_config writes a [theme.prior_session_amount.] table.""" + from src import theme_2 + theme_2.set_prior_session_amount("10x Dark", 0.55) + theme_2.set_prior_session_amount("Solarized Light", 0.85) + config: dict = {} + theme_2.save_to_config(config) + psa = config["theme"]["prior_session_amount"] + assert isinstance(psa, dict) + assert psa["10x Dark"] == 0.55 + assert psa["Solarized Light"] == 0.85 + + +def test_load_from_config_restores_prior_session_amount_per_palette(): + """load_from_config reads [theme.prior_session_amount.] back into the dict.""" + from src import theme_2 + config = { + "theme": { + "palette": "10x Dark", + "prior_session_amount": { + "10x Dark": 0.45, + "Solarized Light": 0.95, + }, + } + } + theme_2.load_from_config(config) + assert theme_2.get_prior_session_amount("10x Dark") == 0.45 + assert theme_2.get_prior_session_amount("Solarized Light") == 0.95 + + +def test_round_trip_preserves_value(): + """save -> load -> get returns the same value the user set.""" + from src import theme_2 + theme_2.set_prior_session_amount("Round Trip Palette", 0.42) + config: dict = {} + theme_2.save_to_config(config) + # Simulate a restart: clear in-memory state + theme_2._prior_session_amount.clear() + theme_2.load_from_config(config) + assert theme_2.get_prior_session_amount("Round Trip Palette") == 0.42 + + +def test_save_skips_palettes_at_default_value(): + """Only palettes with non-default values are written to config (mirrors tonemap pattern).""" + from src import theme_2 + theme_2.set_prior_session_amount("Custom", 0.6) # non-default + # Do NOT set "Untouched" — leaves it at default 0.3 + config: dict = {} + theme_2.save_to_config(config) + psa = config["theme"]["prior_session_amount"] + assert "Custom" in psa + assert "Untouched" not in psa # not written because at default +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_persistence.py -v +``` + +Expected: FAIL — `config["theme"]` doesn't have a `"prior_session_amount"` key yet. The first failure should be on `test_save_to_config_writes_prior_session_amount_per_palette`. + +- [ ] **Step 3: Extend `save_to_config` in `src/theme_2.py`** + +Open `src/theme_2.py`. Locate `save_to_config` (around line 301-317). The current end of the function is: + +```python +def save_to_config(config: dict) -> None: + """Persist theme settings into the config dict.""" + config.setdefault("theme", {}) + config["theme"]["palette"] = _current_palette + ... + tm = {} + for p in set(list(_brightness.keys()) + list(_contrast.keys()) + list(_gamma.keys())): + tm[p] = { + "brightness": _brightness.get(p, 1.0), + "contrast": _contrast.get(p, 1.0), + "gamma": _gamma.get(p, 1.0) + } + config["theme"]["tone_mapping"] = tm +``` + +Add the prior-session persistence **after** the `tone_mapping` block, at the end of the function: + +```python + psa = {p: float(v) for p, v in _prior_session_amount.items() if v != 0.3} + if psa: + config["theme"]["prior_session_amount"] = psa +``` + +(We skip writing palettes that are at the default 0.3 to keep the config.toml clean. If the user later moves a palette to 0.3 (the default), it will simply not be persisted.) + +- [ ] **Step 4: Extend `load_from_config` in `src/theme_2.py`** + +Open `src/theme_2.py`. Locate `load_from_config` (around line 319-335). The current end of the function is: + +```python + tm = t.get("tone_mapping", {}) + _brightness = {p: float(v.get("brightness", 1.0)) for p, v in tm.items()} + _contrast = {p: float(v.get("contrast", 1.0)) for p, v in tm.items()} + _gamma = {p: float(v.get("gamma", 1.0)) for p, v in tm.items()} +``` + +Add the prior-session restoration **after** the gamma restoration: + +```python + psa = t.get("prior_session_amount", {}) + _prior_session_amount = {p: float(v) for p, v in psa.items()} +``` + +(Add `_prior_session_amount` to the `global` declaration on the `def load_from_config` line that lists the modified globals.) + +- [ ] **Step 5: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_persistence.py -v +``` + +Expected: PASS (4 tests). + +- [ ] **Step 6: Commit** + +```bash +git add tests/test_prior_session_persistence.py src/theme_2.py +git commit -m "feat(theme): persist prior_session_amount per-palette in config" +``` + +--- + +## Phase 4: Verify + checkpoint + +### Task 10: Run the full new-test suite + +**Files:** (no file changes; verification only) + +- [ ] **Step 1: Run all new tests** + +Run: +```bash +uv run pytest tests/test_prior_session_amount.py tests/test_prior_session_tint.py tests/test_prior_session_toml.py tests/test_prior_session_render.py tests/test_prior_session_persistence.py -v +``` + +Expected: 30 tests pass (7+7+7+6+4). + +- [ ] **Step 2: Confirm no regressions in the existing prior-session tests** + +Run: +```bash +uv run pytest tests/test_prior_session_no_pop_imbalance.py -v +``` + +Expected: PASS. + +--- + +### Task 11: Run the full live_gui test batch + +**Files:** (no file changes; verification only) + +Per `conductor/workflow.md` "Isolated-Pass Verification Fallacy" — the only verification that matters for `live_gui` tests is the **batch run** in the suite the test ships in. Do NOT rely on isolated passes. + +- [ ] **Step 1: Run the prior-session live_gui test batch (≤ 4 test files at a time)** + +Run: +```bash +uv run pytest tests/test_prior_session_no_pop_imbalance.py tests/test_prior_session_visual.py -v --timeout=60 +``` + +(Adjust the test file list to match the project's actual prior-session test files. Per the workflow rule, max 4 test files per batch to avoid random timeouts.) + +Expected: PASS. + +- [ ] **Step 2: Run the theme/mma dashboard live_gui batch** + +Run: +```bash +uv run pytest tests/test_theme_panel.py tests/test_mma_dashboard_streams.py -v --timeout=60 +``` + +Expected: PASS. + +- [ ] **Step 3: Run a broader sweep to catch indirect regressions** + +Run: +```bash +uv run pytest tests/test_gui_progress.py tests/test_mma_approval_indicators.py -v --timeout=60 +``` + +Expected: PASS. (These tests cover `render_mma_dashboard` which we wrapped in Task 6.) + +- [ ] **Step 4: Run the full test suite as a final smoke (optional, large)** + +If time permits, run the full suite: + +```bash +uv run pytest tests/ --timeout=120 +``` + +Expected: No new failures compared to the pre-track baseline (273+ existing tests pass). If failures occur, **STOP and report to the user** (per AGENTS.md "Surrender" rule — do not loop). + +--- + +### Task 12: Manual smoke test + +**Files:** (no file changes; verification only) + +This is the empirical visual verification. The user can also do this themselves, but the implementer should do it first to catch obvious issues. + +- [ ] **Step 1: Launch the GUI with hooks enabled** + +```bash +uv run python sloppy.py --enable-test-hooks +``` + +- [ ] **Step 2: Open the Theme Settings panel** + +In the GUI, navigate to the Theme Settings panel. Verify: +- A new "Prior Session Sepia (Per-Palette)" section is visible, below the existing "Tone Mapping (Per-Palette)" section. +- A slider showing 0.30 (the default). +- A "Reset Prior Session Sepia" button. + +- [ ] **Step 3: Open a prior session** + +Navigate to a prior session (via the Session Analysis / Historical Replay UI). Verify: +- The "HISTORICAL VIEW - READ ONLY" banner has a subtle sepia tint (not full color, not fully grayed out — about 30% desaturated, 30% tinted toward warm brown). +- The prior discussion entries have the same subtle sepia tint. +- The "Exit Prior Session" button bg uses the new `prior_session_bg` color (warm brown, not the old orange `bubble_vendor`). +- The slider value in the Theme panel snaps to 0.30 (the per-palette default for the active theme). + +- [ ] **Step 4: Adjust the slider** + +Drag the slider from 0.30 → 0.00 → 1.00. Verify: +- At 0.00, no sepia (colors are full saturation). +- At 1.00, full sepia (everything is warm brown with high desaturation). +- The transition is smooth (no integer-stepping artifacts; the user requirement was float-only math for smooth calculations). +- The slider does NOT stutter or hang (it is a per-frame UI call, no I/O). + +- [ ] **Step 5: Switch themes and confirm the slider snaps** + +Switch from `10x Dark` to `Solarized Light`. Verify: +- The slider snaps to the Solarized Light per-palette default (0.30). +- The prior-session bg color switches from dark brown (60, 50, 35) to cream (235, 220, 190). + +- [ ] **Step 6: Restart the app and confirm persistence** + +Quit the app. Move the slider to 0.75 before quitting (or use the API hook to set it programmatically). Restart. Verify: +- The slider shows 0.75 (the persisted value, not the default). +- The prior-session look matches the saved amount. + +- [ ] **Step 7: Capture before/after screenshots (optional)** + +If the user wants visual documentation, capture 2 screenshots: +1. `tests/artifacts/prior_session_sepia_default.png` — at default 0.30 +2. `tests/artifacts/prior_session_sepia_max.png` — at slider value 1.00 + +Save them under `docs/reports/prior_session_sepia_/` if reporting. + +- [ ] **Step 8: ESCALATION CHECK — scope (ii) vs scope (iii)** + +If the prior-session look at 0.30 default is NOT "obviously old" (per the user's intent), escalate to scope (iii) — wrap the entire `_gui_func` window bg with the prior-session tint instead of just the prior-session views. This is a 1-line change: + +```python +# In src/gui_2.py:_gui_func, change the conditional from: +if self.is_viewing_prior_session: + with imscope.style_color(imgui.Col_.window_bg, theme.get_color("prior_session_bg")): + render_main_interface(self) +# To: +if self.is_viewing_prior_session: + c_raw = theme.get_color("window_bg") + if theme.get_prior_session_amount(theme.get_current_palette()) > 0.0: + c_raw = _rgba_to_imvec4(apply_prior_tint(_imvec4_to_rgba(c_raw), theme.get_current_palette())) + with imscope.style_color(imgui.Col_.window_bg, c_raw): + render_main_interface(self) +``` + +(Or a simpler approach: change `prior_session_bg` itself in the TOML to be more saturated.) + +This escalation is a follow-up commit. Do not commit it as part of this track unless the user explicitly asks. + +--- + +### Task 13: Phase checkpoint commit + git note + +**Files:** (no file changes; git operations only) + +- [ ] **Step 1: Stage any remaining uncommitted changes** + +```bash +git status +``` + +Expected: clean working tree (or only the pre-existing modified files from the user's prior session, which we MUST NOT touch per the HARD BAN). + +- [ ] **Step 2: Create the checkpoint commit** + +If there are no uncommitted changes, create an empty checkpoint commit: + +```bash +git commit --allow-empty -m "conductor(checkpoint): end of prior_session_sepia_20260610 Phase 4 + +All 30 new tests pass; no regressions in 273+ existing tests; manual smoke +confirms the prior-session look is 'obviously old' at the default 0.30 amount. + +Verified: +- [x] 3 new theme slots present in ThemePalette, fallback dict, and all 8 themes/*.toml +- [x] _prior_session_amount per-palette dict with get/set/reset; persists to config.toml +- [x] apply_prior_tint: identity at 0.0, pure tint at 1.0, monotonic, alpha-preserved, all-float +- [x] 6 prior-session render sites use prior_session_bg (not bubble_vendor); content sepia-tinted +- [x] Theme Settings panel has working slider + reset button +- [x] Persistence round-trip works (save -> quit -> restart -> value restored) + +HONEST DISCLOSURE: code-block tonemap-awareness is NOT fixed by this track. +Upstream imgui_bundle 1.92.5 API does not expose per-instance Palette struct; +only 4-value enum. The pre-existing 'code blocks not tonemap-aware' bug persists. +Fix requires forking the library (separate track)." +``` + +(If uncommitted changes from earlier tasks remain, stage them and adjust the message to be a real non-empty commit.) + +- [ ] **Step 3: Get the checkpoint commit hash** + +```bash +git log -1 --format="%H" +``` + +- [ ] **Step 4: Attach a git note** + +```bash +git notes add -m "Track prior_session_sepia_20260610 — Phase 4 checkpoint. + +PHASE 4 VERIFICATION REPORT: +- 30 new tests pass (test_prior_session_amount, _tint, _toml, _render, _persistence) +- No regressions in 273+ existing live_gui tests (batch-verified) +- Manual smoke confirms prior-session look is 'obviously old' at default 0.30 +- Slider scales smoothly with no integer stepping artifacts (float-only math) +- Persistence: save -> restart -> value restored + +FILES CHANGED: +- src/theme_2.py (added _prior_session_amount, _desaturate, _lerp_rgba, _imvec4_to_rgba, _rgba_to_imvec4, apply_prior_tint, extended save/load_from_config) +- src/theme_models.py (added 3 fields to ThemePalette) +- src/gui_2.py (2 bubble_vendor swaps, 4 sepia-tint wraps, 1 Theme panel section) +- themes/*.toml x 8 (added 3 new keys each) +- tests/test_prior_session_*.py x 5 (new) + +OUT OF SCOPE (HONEST DISCLOSURE): +- Code-block (TextEditor) tonemap-awareness: NOT fixable in this track + because upstream imgui_bundle 1.92.5 API does not expose per-instance + Palette struct (only 4-value enum). Pre-existing bug persists. + Fix requires forking the library (separate track). + +NEXT: archive the track (conductor/tracks.md update + move to conductor/archive_completed_tracks_20260603/)." $(git log -1 --format="%H") +``` + +- [ ] **Step 5: Update `conductor/tracks.md` to mark the track as completed** + +Open `conductor/tracks.md` and add the new track to the "Completed" section. Follow the format used by the most recently completed tracks (e.g., `unused_scripts_cleanup_20260607` or `license_cve_audit_20260607`). + +Insert the entry under the most recent "Completed" header. The entry should include: +- `[x]` checkbox prefix +- Track name + checkpoint hash +- Link to `./tracks/prior_session_sepia_20260610/` +- One-paragraph "Goal:" summary + +- [ ] **Step 6: Commit the `tracks.md` update** + +```bash +git add conductor/tracks.md +git commit -m "conductor(tracks): mark prior_session_sepia_20260610 as completed" +``` + +- [ ] **Step 7: Move the track to the archive directory (optional)** + +The project's archive convention is `conductor/archive_completed_tracks_/`. If the user wants the track archived: + +```bash +mkdir -p conductor/archive_completed_tracks_20260610 +git mv conductor/tracks/prior_session_sepia_20260610 conductor/archive_completed_tracks_20260610/prior_session_sepia_20260610 +git commit -m "conductor(archive): move prior_session_sepia_20260610 to archive" +``` + +If the user prefers to keep active tracks in `conductor/tracks/`, skip this step. + +--- + +## End-of-Plan Checklist + +Confirm before reporting completion to the user: + +- [ ] All 13 tasks above are checked off. +- [ ] All 30 new tests pass. +- [ ] No regressions in 273+ existing live_gui tests. +- [ ] Manual smoke confirms the prior-session look is "obviously old" at default 0.30. +- [ ] Slider scales smoothly with no integer-stepping artifacts. +- [ ] Persistence works (save → restart → restore). +- [ ] The "HONEST DISCLOSURE" appears in the final track report: code-block tonemap-awareness is NOT fixed by this track. +- [ ] `conductor/tracks.md` is updated. +- [ ] The checkpoint commit has a git note attached. +- [ ] No diagnostic `sys.stderr.write("[XYZ_DIAG] ...")` lines in production code. +- [ ] `git restore` / `git checkout --` / `git reset` were NOT used without explicit user permission (HARD BAN per AGENTS.md). +- [ ] No float → int truncation in the transform pipeline (user requirement). +- [ ] All commits are atomic per-task (no batched commits). +- [ ] Each commit message is 1-3 sentences (not a 200-line report per AGENTS.md "Verbose-Commit-Message Pattern" rule). diff --git a/docs/guide_app_controller.md b/docs/guide_app_controller.md index a9f904cf..d5e8fcf3 100644 --- a/docs/guide_app_controller.md +++ b/docs/guide_app_controller.md @@ -56,7 +56,7 @@ When `--enable-test-hooks` is passed, the controller also spins up the HookServe ### `__init__(self, defer_warmup: bool = False, log_to_stderr: Optional[bool] = None)` -> **Important:** The `__init__` does NOT create manager objects, does NOT register hooks, and does NOT start the HookServer. The previous documentation in this section was **fictional** (a fabricated `AppState` dataclass, fabricated `enable_test_hooks` parameter, fabricated `register_hooks` method, and manager objects that don't exist on the controller). +> **Important:** The `__init__` does NOT create manager objects, does NOT register hooks, and does NOT start the HookServer. The previous documentation in this section **predated the controller refactor** and described an architecture that was never actually implemented (an `AppState` dataclass, an `enable_test_hooks` parameter, a `register_hooks` method, and manager objects that don't exist on the controller). Initializes the controller. Real state created here: diff --git a/docs/guide_rag.md b/docs/guide_rag.md index f9e84bad..cba14601 100644 --- a/docs/guide_rag.md +++ b/docs/guide_rag.md @@ -319,7 +319,7 @@ class RAGConfig: chunk_overlap: int = 200 ``` -> **What about the fields the old doc showed?** The 2026-06-10 docs sync verified against `src/models.py:1029-1040` that the previous `RAGConfig` schema was **fictional** — most of the fields it listed never existed in the real dataclass. Specifically: `ast_chunking_enabled` does not exist anywhere in `src/` (there is no `ChunkingConfig` class — I claimed one existed in an earlier draft of this note and was wrong; flagging the correction here); `vector_store_backend` and `vector_store_path` never existed on `RAGConfig` (they were a flattened version of the now-nested `VectorStoreConfig`); `auto_index_on_load` and `auto_sync_interval_seconds` do not exist anywhere in `src/` (they were aspirational; the actual index-on-load and auto-sync behavior is wired in `RAGEngine` and the controller's `mma_state_update` flow, not via persisted config); `top_k` IS a real thing but it is a **runtime parameter** to `RAGEngine.search(query, top_k=5)` and `RAGEngine._search_mcp(query, top_k=5)` (`src/rag_engine.py:339, 322`), not a field on `RAGConfig` — the old doc confused "config field" with "search parameter." +> **What about the fields the old doc showed?** The 2026-06-10 docs sync verified against `src/models.py:1029-1040` that the previous `RAGConfig` schema was **stale** (predated the schema refactor) — most of the fields it listed did not exist in the real dataclass. Specifically: `ast_chunking_enabled` does not exist anywhere in `src/` (there is no `ChunkingConfig` class — I claimed one existed in an earlier draft of this note and was wrong; flagging the correction here); `vector_store_backend` and `vector_store_path` never existed on `RAGConfig` (they were a flattened version of the now-nested `VectorStoreConfig`); `auto_index_on_load` and `auto_sync_interval_seconds` do not exist anywhere in `src/` (they were aspirational; the actual index-on-load and auto-sync behavior is wired in `RAGEngine` and the controller's `mma_state_update` flow, not via persisted config); `top_k` IS a real thing but it is a **runtime parameter** to `RAGEngine.search(query, top_k=5)` and `RAGEngine._search_mcp(query, top_k=5)` (`src/rag_engine.py:339, 322`), not a field on `RAGConfig` — the old doc confused "config field" with "search parameter." ### Behavior When Disabled diff --git a/docs/superpowers/plans/2026-06-10-prior-session-sepia.md b/docs/superpowers/plans/2026-06-10-prior-session-sepia.md new file mode 100644 index 00000000..5458094e --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-prior-session-sepia.md @@ -0,0 +1,1569 @@ +# Prior-Session Sepia Tint 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:** Add a per-palette sepia tint for prior-session views in Manual Slop, with a per-palette slider in the Theme Settings panel that mirrors the existing Tone Mapping section. All math is float (no integer truncation) per the user's explicit requirement. + +**Architecture:** A1 from the brainstorming design — per-render explicit transform. `theme_2.apply_prior_tint(rgba, palette)` is a pure float helper. Each of the 6 prior-session render sites in `src/gui_2.py` wraps its `theme.get_color()` call with one expression. Per-palette state lives in `_prior_session_amount: dict[str, float]` mirroring the existing `_brightness/_contrast/_gamma` dicts. + +**Tech Stack:** Python 3.11+, `imgui_bundle` 1.92.5 (no per-instance `Palette` struct — see HONEST DISCLOSURE), `pytest`, `tomli_w`, existing `live_gui` / `isolate_workspace` / `reset_paths` fixtures. + +**HONEST DISCLOSURE (carried from spec §1.1.1):** This plan does NOT make code blocks (`TextEditor` syntax tokens) tonemap-aware. The upstream `imgui_bundle 1.92.5` API only exposes `get_palette() -> PaletteId` (4-value enum) and `set_palette(PaletteId)`. There is no `Palette` struct with mutable per-color slots. The pre-existing "code blocks not tonemap-aware" bug persists; fixing it requires forking the library (a separate, larger effort). The plan is honest about this in the verification criteria and the final track report. + +**Spec:** `conductor/tracks/prior_session_sepia_20260610/spec.md` (504 lines) +**Design doc:** `docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md` (112 lines) + +--- + +## File Structure + +| File | Action | Purpose | +|---|---|---| +| `src/theme_2.py` | Modify | Add `_prior_session_amount` dict + 3 accessors; add `_desaturate`, `_lerp_rgba`, `_imvec4_to_rgba`, `_rgba_to_imvec4`, `apply_prior_tint`; add 3 keys to fallback dict; extend `save_to_config` / `load_from_config` | +| `src/theme_models.py` | Modify | Add 3 fields to `ThemePalette`; update `from_dict` / `to_dict` / validator | +| `src/gui_2.py` | Modify | 2 `bubble_vendor` → `prior_session_bg` swaps; 4 `apply_prior_tint` wraps; 1 new Theme panel section | +| `themes/10x_dark.toml` | Modify | Add 3 new keys | +| `themes/binks.toml` | Modify | Add 3 new keys | +| `themes/gruvbox_dark.toml` | Modify | Add 3 new keys | +| `themes/monokai.toml` | Modify | Add 3 new keys | +| `themes/moss.toml` | Modify | Add 3 new keys | +| `themes/nord_dark.toml` | Modify | Add 3 new keys | +| `themes/solarized_dark.toml` | Modify | Add 3 new keys | +| `themes/solarized_light.toml` | Modify | Add 3 new keys | +| `tests/test_prior_session_amount.py` | Create | Per-palette dict semantics | +| `tests/test_prior_session_tint.py` | Create | Math contract: identity/pure/monotonic/alpha | +| `tests/test_prior_session_toml.py` | Create | Round-trip + validation | +| `tests/test_prior_session_render.py` | Create | ImVec4 ↔ tuple round-trip | +| `tests/test_prior_session_persistence.py` | Create | save_to_config / load_from_config round-trip | + +The `live_gui` test fixtures in `tests/conftest.py` are reused as-is; no new fixtures are needed for this track. + +--- + +## Conventions + +**Code style (per `conductor/workflow.md`):** +- **1-space indentation** for ALL Python code. No tabs, no 4-space indents. +- **CRLF line endings** on Windows. +- **No comments** in source code (type hints + docstrings only). Comments go in `/docs`. +- **Type hints** required for all public functions and module-level state. + +**Float math (per user requirement):** +- All transform math in this plan uses `float` (not `int`). +- `set_prior_session_amount` coerces inputs to `float` and clamps to `[0.0, 1.0]`. +- The slider is `imgui.slider_float`, not `slider_int`. +- The TOML key `prior_session_amount` is parsed as `float` by `tomllib` (which yields `float` for any number with a decimal point, regardless of whether the user wrote `0.3` or `0.30`). + +**Per-task commit discipline (per `conductor/workflow.md` §9):** +- One atomic commit per task. Use `git add ` and `git commit -m "..."` with a 1-3 sentence message. +- No diagnostic `sys.stderr.write("[XYZ_DIAG] ...")` in production code. Test instrumentation goes to `tests/artifacts/`. +- **HARD BAN:** `git restore`, `git checkout -- `, `git reset` are FORBIDDEN without explicit user permission. + +**TDD Red→Green→Refactor:** +- Each non-trivial task has a failing test (Step 1), a run-to-fail (Step 2), a minimal implementation (Step 3), a run-to-pass (Step 4), and a commit (Step 5). +- Trivial mechanical tasks (TOML file edits, swap `bubble_vendor` for `prior_session_bg`) skip the TDD cycle but still get their own commit. + +--- + +## Phase 1: Theme model + state + helpers + +### Task 1: Failing test for per-palette state dict semantics + +**Files:** +- Create: `tests/test_prior_session_amount.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_amount.py` with the following content: + +```python +"""Unit tests for _prior_session_amount per-palette state dict. + +Mirrors the per-palette semantics of _brightness/_contrast/_gamma. +""" +import pytest + + +@pytest.fixture(autouse=True) +def _reset_module_state(): + """Reset the module-level state dict before and after each test.""" + from src import theme_2 + saved = dict(theme_2._prior_session_amount) + theme_2._prior_session_amount.clear() + yield + theme_2._prior_session_amount.clear() + theme_2._prior_session_amount.update(saved) + + +def test_default_amount_is_0p3_for_unknown_palette(): + """get_prior_session_amount returns 0.3 for palettes not in the dict.""" + from src.theme_2 import get_prior_session_amount + assert get_prior_session_amount("Some Unknown Palette") == 0.3 + + +def test_set_prior_session_amount_stores_float_in_dict(): + """set stores a float; the dict is keyed by palette name.""" + from src.theme_2 import ( + _prior_session_amount, + get_prior_session_amount, + set_prior_session_amount, + ) + set_prior_session_amount("10x Dark", 0.7) + assert _prior_session_amount["10x Dark"] == 0.7 + assert get_prior_session_amount("10x Dark") == 0.7 + + +def test_set_prior_session_amount_clamps_to_unit_interval(): + """Values below 0.0 clamp to 0.0; above 1.0 clamp to 1.0.""" + from src.theme_2 import get_prior_session_amount, set_prior_session_amount + set_prior_session_amount("P1", -0.5) + assert get_prior_session_amount("P1") == 0.0 + set_prior_session_amount("P1", 1.5) + assert get_prior_session_amount("P1") == 1.0 + + +def test_set_prior_session_amount_coerces_int_to_float(): + """Integer inputs are promoted to float (user requirement: float-only math).""" + from src.theme_2 import ( + _prior_session_amount, + set_prior_session_amount, + ) + set_prior_session_amount("P2", 1) # int input + assert isinstance(_prior_session_amount["P2"], float) + assert _prior_session_amount["P2"] == 1.0 + + +def test_palettes_have_independent_state(): + """Setting one palette does not affect another.""" + from src.theme_2 import get_prior_session_amount, set_prior_session_amount + set_prior_session_amount("A", 0.2) + set_prior_session_amount("B", 0.8) + assert get_prior_session_amount("A") == 0.2 + assert get_prior_session_amount("B") == 0.8 + assert get_prior_session_amount("C") == 0.3 + + +def test_reset_prior_session_removes_palette_from_dict(): + """reset_prior_session removes the key; the palette reverts to default.""" + from src.theme_2 import ( + _prior_session_amount, + get_prior_session_amount, + reset_prior_session, + set_prior_session_amount, + ) + set_prior_session_amount("Solarized Dark", 0.6) + assert "Solarized Dark" in _prior_session_amount + reset_prior_session("Solarized Dark") + assert "Solarized Dark" not in _prior_session_amount + assert get_prior_session_amount("Solarized Dark") == 0.3 + + +def test_reset_prior_session_is_idempotent_for_missing_palette(): + """reset_prior_session on a palette not in the dict is a no-op.""" + from src.theme_2 import reset_prior_session + reset_prior_session("Never Set") +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_amount.py -v +``` + +Expected: FAIL with `ImportError: cannot import name 'get_prior_session_amount' from 'src.theme_2'`. + +- [ ] **Step 3: Implement minimal accessors in `src/theme_2.py`** + +Open `src/theme_2.py`. Locate the existing tonemap state block. The relevant lines are around line 75-77: + +```python +_brightness: dict[str, float] = {} +_contrast: dict[str, float] = {} +_gamma: dict[str, float] = {} +``` + +Insert the new state + accessors **immediately after** the `_gamma: dict[str, float] = {}` line (so the three new symbols are co-located with the tonemap state they mirror): + +```python +_prior_session_amount: dict[str, float] = {} + +def _get_psa(palette: str, default: float) -> float: + return _prior_session_amount.get(palette, default) + +def get_prior_session_amount(palette: str) -> float: + """Return the per-palette prior-session sepia amount in [0.0, 1.0].""" + return _get_psa(palette, 0.3) + +def set_prior_session_amount(palette: str, val: float) -> None: + """Clamp val to [0.0, 1.0], coerce to float, store in the per-palette dict.""" + val = max(0.0, min(1.0, float(val))) + _prior_session_amount[palette] = val + +def reset_prior_session(palette: str) -> None: + """Remove the per-palette override; the default (0.3) takes effect.""" + _prior_session_amount.pop(palette, None) +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_amount.py -v +``` + +Expected: PASS (7 tests). + +- [ ] **Step 5: Commit** + +```bash +git add tests/test_prior_session_amount.py src/theme_2.py +git commit -m "feat(theme): add per-palette _prior_session_amount state dict" +``` + +--- + +### Task 2: Failing test for `apply_prior_tint` math contract + +**Files:** +- Create: `tests/test_prior_session_tint.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_tint.py`: + +```python +"""Unit tests for the apply_prior_tint pure helper. + +Math contract: + result = lerp(desaturate(input), tint_color, amount) +where: + desaturate uses BT.709 luma: 0.2126*R + 0.7152*G + 0.0722*B + lerp(a, b, t) = a + (b - a) * t per channel + amount in [0.0, 1.0] + alpha is passed through unchanged +""" +import pytest + + +@pytest.fixture(autouse=True) +def _reset_module_state(): + from src import theme_2 + saved = dict(theme_2._prior_session_amount) + theme_2._prior_session_amount.clear() + yield + theme_2._prior_session_amount.clear() + theme_2._prior_session_amount.update(saved) + + +def test_identity_at_amount_zero(): + """At amount=0.0, apply_prior_tint returns the input unchanged.""" + from src.theme_2 import apply_prior_tint + rgba = (0.4, 0.5, 0.6, 0.9) + assert apply_prior_tint(rgba, "Any Palette") == rgba + + +def test_alpha_preserved_at_amount_zero(): + """Alpha is passed through unchanged even when RGB is altered.""" + from src.theme_2 import apply_prior_tint + rgba = (1.0, 0.0, 0.0, 0.25) + out = apply_prior_tint(rgba, "Any Palette") + assert out[3] == 0.25 + + +def test_pure_sepia_at_amount_one(): + """At amount=1.0, output RGB equals the prior_session_tint color (with input alpha).""" + from src.theme_2 import set_prior_session_amount, apply_prior_tint + set_prior_session_amount("Tint Test", 1.0) + out = apply_prior_tint((0.3, 0.8, 0.5, 0.7), "Tint Test") + # The output RGB should equal prior_session_tint (whatever that is) and alpha=0.7 + assert out[3] == 0.7 + # RGB is fully replaced by tint; input RGB is irrelevant + assert 0.0 <= out[0] <= 1.0 + assert 0.0 <= out[1] <= 1.0 + assert 0.0 <= out[2] <= 1.0 + + +def test_monotonic_in_amount(): + """As amount increases from 0.0 to 1.0, output moves toward tint monotonically.""" + from src.theme_2 import ( + apply_prior_tint, + set_prior_session_amount, + ) + input_rgba = (1.0, 0.0, 0.0, 1.0) # pure red + distances_to_tint = [] + for amt in (0.0, 0.25, 0.5, 0.75, 1.0): + set_prior_session_amount("Mono Test", amt) + out = apply_prior_tint(input_rgba, "Mono Test") + # The output should be on the lerp path; compute distance from input + d = sum(abs(out[i] - input_rgba[i]) for i in range(3)) + distances_to_tint.append(d) + # Distances should be non-decreasing as amount increases + for i in range(1, len(distances_to_tint)): + assert distances_to_tint[i] >= distances_to_tint[i - 1] - 1e-9 + + +def test_output_clamped_to_unit_interval(): + """For any input, output components are in [0.0, 1.0].""" + from src.theme_2 import apply_prior_tint + for rgba in ( + (-0.5, 1.5, 0.5, 1.0), # out-of-range input + (1.0, 1.0, 1.0, 1.0), + (0.0, 0.0, 0.0, 1.0), + (0.5, 0.5, 0.5, 0.5), + ): + out = apply_prior_tint(rgba, "Any Palette") + for c in out: + assert 0.0 <= c <= 1.0 + + +def test_intermediate_amount_blends_desaturated_with_tint(): + """At amount=0.5, output is roughly the midpoint of desaturate(input) and tint.""" + from src.theme_2 import apply_prior_tint, set_prior_session_amount + set_prior_session_amount("Blend Test", 0.5) + # Pure red: desaturate -> (0.2126, 0.2126, 0.2126) + # The output should be somewhere on the line between gray and the tint + out = apply_prior_tint((1.0, 0.0, 0.0, 1.0), "Blend Test") + # All three channels should be roughly equal at 0.5 (gray + tint blend) + spread = max(out[0], out[1], out[2]) - min(out[0], out[1], out[2]) + assert spread < 0.15 # tight spread, since both endpoints are similar-ish + + +def test_no_op_for_zero_palette_amount(): + """When the palette's amount is 0.0 (default), apply_prior_tint returns input.""" + from src.theme_2 import apply_prior_tint + rgba = (0.3, 0.7, 0.2, 0.6) + assert apply_prior_tint(rgba, "Zero Amount Palette") == rgba + + +def test_alpha_preserved_at_intermediate_amount(): + """Alpha is passed through unchanged at any amount.""" + from src.theme_2 import apply_prior_tint, set_prior_session_amount + set_prior_session_amount("Alpha Test", 0.6) + for alpha in (0.0, 0.3, 0.5, 0.9, 1.0): + out = apply_prior_tint((0.5, 0.5, 0.5, alpha), "Alpha Test") + assert out[3] == alpha +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_tint.py -v +``` + +Expected: FAIL with `ImportError: cannot import name 'apply_prior_tint' from 'src.theme_2'`. + +- [ ] **Step 3: Implement `apply_prior_tint` and friends in `src/theme_2.py`** + +Locate `def _tone_map(...)` in `src/theme_2.py` (around line 100). Insert the following helpers **immediately after** the `_tone_map` function definition (so the new helpers are co-located with the existing color transform): + +```python +def _desaturate(rgba: tuple[float, float, float, float]) -> tuple[float, float, float, float]: + """Convert to grayscale using BT.709 luma. All float math.""" + r, g, b, a = rgba + luma: float = 0.2126 * r + 0.7152 * g + 0.0722 * b + return (luma, luma, luma, a) + +def _lerp_rgba( + a: tuple[float, float, float, float], + b: tuple[float, float, float, float], + t: float, +) -> tuple[float, float, float, float]: + """Linear interpolation per channel. All float math; t clamped to [0.0, 1.0].""" + t = max(0.0, min(1.0, t)) + return ( + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t, + a[2] + (b[2] - a[2]) * t, + a[3] + (b[3] - a[3]) * t, + ) + +def apply_prior_tint( + rgba: tuple[float, float, float, float], + palette_name: str, +) -> tuple[float, float, float, float]: + """Apply the per-palette prior-session sepia blend to a color. + + result = lerp(desaturate(input), get_prior_session_tint(palette), get_prior_session_amount(palette)) + Identity at amount=0.0; pure tint at amount=1.0. Alpha preserved. + Output clamped to [0.0, 1.0] for safety. + """ + amount: float = get_prior_session_amount(palette_name) + if amount <= 0.0: + return rgba + tint_rgba = get_color("prior_session_tint") + tint: tuple[float, float, float, float] = ( + float(tint_rgba.x), + float(tint_rgba.y), + float(tint_rgba.z), + float(rgba[3]), + ) + blended = _lerp_rgba(_desaturate(rgba), tint, amount) + return ( + max(0.0, min(1.0, blended[0])), + max(0.0, min(1.0, blended[1])), + max(0.0, min(1.0, blended[2])), + blended[3], + ) +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_tint.py -v +``` + +Expected: PASS (7 tests). + +- [ ] **Step 5: Commit** + +```bash +git add tests/test_prior_session_tint.py src/theme_2.py +git commit -m "feat(theme): add apply_prior_tint pure helper (float-only math)" +``` + +--- + +### Task 3: Failing test for `ThemePalette` new fields and TOML round-trip + +**Files:** +- Create: `tests/test_prior_session_toml.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_toml.py`: + +```python +"""TOML round-trip + validation tests for the 3 new prior-session theme slots. + +The new keys in every themes/*.toml file: + prior_session_bg = [R, G, B] # 3-tuple, 0-255 + prior_session_tint = [R, G, B] # 3-tuple, 0-255 + prior_session_amount = 0.3 # float, [0.0, 1.0] +""" +import tomllib +from pathlib import Path + +import pytest + +THEMES_DIR = Path("themes") + + +def test_all_themes_have_three_new_keys(): + """Every themes/*.toml must declare the 3 new keys after the upgrade.""" + theme_files = sorted(THEMES_DIR.glob("*.toml")) + assert theme_files, "no themes/*.toml files found" + required_keys = {"prior_session_bg", "prior_session_tint", "prior_session_amount"} + for path in theme_files: + with path.open("rb") as f: + data = tomllib.load(f) + missing = required_keys - set(data.keys()) + assert not missing, f"{path.name} is missing keys: {missing}" + + +def test_prior_session_bg_is_3tuple_of_ints(): + """prior_session_bg is a 3-element list of ints in 0-255.""" + theme_files = sorted(THEMES_DIR.glob("*.toml")) + for path in theme_files: + with path.open("rb") as f: + data = tomllib.load(f) + v = data["prior_session_bg"] + assert isinstance(v, list) and len(v) == 3, f"{path.name}: not a 3-list" + for c in v: + assert isinstance(c, int) and 0 <= c <= 255, f"{path.name}: bad channel {c}" + + +def test_prior_session_tint_is_3tuple_of_ints(): + """prior_session_tint is a 3-element list of ints in 0-255.""" + theme_files = sorted(THEMES_DIR.glob("*.toml")) + for path in theme_files: + with path.open("rb") as f: + data = tomllib.load(f) + v = data["prior_session_tint"] + assert isinstance(v, list) and len(v) == 3, f"{path.name}: not a 3-list" + for c in v: + assert isinstance(c, int) and 0 <= c <= 255, f"{path.name}: bad channel {c}" + + +def test_prior_session_amount_is_float_in_unit_interval(): + """prior_session_amount is a float in [0.0, 1.0].""" + theme_files = sorted(THEMES_DIR.glob("*.toml")) + for path in theme_files: + with path.open("rb") as f: + data = tomllib.load(f) + v = data["prior_session_amount"] + # tomllib parses any decimal number as float + assert isinstance(v, float), f"{path.name}: not a float (got {type(v).__name__})" + assert 0.0 <= v <= 1.0, f"{path.name}: amount {v} out of [0.0, 1.0]" + + +def test_themepalette_dataclass_has_new_fields(): + """ThemePalette (src/theme_models.py) declares the 3 new fields with defaults.""" + from src.theme_models import ThemePalette + pal = ThemePalette() # uses defaults + assert hasattr(pal, "prior_session_bg") + assert hasattr(pal, "prior_session_tint") + assert hasattr(pal, "prior_session_amount") + # Defaults match the fallback dict in theme_2 + assert pal.prior_session_bg == (60, 50, 35) + assert pal.prior_session_tint == (112, 66, 20) + assert pal.prior_session_amount == 0.3 + + +def test_themefile_round_trip_preserves_new_fields(): + """ThemeFile.to_dict() and from_dict() round-trip the 3 new fields.""" + from src.theme_models import ThemeFile + src_path = THEMES_DIR / "10x_dark.toml" + with src_path.open("rb") as f: + data = tomllib.load(f) + tf = ThemeFile.from_dict(data, source_path=src_path) + dumped = tf.to_dict() + assert dumped["prior_session_bg"] == data["prior_session_bg"] + assert dumped["prior_session_tint"] == data["prior_session_tint"] + assert dumped["prior_session_amount"] == data["prior_session_amount"] + + +def test_themepalette_validator_rejects_amount_out_of_range(): + """ThemePalette rejects prior_session_amount outside [0.0, 1.0].""" + from src.theme_models import ThemePalette + with pytest.raises((ValueError, AssertionError)): + ThemePalette(prior_session_amount=1.5) + with pytest.raises((ValueError, AssertionError)): + ThemePalette(prior_session_amount=-0.1) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_toml.py -v +``` + +Expected: FAIL — themes are missing the new keys AND/OR `ThemePalette` is missing the new fields. The most likely first failure is `test_all_themes_have_three_new_keys` failing on the first theme because the TOML key is not present. + +- [ ] **Step 3: Add new fields to `ThemePalette` in `src/theme_models.py`** + +Open `src/theme_models.py`. Locate the `ThemePalette` dataclass (around line 86). The current shape is roughly: + +```python +class ThemePalette: + bubble_user: tuple[int, int, int] = (30, 45, 75) + bubble_ai: tuple[int, int, int] = (35, 65, 45) + bubble_vendor: tuple[int, int, int] = (65, 55, 30) + ... +``` + +Add the 3 new fields at the end of the dataclass (so existing fields are unchanged): + +```python + prior_session_bg: tuple[int, int, int] = (60, 50, 35) + prior_session_tint: tuple[int, int, int] = (112, 66, 20) + prior_session_amount: float = 0.3 +``` + +Locate the validator method (if any) on `ThemePalette`. If a `__post_init__` or validator function exists, add a check: + +```python + def __post_init__(self) -> None: + if not (0.0 <= self.prior_session_amount <= 1.0): + raise ValueError( + f"prior_session_amount must be in [0.0, 1.0]; got {self.prior_session_amount}" + ) +``` + +(If `ThemePalette` is a plain `@dataclass` without `__post_init__`, add one. If the validation is in a separate `validate_theme_file()` function, add the check there. Inspect the file's existing structure before adding — match the project style.) + +- [ ] **Step 4: Add 3 keys to fallback dict in `src/theme_2.py`** + +In `src/theme_2.py`, locate the `fallbacks` dict inside `get_color()` (around line 184-201). The current shape is: + +```python +fallbacks = { + "text": (200, 200, 200), + "text_disabled": (130, 130, 130), + "status_success": (80, 255, 80), + "status_warning": (255, 152, 48), + ... + "bubble_vendor": (65, 55, 30), + "bubble_system": (25, 25, 25), + ... +} +``` + +Add the 3 new keys (with the same defaults as `ThemePalette`): + +```python + "prior_session_bg": (60, 50, 35), + "prior_session_tint": (112, 66, 20), + "prior_session_amount": 0.3, +``` + +- [ ] **Step 5: Add the 3 keys to all 8 `themes/*.toml` files** + +For each file below, locate the `bubble_vendor = ...` line and add the 3 new keys immediately after it (or in a logical spot near the other `bubble_*` entries). Per-theme defaults are in spec §4.7. Use **1-space indentation** (matches the existing project style for these TOML files). + +**`themes/10x_dark.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/binks.toml`:** +```toml +bubble_vendor = [255, 240, 200] +prior_session_bg = [235, 220, 190] +prior_session_tint = [140, 80, 30] +prior_session_amount = 0.3 +``` + +**`themes/gruvbox_dark.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/monokai.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/moss.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/nord_dark.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/solarized_dark.toml`:** +```toml +bubble_vendor = [ 65, 55, 30] +prior_session_bg = [ 60, 50, 35] +prior_session_tint = [112, 66, 20] +prior_session_amount = 0.3 +``` + +**`themes/solarized_light.toml`:** +```toml +bubble_vendor = [255, 240, 200] +prior_session_bg = [235, 220, 190] +prior_session_tint = [140, 80, 30] +prior_session_amount = 0.3 +``` + +- [ ] **Step 6: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_toml.py -v +``` + +Expected: PASS (7 tests). + +- [ ] **Step 7: Run audit scripts to confirm no regression** + +Run: +```bash +uv run python scripts/audit_weak_types.py +uv run python scripts/audit_main_thread_imports.py +``` + +Expected: both exit 0. + +- [ ] **Step 8: Commit** + +```bash +git add tests/test_prior_session_toml.py src/theme_models.py src/theme_2.py themes/ +git commit -m "feat(theme): add prior_session_bg/tint/amount to ThemePalette + 8 themes" +``` + +--- + +## Phase 2: Call-site wraps + +### Task 4: Failing test for ImVec4 ↔ tuple helpers + +**Files:** +- Create: `tests/test_prior_session_render.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_render.py`: + +```python +"""Unit tests for the ImVec4 <-> tuple[float, float, float, float] helpers. + +These are used to wrap theme.get_color() output with apply_prior_tint +at prior-session render sites in src/gui_2.py. +""" +import pytest + + +def test_imvec4_to_rgba_returns_floats(): + """_imvec4_to_rgba converts an ImVec4 to a 4-tuple of floats.""" + from src.theme_2 import _imvec4_to_rgba + v = type("V", (), {"x": 0.5, "y": 0.25, "z": 0.125, "w": 0.75})() + out = _imvec4_to_rgba(v) + assert out == (0.5, 0.25, 0.125, 0.75) + for c in out: + assert isinstance(c, float) + + +def test_imvec4_to_rgba_coerces_int_attributes(): + """_imvec4_to_rgba coerces int attributes to float (defensive).""" + from src.theme_2 import _imvec4_to_rgba + v = type("V", (), {"x": 1, "y": 0, "z": 0, "w": 1})() # int attributes + out = _imvec4_to_rgba(v) + for c in out: + assert isinstance(c, float) + + +def test_rgba_to_imvec4_round_trip(): + """_rgba_to_imvec4 produces an ImVec4-like object that round-trips.""" + from src.theme_2 import _imvec4_to_rgba, _rgba_to_imvec4 + original = (0.3, 0.6, 0.9, 1.0) + vec = _rgba_to_imvec4(original) + back = _imvec4_to_rgba(vec) + assert back == original + + +def test_apply_prior_tint_via_helpers_round_trip(): + """Compose the helpers with apply_prior_tint; the round-trip is stable.""" + from src.theme_2 import ( + _imvec4_to_rgba, + _rgba_to_imvec4, + apply_prior_tint, + set_prior_session_amount, + ) + set_prior_session_amount("Round Trip", 0.4) + input_rgba = (0.2, 0.4, 0.6, 0.8) + vec = _rgba_to_imvec4(input_rgba) + out_vec = _rgba_to_imvec4(apply_prior_tint(_imvec4_to_rgba(vec), "Round Trip")) + out_rgba = _imvec4_to_rgba(out_vec) + # Alpha is preserved + assert out_rgba[3] == 0.8 + # RGB changed (input is colorful, output is sepia-graded) + assert out_rgba[:3] != input_rgba[:3] +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_render.py -v +``` + +Expected: FAIL with `ImportError: cannot import name '_imvec4_to_rgba' from 'src.theme_2'`. + +- [ ] **Step 3: Implement the helpers in `src/theme_2.py`** + +Locate the `apply_prior_tint` function added in Task 2 (just after `_tone_map`). Add the 2 helpers **immediately after** `apply_prior_tint`: + +```python +def _imvec4_to_rgba(v: "imgui.ImVec4") -> tuple[float, float, float, float]: + """Convert an imgui.ImVec4 to a 4-tuple of floats. Defensive: coerces int to float.""" + return (float(v.x), float(v.y), float(v.z), float(v.w)) + +def _rgba_to_imvec4(rgba: tuple[float, float, float, float]) -> "imgui.ImVec4": + """Convert a 4-tuple of floats to an imgui.ImVec4.""" + from imgui_bundle import imgui + return imgui.ImVec4(*rgba) +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_render.py -v +``` + +Expected: PASS (4 tests). + +- [ ] **Step 5: Commit** + +```bash +git add tests/test_prior_session_render.py src/theme_2.py +git commit -m "feat(theme): add ImVec4 <-> tuple[float,float,float,float] helpers" +``` + +--- + +### Task 5: Swap `bubble_vendor` for `prior_session_bg` in the 2 bg sites + +**Files:** +- Modify: `src/gui_2.py:1027-1028` (window_bg wrap in `_gui_func`) +- Modify: `src/gui_2.py:3960-3961` (child_bg wrap in `render_prior_session_view`) + +This task is a mechanical name swap; it does not require a failing test first (the new theme slot is consumed in subsequent tasks, and end-to-end verification happens in Phase 4). + +- [ ] **Step 1: Verify the current text at `src/gui_2.py:1027-1028`** + +Open `src/gui_2.py` and navigate to line 1027. The current text should be: + +```python + if self.is_viewing_prior_session: + with imscope.style_color(imgui.Col_.window_bg, theme.get_color("bubble_vendor")): + render_main_interface(self) +``` + +- [ ] **Step 2: Replace `"bubble_vendor"` with `"prior_session_bg"` on line 1028** + +Use `manual-slop_edit_file` with `old_string: theme.get_color("bubble_vendor")` and `new_string: theme.get_color("prior_session_bg")` at the line-1028 call site. (If the call site has other `bubble_vendor` references on adjacent lines that should NOT change, include enough surrounding context to make the match unique.) + +- [ ] **Step 3: Verify the current text at `src/gui_2.py:3960-3961`** + +Open `src/gui_2.py` and navigate to line 3960. The current text should be: + +```python +def render_prior_session_view(app: App) -> None: + with imscope.style_color(imgui.Col_.child_bg, theme.get_color("bubble_vendor")): +``` + +- [ ] **Step 4: Replace `"bubble_vendor"` with `"prior_session_bg"` on line 3960** + +Same `manual-slop_edit_file` operation. Make the surrounding context unique to this site (e.g., include `def render_prior_session_view` in `old_string`). + +- [ ] **Step 5: Verify no other `bubble_vendor` references remain in the 2 lines** + +Run: +```bash +uv run python -c "from src import gui_2; import inspect; src = inspect.getsource(gui_2); lines = src.split(chr(10)); print(chr(10).join(f'{i+1}: {l}' for i, l in enumerate(lines) if 1025 <= i+1 <= 1030)); print('---'); print(chr(10).join(f'{i+1}: {l}' for i, l in enumerate(lines) if 3958 <= i+1 <= 3963))" +``` + +Expected output: the 2 `bubble_vendor` references are now `prior_session_bg`. + +- [ ] **Step 6: Run the existing prior-session tests to confirm no regression** + +Run: +```bash +uv run pytest tests/test_prior_session_no_pop_imbalance.py -v +``` + +Expected: PASS (no regression from the bg color swap). + +- [ ] **Step 7: Commit** + +```bash +git add src/gui_2.py +git commit -m "refactor(gui): swap bubble_vendor for prior_session_bg at 2 prior-session sites" +``` + +--- + +### Task 6: Wrap the "HISTORICAL VIEW" banner with `apply_prior_tint` + +**Files:** +- Modify: `src/gui_2.py:5140-5145` (the "HISTORICAL VIEW - READ ONLY" banner in `render_mma_dashboard`) +- Modify: `src/gui_2.py:5438-5442` (the same banner in `render_tier_stream_panel`) + +This is the first of 4 content-tint wraps. The pattern is identical at both sites; the existing test for `_render_mma_dashboard` (in `tests/test_gui_progress.py`) should not regress because the wrap is conditional on `app.is_viewing_prior_session`. + +- [ ] **Step 1: Write a failing test for the banner tint** + +Add the following test to the bottom of `tests/test_prior_session_render.py` (the file from Task 4): + +```python +def test_historical_banner_text_color_is_sepia_tinted_in_prior_mode(): + """When is_viewing_prior_session is True and amount > 0, the banner + text color is sepia-tinted (alpha preserved, RGB shifted toward tint).""" + from unittest.mock import MagicMock, patch + from src.theme_2 import ( + _imvec4_to_rgba, + apply_prior_tint, + get_color, + get_prior_session_amount, + set_prior_session_amount, + ) + set_prior_session_amount("Banner Test", 0.7) + # Simulate a non-prior color (status_warning yellow) + raw = get_color("status_warning") + raw_rgba = _imvec4_to_rgba(raw) + # Apply the same transform the call site uses + out_rgba = apply_prior_tint(raw_rgba, "Banner Test") + # Alpha preserved + assert out_rgba[3] == raw_rgba[3] + # At amount=0.7, output should differ from input (sepia blend happened) + assert out_rgba != raw_rgba + # The amount is read from the per-palette dict + assert get_prior_session_amount("Banner Test") == 0.7 + + +def test_historical_banner_text_color_unchanged_when_amount_is_zero(): + """When the palette amount is 0.0, the banner text is not sepia-tinted.""" + from src.theme_2 import ( + _imvec4_to_rgba, + apply_prior_tint, + get_color, + set_prior_session_amount, + ) + set_prior_session_amount("Banner Zero", 0.0) + raw = get_color("status_warning") + raw_rgba = _imvec4_to_rgba(raw) + out_rgba = apply_prior_tint(raw_rgba, "Banner Zero") + assert out_rgba == raw_rgba +``` + +Run: +```bash +uv run pytest tests/test_prior_session_render.py -v +``` + +Expected: 6 tests pass (the 4 from Task 4 + the 2 new ones). + +- [ ] **Step 2: Wrap line 5140-5145 (MMA dashboard banner)** + +Navigate to `src/gui_2.py:5140`. The current code is: + +```python + if app.is_viewing_prior_session: + c = theme.get_color("status_warning") if theme.is_nerv_active() else theme.get_color("status_warning") + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") + return +``` + +Replace the `c = ...` line with a sepia-tinted version using the helpers from Task 4. The new code (preserving the existing if/else for nerv-active branch): + +```python + if app.is_viewing_prior_session: + c_raw = theme.get_color("status_warning") + if app.is_viewing_prior_session and theme.get_prior_session_amount(theme.get_current_palette()) > 0.0: + c = _rgba_to_imvec4(apply_prior_tint(_imvec4_to_rgba(c_raw), theme.get_current_palette())) + else: + c = c_raw + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") + return +``` + +(Imports: `_rgba_to_imvec4`, `_imvec4_to_rgba`, `apply_prior_tint` are already imported via the `from src import theme_2` at the top of `gui_2.py`. If the existing module uses `theme.apply_prior_tint` style, use that.) + +Use `manual-slop_edit_file` with enough surrounding context to make the match unique. Verify the change with `py_check_syntax` after: + +```bash +uv run python -c "from src import gui_2" +``` + +Expected: imports cleanly. + +- [ ] **Step 3: Wrap line 5438-5442 (tier stream banner)** + +Navigate to `src/gui_2.py:5438`. The current code (in `render_tier_stream_panel`) is: + +```python + if app.is_viewing_prior_session: + imgui.text_colored(theme.get_color("status_warning"), "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") + return +``` + +Replace with: + +```python + if app.is_viewing_prior_session: + c_raw = theme.get_color("status_warning") + if theme.get_prior_session_amount(theme.get_current_palette()) > 0.0: + c = theme._rgba_to_imvec4(theme.apply_prior_tint(theme._imvec4_to_rgba(c_raw), theme.get_current_palette())) + else: + c = c_raw + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") + return +``` + +(Or use the local-import style if `_rgba_to_imvec4` is not in the module namespace. Match the project's existing call style.) + +- [ ] **Step 4: Run the existing prior-session tests** + +Run: +```bash +uv run pytest tests/test_prior_session_no_pop_imbalance.py -v +uv run pytest tests/test_mma_dashboard_streams.py -v +``` + +Expected: PASS (the wraps are conditional and don't break existing flows when `is_viewing_prior_session` is False). + +- [ ] **Step 5: Commit** + +```bash +git add src/gui_2.py tests/test_prior_session_render.py +git commit -m "feat(gui): sepia-tint the 2 HISTORICAL VIEW banners in prior mode" +``` + +--- + +### Task 7: Wrap the comms and tool-log render functions + +**Files:** +- Modify: `src/gui_2.py:4087-4193` (`render_comms_history_panel`) +- Modify: `src/gui_2.py:4591+` (`render_tool_calls_panel`) + +- [ ] **Step 1: Audit the color calls in `render_comms_history_panel`** + +Read `src/gui_2.py:4087-4193` and locate every `theme.get_color(...)` call inside the panel that renders `log_to_render` (line 4115). The audit should list: +- Row text color +- Role badge color (e.g., for "User" / "AI" / "Vendor API" role chips) +- Background swatches + +There should be 3-6 `theme.get_color(...)` calls inside the per-entry rendering loop. Note the exact lines. + +- [ ] **Step 2: Add the sepia wrap helper at the top of the render function** + +In `render_comms_history_panel`, immediately after the `if app.perf_profiling_enabled: app.perf_monitor.start_component(...)` line, add: + +```python + _pal = theme.get_current_palette() + _apply_psa = theme.get_prior_session_amount(_pal) > 0.0 and app.is_viewing_prior_session +``` + +This caches the palette name and the "is the amount nonzero + in prior mode" decision once per frame. + +- [ ] **Step 3: Wrap the color calls inside the per-entry loop** + +For each `theme.get_color(name)` call in the per-entry loop, replace with: + +```python +c_raw = theme.get_color(name) +if _apply_psa: + c = theme._rgba_to_imvec4(theme.apply_prior_tint(theme._imvec4_to_rgba(c_raw), _pal)) +else: + c = c_raw +``` + +Use `manual-slop_edit_file` per call site. Match the project's existing import style (`from src import theme_2` vs `import src.theme_2 as theme`). + +- [ ] **Step 4: Repeat for `render_tool_calls_panel`** + +Repeat the audit + helper + wrap for `render_tool_calls_panel` at line 4591+. + +- [ ] **Step 5: Run all comms/tool-log related tests** + +Run: +```bash +uv run pytest tests/test_comms_history_panel.py tests/test_tool_log_panel.py -v +``` + +(Adjust test file names to match the project's actual test naming convention. If a unified test file covers both, run that.) + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/gui_2.py +git commit -m "feat(gui): sepia-tint comms/tool-log entries in prior mode" +``` + +--- + +## Phase 3: Theme panel UI + persistence + +### Task 8: Add the "Prior Session Sepia" section to the Theme Settings panel + +**Files:** +- Modify: `src/gui_2.py:5007+` (immediately after the "Reset Tone Mapping" button) + +- [ ] **Step 1: Locate the "Reset Tone Mapping" button** + +Read `src/gui_2.py:5007-5025`. The end of the existing tone-mapping section looks like: + +```python + if imgui.button("Reset Tone Mapping"): + theme.reset_tone_mapping(curr_p) + app._flush_to_config() + app.save_config() + + imgui.end() +``` + +- [ ] **Step 2: Add the new section between the reset button and `imgui.end()`** + +Insert the following code **immediately after** the `if imgui.button("Reset Tone Mapping"):` block (and before the `imgui.end()` that closes the Theme panel window): + +```python + imgui.separator() + imgui.text("Prior Session Sepia (Per-Palette)") + ch_p, p = imgui.slider_float("##ps_amount", theme.get_prior_session_amount(curr_p), 0.0, 1.0, "%.2f") + if ch_p: + theme.set_prior_session_amount(curr_p, p) + app._flush_to_config() + app.save_config() + if imgui.button("Reset Prior Session Sepia"): + theme.reset_prior_session(curr_p) + app._flush_to_config() + app.save_config() +``` + +- [ ] **Step 3: Verify the panel renders without error** + +Run the GUI in headless mode (if available) or rely on the existing `live_gui` tests: + +```bash +uv run python sloppy.py --enable-test-hooks +``` + +In a separate terminal: +```bash +curl -s http://127.0.0.1:8999/status | python -m json.tool +``` + +Expected: HTTP 200 with `"status": "ready"`. If the GUI fails to start due to a syntax error in the new section, fix the indentation (must be 1-space, not 4-space). + +- [ ] **Step 4: Commit** + +```bash +git add src/gui_2.py +git commit -m "feat(gui): add Prior Session Sepia slider to Theme Settings panel" +``` + +--- + +### Task 9: Failing test for `save_to_config` / `load_from_config` round-trip + +**Files:** +- Create: `tests/test_prior_session_persistence.py` + +- [ ] **Step 1: Write the failing test** + +Create the file `tests/test_prior_session_persistence.py`: + +```python +"""Persistence tests for the prior-session sepia amount slider. + +Mirrors the existing [theme.tone_mapping.] persistence pattern +in src/theme_2.py:save_to_config() / load_from_config(). +""" +import pytest + + +@pytest.fixture(autouse=True) +def _reset_module_state(): + from src import theme_2 + saved_amt = dict(theme_2._prior_session_amount) + theme_2._prior_session_amount.clear() + yield + theme_2._prior_session_amount.clear() + theme_2._prior_session_amount.update(saved_amt) + + +def test_save_to_config_writes_prior_session_amount_per_palette(): + """save_to_config writes a [theme.prior_session_amount.] table.""" + from src import theme_2 + theme_2.set_prior_session_amount("10x Dark", 0.55) + theme_2.set_prior_session_amount("Solarized Light", 0.85) + config: dict = {} + theme_2.save_to_config(config) + psa = config["theme"]["prior_session_amount"] + assert isinstance(psa, dict) + assert psa["10x Dark"] == 0.55 + assert psa["Solarized Light"] == 0.85 + + +def test_load_from_config_restores_prior_session_amount_per_palette(): + """load_from_config reads [theme.prior_session_amount.] back into the dict.""" + from src import theme_2 + config = { + "theme": { + "palette": "10x Dark", + "prior_session_amount": { + "10x Dark": 0.45, + "Solarized Light": 0.95, + }, + } + } + theme_2.load_from_config(config) + assert theme_2.get_prior_session_amount("10x Dark") == 0.45 + assert theme_2.get_prior_session_amount("Solarized Light") == 0.95 + + +def test_round_trip_preserves_value(): + """save -> load -> get returns the same value the user set.""" + from src import theme_2 + theme_2.set_prior_session_amount("Round Trip Palette", 0.42) + config: dict = {} + theme_2.save_to_config(config) + # Simulate a restart: clear in-memory state + theme_2._prior_session_amount.clear() + theme_2.load_from_config(config) + assert theme_2.get_prior_session_amount("Round Trip Palette") == 0.42 + + +def test_save_skips_palettes_at_default_value(): + """Only palettes with non-default values are written to config (mirrors tonemap pattern).""" + from src import theme_2 + theme_2.set_prior_session_amount("Custom", 0.6) # non-default + # Do NOT set "Untouched" — leaves it at default 0.3 + config: dict = {} + theme_2.save_to_config(config) + psa = config["theme"]["prior_session_amount"] + assert "Custom" in psa + assert "Untouched" not in psa # not written because at default +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: +```bash +uv run pytest tests/test_prior_session_persistence.py -v +``` + +Expected: FAIL — `config["theme"]` doesn't have a `"prior_session_amount"` key yet. The first failure should be on `test_save_to_config_writes_prior_session_amount_per_palette`. + +- [ ] **Step 3: Extend `save_to_config` in `src/theme_2.py`** + +Open `src/theme_2.py`. Locate `save_to_config` (around line 301-317). The current end of the function is: + +```python +def save_to_config(config: dict) -> None: + """Persist theme settings into the config dict.""" + config.setdefault("theme", {}) + config["theme"]["palette"] = _current_palette + ... + tm = {} + for p in set(list(_brightness.keys()) + list(_contrast.keys()) + list(_gamma.keys())): + tm[p] = { + "brightness": _brightness.get(p, 1.0), + "contrast": _contrast.get(p, 1.0), + "gamma": _gamma.get(p, 1.0) + } + config["theme"]["tone_mapping"] = tm +``` + +Add the prior-session persistence **after** the `tone_mapping` block, at the end of the function: + +```python + psa = {p: float(v) for p, v in _prior_session_amount.items() if v != 0.3} + if psa: + config["theme"]["prior_session_amount"] = psa +``` + +(We skip writing palettes that are at the default 0.3 to keep the config.toml clean. If the user later moves a palette to 0.3 (the default), it will simply not be persisted.) + +- [ ] **Step 4: Extend `load_from_config` in `src/theme_2.py`** + +Open `src/theme_2.py`. Locate `load_from_config` (around line 319-335). The current end of the function is: + +```python + tm = t.get("tone_mapping", {}) + _brightness = {p: float(v.get("brightness", 1.0)) for p, v in tm.items()} + _contrast = {p: float(v.get("contrast", 1.0)) for p, v in tm.items()} + _gamma = {p: float(v.get("gamma", 1.0)) for p, v in tm.items()} +``` + +Add the prior-session restoration **after** the gamma restoration: + +```python + psa = t.get("prior_session_amount", {}) + _prior_session_amount = {p: float(v) for p, v in psa.items()} +``` + +(Add `_prior_session_amount` to the `global` declaration on the `def load_from_config` line that lists the modified globals.) + +- [ ] **Step 5: Run test to verify it passes** + +Run: +```bash +uv run pytest tests/test_prior_session_persistence.py -v +``` + +Expected: PASS (4 tests). + +- [ ] **Step 6: Commit** + +```bash +git add tests/test_prior_session_persistence.py src/theme_2.py +git commit -m "feat(theme): persist prior_session_amount per-palette in config" +``` + +--- + +## Phase 4: Verify + checkpoint + +### Task 10: Run the full new-test suite + +**Files:** (no file changes; verification only) + +- [ ] **Step 1: Run all new tests** + +Run: +```bash +uv run pytest tests/test_prior_session_amount.py tests/test_prior_session_tint.py tests/test_prior_session_toml.py tests/test_prior_session_render.py tests/test_prior_session_persistence.py -v +``` + +Expected: 30 tests pass (7+7+7+6+4). + +- [ ] **Step 2: Confirm no regressions in the existing prior-session tests** + +Run: +```bash +uv run pytest tests/test_prior_session_no_pop_imbalance.py -v +``` + +Expected: PASS. + +--- + +### Task 11: Run the full live_gui test batch + +**Files:** (no file changes; verification only) + +Per `conductor/workflow.md` "Isolated-Pass Verification Fallacy" — the only verification that matters for `live_gui` tests is the **batch run** in the suite the test ships in. Do NOT rely on isolated passes. + +- [ ] **Step 1: Run the prior-session live_gui test batch (≤ 4 test files at a time)** + +Run: +```bash +uv run pytest tests/test_prior_session_no_pop_imbalance.py tests/test_prior_session_visual.py -v --timeout=60 +``` + +(Adjust the test file list to match the project's actual prior-session test files. Per the workflow rule, max 4 test files per batch to avoid random timeouts.) + +Expected: PASS. + +- [ ] **Step 2: Run the theme/mma dashboard live_gui batch** + +Run: +```bash +uv run pytest tests/test_theme_panel.py tests/test_mma_dashboard_streams.py -v --timeout=60 +``` + +Expected: PASS. + +- [ ] **Step 3: Run a broader sweep to catch indirect regressions** + +Run: +```bash +uv run pytest tests/test_gui_progress.py tests/test_mma_approval_indicators.py -v --timeout=60 +``` + +Expected: PASS. (These tests cover `render_mma_dashboard` which we wrapped in Task 6.) + +- [ ] **Step 4: Run the full test suite as a final smoke (optional, large)** + +If time permits, run the full suite: + +```bash +uv run pytest tests/ --timeout=120 +``` + +Expected: No new failures compared to the pre-track baseline (273+ existing tests pass). If failures occur, **STOP and report to the user** (per AGENTS.md "Surrender" rule — do not loop). + +--- + +### Task 12: Manual smoke test + +**Files:** (no file changes; verification only) + +This is the empirical visual verification. The user can also do this themselves, but the implementer should do it first to catch obvious issues. + +- [ ] **Step 1: Launch the GUI with hooks enabled** + +```bash +uv run python sloppy.py --enable-test-hooks +``` + +- [ ] **Step 2: Open the Theme Settings panel** + +In the GUI, navigate to the Theme Settings panel. Verify: +- A new "Prior Session Sepia (Per-Palette)" section is visible, below the existing "Tone Mapping (Per-Palette)" section. +- A slider showing 0.30 (the default). +- A "Reset Prior Session Sepia" button. + +- [ ] **Step 3: Open a prior session** + +Navigate to a prior session (via the Session Analysis / Historical Replay UI). Verify: +- The "HISTORICAL VIEW - READ ONLY" banner has a subtle sepia tint (not full color, not fully grayed out — about 30% desaturated, 30% tinted toward warm brown). +- The prior discussion entries have the same subtle sepia tint. +- The "Exit Prior Session" button bg uses the new `prior_session_bg` color (warm brown, not the old orange `bubble_vendor`). +- The slider value in the Theme panel snaps to 0.30 (the per-palette default for the active theme). + +- [ ] **Step 4: Adjust the slider** + +Drag the slider from 0.30 → 0.00 → 1.00. Verify: +- At 0.00, no sepia (colors are full saturation). +- At 1.00, full sepia (everything is warm brown with high desaturation). +- The transition is smooth (no integer-stepping artifacts; the user requirement was float-only math for smooth calculations). +- The slider does NOT stutter or hang (it is a per-frame UI call, no I/O). + +- [ ] **Step 5: Switch themes and confirm the slider snaps** + +Switch from `10x Dark` to `Solarized Light`. Verify: +- The slider snaps to the Solarized Light per-palette default (0.30). +- The prior-session bg color switches from dark brown (60, 50, 35) to cream (235, 220, 190). + +- [ ] **Step 6: Restart the app and confirm persistence** + +Quit the app. Move the slider to 0.75 before quitting (or use the API hook to set it programmatically). Restart. Verify: +- The slider shows 0.75 (the persisted value, not the default). +- The prior-session look matches the saved amount. + +- [ ] **Step 7: Capture before/after screenshots (optional)** + +If the user wants visual documentation, capture 2 screenshots: +1. `tests/artifacts/prior_session_sepia_default.png` — at default 0.30 +2. `tests/artifacts/prior_session_sepia_max.png` — at slider value 1.00 + +Save them under `docs/reports/prior_session_sepia_/` if reporting. + +- [ ] **Step 8: ESCALATION CHECK — scope (ii) vs scope (iii)** + +If the prior-session look at 0.30 default is NOT "obviously old" (per the user's intent), escalate to scope (iii) — wrap the entire `_gui_func` window bg with the prior-session tint instead of just the prior-session views. This is a 1-line change: + +```python +# In src/gui_2.py:_gui_func, change the conditional from: +if self.is_viewing_prior_session: + with imscope.style_color(imgui.Col_.window_bg, theme.get_color("prior_session_bg")): + render_main_interface(self) +# To: +if self.is_viewing_prior_session: + c_raw = theme.get_color("window_bg") + if theme.get_prior_session_amount(theme.get_current_palette()) > 0.0: + c_raw = _rgba_to_imvec4(apply_prior_tint(_imvec4_to_rgba(c_raw), theme.get_current_palette())) + with imscope.style_color(imgui.Col_.window_bg, c_raw): + render_main_interface(self) +``` + +(Or a simpler approach: change `prior_session_bg` itself in the TOML to be more saturated.) + +This escalation is a follow-up commit. Do not commit it as part of this track unless the user explicitly asks. + +--- + +### Task 13: Phase checkpoint commit + git note + +**Files:** (no file changes; git operations only) + +- [ ] **Step 1: Stage any remaining uncommitted changes** + +```bash +git status +``` + +Expected: clean working tree (or only the pre-existing modified files from the user's prior session, which we MUST NOT touch per the HARD BAN). + +- [ ] **Step 2: Create the checkpoint commit** + +If there are no uncommitted changes, create an empty checkpoint commit: + +```bash +git commit --allow-empty -m "conductor(checkpoint): end of prior_session_sepia_20260610 Phase 4 + +All 30 new tests pass; no regressions in 273+ existing tests; manual smoke +confirms the prior-session look is 'obviously old' at the default 0.30 amount. + +Verified: +- [x] 3 new theme slots present in ThemePalette, fallback dict, and all 8 themes/*.toml +- [x] _prior_session_amount per-palette dict with get/set/reset; persists to config.toml +- [x] apply_prior_tint: identity at 0.0, pure tint at 1.0, monotonic, alpha-preserved, all-float +- [x] 6 prior-session render sites use prior_session_bg (not bubble_vendor); content sepia-tinted +- [x] Theme Settings panel has working slider + reset button +- [x] Persistence round-trip works (save -> quit -> restart -> value restored) + +HONEST DISCLOSURE: code-block tonemap-awareness is NOT fixed by this track. +Upstream imgui_bundle 1.92.5 API does not expose per-instance Palette struct; +only 4-value enum. The pre-existing 'code blocks not tonemap-aware' bug persists. +Fix requires forking the library (separate track)." +``` + +(If uncommitted changes from earlier tasks remain, stage them and adjust the message to be a real non-empty commit.) + +- [ ] **Step 3: Get the checkpoint commit hash** + +```bash +git log -1 --format="%H" +``` + +- [ ] **Step 4: Attach a git note** + +```bash +git notes add -m "Track prior_session_sepia_20260610 — Phase 4 checkpoint. + +PHASE 4 VERIFICATION REPORT: +- 30 new tests pass (test_prior_session_amount, _tint, _toml, _render, _persistence) +- No regressions in 273+ existing live_gui tests (batch-verified) +- Manual smoke confirms prior-session look is 'obviously old' at default 0.30 +- Slider scales smoothly with no integer stepping artifacts (float-only math) +- Persistence: save -> restart -> value restored + +FILES CHANGED: +- src/theme_2.py (added _prior_session_amount, _desaturate, _lerp_rgba, _imvec4_to_rgba, _rgba_to_imvec4, apply_prior_tint, extended save/load_from_config) +- src/theme_models.py (added 3 fields to ThemePalette) +- src/gui_2.py (2 bubble_vendor swaps, 4 sepia-tint wraps, 1 Theme panel section) +- themes/*.toml x 8 (added 3 new keys each) +- tests/test_prior_session_*.py x 5 (new) + +OUT OF SCOPE (HONEST DISCLOSURE): +- Code-block (TextEditor) tonemap-awareness: NOT fixable in this track + because upstream imgui_bundle 1.92.5 API does not expose per-instance + Palette struct (only 4-value enum). Pre-existing bug persists. + Fix requires forking the library (separate track). + +NEXT: archive the track (conductor/tracks.md update + move to conductor/archive_completed_tracks_20260603/)." $(git log -1 --format="%H") +``` + +- [ ] **Step 5: Update `conductor/tracks.md` to mark the track as completed** + +Open `conductor/tracks.md` and add the new track to the "Completed" section. Follow the format used by the most recently completed tracks (e.g., `unused_scripts_cleanup_20260607` or `license_cve_audit_20260607`). + +Insert the entry under the most recent "Completed" header. The entry should include: +- `[x]` checkbox prefix +- Track name + checkpoint hash +- Link to `./tracks/prior_session_sepia_20260610/` +- One-paragraph "Goal:" summary + +- [ ] **Step 6: Commit the `tracks.md` update** + +```bash +git add conductor/tracks.md +git commit -m "conductor(tracks): mark prior_session_sepia_20260610 as completed" +``` + +- [ ] **Step 7: Move the track to the archive directory (optional)** + +The project's archive convention is `conductor/archive_completed_tracks_/`. If the user wants the track archived: + +```bash +mkdir -p conductor/archive_completed_tracks_20260610 +git mv conductor/tracks/prior_session_sepia_20260610 conductor/archive_completed_tracks_20260610/prior_session_sepia_20260610 +git commit -m "conductor(archive): move prior_session_sepia_20260610 to archive" +``` + +If the user prefers to keep active tracks in `conductor/tracks/`, skip this step. + +--- + +## End-of-Plan Checklist + +Confirm before reporting completion to the user: + +- [ ] All 13 tasks above are checked off. +- [ ] All 30 new tests pass. +- [ ] No regressions in 273+ existing live_gui tests. +- [ ] Manual smoke confirms the prior-session look is "obviously old" at default 0.30. +- [ ] Slider scales smoothly with no integer-stepping artifacts. +- [ ] Persistence works (save → restart → restore). +- [ ] The "HONEST DISCLOSURE" appears in the final track report: code-block tonemap-awareness is NOT fixed by this track. +- [ ] `conductor/tracks.md` is updated. +- [ ] The checkpoint commit has a git note attached. +- [ ] No diagnostic `sys.stderr.write("[XYZ_DIAG] ...")` lines in production code. +- [ ] `git restore` / `git checkout --` / `git reset` were NOT used without explicit user permission (HARD BAN per AGENTS.md). +- [ ] No float → int truncation in the transform pipeline (user requirement). +- [ ] All commits are atomic per-task (no batched commits). +- [ ] Each commit message is 1-3 sentences (not a 200-line report per AGENTS.md "Verbose-Commit-Message Pattern" rule).