Private
Public Access
conductor(archive): move legacy superpowers specs/plans from docs/superpowers/ to conductor/archive/superpowers/
Resolves HIGH-priority recommendation #3 from superpowers_review_20260619 §16.1 (dual-convention). 41 files moved (21 specs + 20 plans + 2 subdirs + root): - docs/superpowers/specs/*.md -> conductor/archive/superpowers/specs/*.md (21 files) - docs/superpowers/plans/*.md -> conductor/archive/superpowers/plans/*.md (20 files) Original taxonomy preserved (specs/ and plans/ subdirectories intact). The superpowers-plugin source itself remains at C:/Users/Ed/.cache/opencode/.../superpowers/skills/. Future spec/plan artifacts must use the conductor convention: conductor/tracks/<id>/spec.md + plan.md.
This commit is contained in:
@@ -1,351 +0,0 @@
|
||||
# ImGui Context Manager Suite 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:** Create a context manager suite that pairs `begin`/`end` ImGui calls using Python's `with` statement to reduce scope pairing errors and improve AI legibility.
|
||||
|
||||
**Architecture:** Base `ImGuiScope` class with factory functions returning scope objects. Each scope handles begin on `__enter__` and end on `__exit__`.
|
||||
|
||||
**Tech Stack:** Python 3.11+, imgui-bundle (Dear PyGui)
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| Action | Path |
|
||||
|--------|------|
|
||||
| Create | `src/imgui_scopes.py` |
|
||||
| Create | `tests/test_imgui_scopes.py` |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create `src/imgui_scopes.py`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/imgui_scopes.py`
|
||||
|
||||
- [ ] **Step 1: Write the module**
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
import imgui_bundle
|
||||
|
||||
|
||||
class ImGuiScope:
|
||||
def __init__(self, begin_fn, end_fn, *args, **kwargs):
|
||||
self._begin_fn = begin_fn
|
||||
self._end_fn = end_fn
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
self._opened: bool | tuple = False
|
||||
self._entered = False
|
||||
|
||||
def __enter__(self):
|
||||
result = self._begin_fn(*self._args, **self._kwargs)
|
||||
if isinstance(result, tuple):
|
||||
self._opened = result[0]
|
||||
else:
|
||||
self._opened = result
|
||||
self._entered = bool(self._opened)
|
||||
return self._opened
|
||||
|
||||
def __exit__(self, *args):
|
||||
if self._entered:
|
||||
self._end_fn()
|
||||
return False
|
||||
|
||||
|
||||
def imgui_window(name: str, visible: bool = True, flags: int = 0):
|
||||
return ImGuiScope(imgui_bundle.imgui.begin, imgui_bundle.imgui.end, name, visible, flags)
|
||||
|
||||
|
||||
def imgui_table(name: str, columns: int, flags: int = 0):
|
||||
return ImGuiScope(imgui_bundle.imgui.begin_table, imgui_bundle.imgui.end_table, name, columns, flags)
|
||||
|
||||
|
||||
def imgui_menu_bar():
|
||||
return ImGuiScope(imgui_bundle.imgui.begin_menu_bar, imgui_bundle.imgui.end_menu_bar)
|
||||
|
||||
|
||||
def imgui_menu(label: str):
|
||||
return ImGuiScope(imgui_bundle.imgui.begin_menu, imgui_bundle.imgui.end_menu, label)
|
||||
|
||||
|
||||
def imgui_child(id_str: str, width: float = 0, height: float = 0, flags: int = 0):
|
||||
return ImGuiScope(imgui_bundle.imgui.begin_child, imgui_bundle.imgui.end_child, id_str, width, height, flags)
|
||||
|
||||
|
||||
def imgui_group():
|
||||
return ImGuiScope(imgui_bundle.imgui.begin_group, imgui_bundle.imgui.end_group)
|
||||
|
||||
|
||||
def imgui_popup(id_str: str):
|
||||
return ImGuiScope(imgui_bundle.imgui.begin_popup, imgui_bundle.imgui.end_popup, id_str)
|
||||
|
||||
|
||||
def imgui_tooltip():
|
||||
return ImGuiScope(imgui_bundle.imgui.begin_tooltip, imgui_bundle.imgui.end_tooltip)
|
||||
|
||||
|
||||
def imgui_clipper(count: int):
|
||||
return ImGuiScope(
|
||||
lambda n: imgui_bundle.imgui.listing_builder.begin_clipper(n, -1),
|
||||
imgui_bundle.imgui.listing_builder.end_clipper,
|
||||
count
|
||||
)
|
||||
|
||||
|
||||
def node_editor_scope(name: str):
|
||||
ed = imgui_bundle.imgui_node_editor
|
||||
return ImGuiScope(ed.begin, ed.end, name)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify syntax**
|
||||
|
||||
Run: `uv run python -c "import ast; ast.parse(open('src/imgui_scopes.py').read())"`
|
||||
Expected: No output (parse successful)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/imgui_scopes.py
|
||||
git commit -m "feat(gui): Add ImGuiScope base class and scope helpers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Create `tests/test_imgui_scopes.py`
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_imgui_scopes.py`
|
||||
- Depends on: Task 1
|
||||
|
||||
- [ ] **Step 1: Write tests**
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
|
||||
|
||||
class TestImGuiScope:
|
||||
def test_enter_calls_begin_with_args(self):
|
||||
from src.imgui_scopes import ImGuiScope
|
||||
mock_begin = MagicMock(return_value=True)
|
||||
mock_end = MagicMock()
|
||||
scope = ImGuiScope(mock_begin, mock_end, "arg1", kwarg="val")
|
||||
with scope:
|
||||
pass
|
||||
mock_begin.assert_called_once_with("arg1", kwarg="val")
|
||||
|
||||
def test_exit_calls_end_when_entered(self):
|
||||
from src.imgui_scopes import ImGuiScope
|
||||
mock_begin = MagicMock(return_value=True)
|
||||
mock_end = MagicMock()
|
||||
scope = ImGuiScope(mock_begin, mock_end)
|
||||
with scope:
|
||||
pass
|
||||
mock_end.assert_called_once()
|
||||
|
||||
def test_exit_does_not_call_end_when_not_entered(self):
|
||||
from src.imgui_scopes import ImGuiScope
|
||||
mock_begin = MagicMock(return_value=False)
|
||||
mock_end = MagicMock()
|
||||
scope = ImGuiScope(mock_begin, mock_end)
|
||||
with scope:
|
||||
pass
|
||||
mock_end.assert_not_called()
|
||||
|
||||
def test_enter_returns_opened_value(self):
|
||||
from src.imgui_scopes import ImGuiScope
|
||||
mock_begin = MagicMock(return_value=True)
|
||||
mock_end = MagicMock()
|
||||
scope = ImGuiScope(mock_begin, mock_end)
|
||||
result = scope.__enter__()
|
||||
assert result is True
|
||||
|
||||
def test_enter_handles_tuple_return(self):
|
||||
from src.imgui_scopes import ImGuiScope
|
||||
mock_begin = MagicMock(return_value=(True, "extra"))
|
||||
mock_end = MagicMock()
|
||||
scope = ImGuiScope(mock_begin, mock_end)
|
||||
with scope:
|
||||
pass
|
||||
mock_end.assert_called_once()
|
||||
|
||||
|
||||
class TestImguiWindow:
|
||||
def test_imgui_window_returns_scope(self):
|
||||
from src.imgui_scopes import imgui_window
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.begin", return_value=True) as mock_begin:
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.end") as mock_end:
|
||||
scope = imgui_window("Test")
|
||||
assert hasattr(scope, "__enter__")
|
||||
assert hasattr(scope, "__exit__")
|
||||
|
||||
|
||||
class TestImguiTable:
|
||||
def test_imgui_table_returns_scope(self):
|
||||
from src.imgui_scopes import imgui_table
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.begin_table", return_value=True) as mock_begin:
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.end_table") as mock_end:
|
||||
scope = imgui_table("TestTable", 3)
|
||||
with scope:
|
||||
pass
|
||||
mock_begin.assert_called_once()
|
||||
mock_end.assert_called_once()
|
||||
|
||||
|
||||
class TestImguiMenuBar:
|
||||
def test_imgui_menu_bar_no_args(self):
|
||||
from src.imgui_scopes import imgui_menu_bar
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.begin_menu_bar", return_value=True) as mock_begin:
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.end_menu_bar") as mock_end:
|
||||
scope = imgui_menu_bar()
|
||||
with scope:
|
||||
pass
|
||||
mock_begin.assert_called_once_with()
|
||||
mock_end.assert_called_once()
|
||||
|
||||
|
||||
class TestImguiMenu:
|
||||
def test_imgui_menu_returns_scope(self):
|
||||
from src.imgui_scopes import imgui_menu
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.begin_menu", return_value=True) as mock_begin:
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.end_menu") as mock_end:
|
||||
scope = imgui_menu("File")
|
||||
with scope:
|
||||
pass
|
||||
mock_begin.assert_called_once()
|
||||
mock_end.assert_called_once()
|
||||
|
||||
|
||||
class TestImguiChild:
|
||||
def test_imgui_child_returns_scope(self):
|
||||
from src.imgui_scopes import imgui_child
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.begin_child", return_value=True) as mock_begin:
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.end_child") as mock_end:
|
||||
scope = imgui_child("child_id", 100, 200)
|
||||
with scope:
|
||||
pass
|
||||
mock_begin.assert_called_once()
|
||||
mock_end.assert_called_once()
|
||||
|
||||
|
||||
class TestImguiGroup:
|
||||
def test_imgui_group_no_args(self):
|
||||
from src.imgui_scopes import imgui_group
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.begin_group", return_value=True) as mock_begin:
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.end_group") as mock_end:
|
||||
scope = imgui_group()
|
||||
with scope:
|
||||
pass
|
||||
mock_begin.assert_called_once()
|
||||
mock_end.assert_called_once()
|
||||
|
||||
|
||||
class TestImguiPopup:
|
||||
def test_imgui_popup_returns_scope(self):
|
||||
from src.imgui_scopes import imgui_popup
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.begin_popup", return_value=True) as mock_begin:
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.end_popup") as mock_end:
|
||||
scope = imgui_popup("popup_id")
|
||||
with scope:
|
||||
pass
|
||||
mock_begin.assert_called_once()
|
||||
mock_end.assert_called_once()
|
||||
|
||||
|
||||
class TestImguiTooltip:
|
||||
def test_imgui_tooltip_no_args(self):
|
||||
from src.imgui_scopes import imgui_tooltip
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.begin_tooltip", return_value=True) as mock_begin:
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui.end_tooltip") as mock_end:
|
||||
scope = imgui_tooltip()
|
||||
with scope:
|
||||
pass
|
||||
mock_begin.assert_called_once()
|
||||
mock_end.assert_called_once()
|
||||
|
||||
|
||||
class TestNodeEditorScope:
|
||||
def test_node_editor_scope_returns_scope(self):
|
||||
from src.imgui_scopes import node_editor_scope
|
||||
mock_ed = MagicMock()
|
||||
mock_ed.begin = MagicMock(return_value=True)
|
||||
mock_ed.end = MagicMock()
|
||||
with patch("src.imgui_scopes.imgui_bundle.imgui_node_editor", mock_ed):
|
||||
scope = node_editor_scope("TestNode")
|
||||
with scope:
|
||||
pass
|
||||
mock_ed.begin.assert_called_once()
|
||||
mock_ed.end.assert_called_once()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests**
|
||||
|
||||
Run: `uv run pytest tests/test_imgui_scopes.py -v`
|
||||
Expected: All tests PASS
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/test_imgui_scopes.py
|
||||
git commit -m "test(gui): Add tests for ImGuiScope context managers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Integration Verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` (sample migration of 2-3 windows)
|
||||
- Depends on: Task 1
|
||||
|
||||
- [ ] **Step 1: Add import to gui_2.py**
|
||||
|
||||
Add near line 1 (after existing imports):
|
||||
```python
|
||||
from src.imgui_scopes import imgui_window, imgui_table, imgui_menu_bar, imgui_menu, imgui_child, imgui_group, imgui_popup, imgui_tooltip, imgui_clipper, node_editor_scope
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Migrate one window as proof-of-concept**
|
||||
|
||||
Pick one window in `_gui_func` (e.g., lines 1061-1056 for "AI Settings"):
|
||||
|
||||
Before:
|
||||
```python
|
||||
exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"])
|
||||
if exp:
|
||||
# ... content ...
|
||||
imgui.end()
|
||||
```
|
||||
|
||||
After:
|
||||
```python
|
||||
if imgui_window("AI Settings", self.show_windows["AI Settings"]):
|
||||
# ... content ...
|
||||
# end() called automatically
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run smoke test**
|
||||
|
||||
Run: `uv run pytest tests/test_gui_startup_smoke.py -v`
|
||||
Expected: PASS (no regression from import)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/gui_2.py
|
||||
git commit -m "refactor(gui): Migrate AI Settings window to imgui_window scope"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- [ ] All scope helpers implemented with correct signatures
|
||||
- [ ] Tests mock all begin/end pairs
|
||||
- [ ] Integration test confirms no regression in gui_2.py
|
||||
- [ ] No placeholders (TBD, TODO) in any step
|
||||
- [ ] Each task commits independently
|
||||
@@ -1,208 +0,0 @@
|
||||
# ai_client.py Style Convention Curation 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:** Refactor `src/ai_client.py` to follow `conductor/code_styleguides/python.md` conventions (region blocks, module-level `ProviderError`, vertical compaction)
|
||||
|
||||
**Architecture:** Organizational refactor only — no logic changes. Move `ProviderError` class to module level, add `#region` sections, apply vertical alignment to dense conditionals.
|
||||
|
||||
**Tech Stack:** Python 3.11+, same codebase
|
||||
|
||||
---
|
||||
|
||||
## Pre-Work: Audit Current State
|
||||
|
||||
Before modifying, verify current line ranges and confirm exact locations for edits.
|
||||
|
||||
- [ ] **Step 1: Verify `ProviderError` location**
|
||||
|
||||
Run: `python -c "import src.ai_client; print(src.ai_client.ProviderError.__module__)"`
|
||||
Expected: `src.ai_client` (currently nested but accessible)
|
||||
|
||||
- [ ] **Step 2: Get exact line range of ai_client.py**
|
||||
|
||||
Run: `Get-Content src/ai_client.py | Measure-Object -Line`
|
||||
Expected: ~2522 lines
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Move `ProviderError` to Module Level
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ai_client.py:301-324`
|
||||
|
||||
- [ ] **Step 1: Read current `ProviderError` definition**
|
||||
|
||||
```python
|
||||
# Lines 301-324
|
||||
class ProviderError(Exception):
|
||||
def __init__(self, provider: str, message: str, code: str | None = None):
|
||||
self.provider = provider
|
||||
self.message = message
|
||||
self.code = code
|
||||
|
||||
def ui_message(self) -> str:
|
||||
lines = self.message.split("\n")
|
||||
first = lines[0]
|
||||
prefix = f"[{self.provider.upper()}] "
|
||||
if first.startswith(prefix):
|
||||
return first
|
||||
return f"{prefix}{first}"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Find insertion point (after imports, before first function)**
|
||||
|
||||
Read `src/ai_client.py:1-60` to find where imports end and first function begins (~line 54).
|
||||
|
||||
- [ ] **Step 3: Insert `ProviderError` at module level** (before line 54)
|
||||
|
||||
- [ ] **Step 4: Remove original `ProviderError` class definition** (lines 301-324)
|
||||
|
||||
- [ ] **Step 5: Verify no import changes needed** — class moved within same module
|
||||
|
||||
- [ ] **Step 6: Run syntax check**
|
||||
|
||||
Run: `uv run python -m py_compile src/ai_client.py`
|
||||
Expected: No errors
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ai_client.py
|
||||
git commit -m "refactor(ai_client): Move ProviderError to module level"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add Region Blocks
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ai_client.py:54-2522` (throughout)
|
||||
|
||||
Insert `#region: Name` before first function of each section, `#endregion: Name` after last function.
|
||||
|
||||
Region order (follows logical flow from setup to public API):
|
||||
|
||||
| Order | Region Name | Start Line | End Line |
|
||||
|-------|-------------|------------|----------|
|
||||
| 1 | `#region: Provider Configuration` | after `ProviderError` | before `_get_proxy` |
|
||||
| 2 | `#region: Credentials & Setup` | `_get_proxy` | before `set_model_params` |
|
||||
| 3 | `#region: System Prompt Management` | `set_model_params` | before `get_current_tier` |
|
||||
| 4 | `#region: Thread Context` | `get_current_tier` | before `set_custom_system_prompt` |
|
||||
| 5 | `#region: Comms Log` | `set_custom_system_prompt` section | before `_classify_anthropic_error` |
|
||||
| 6 | `#region: Error Classification` | `_classify_anthropic_error` | before `set_provider` |
|
||||
| 7 | `#region: Provider Lifecycle` | `set_provider` | before `set_agent_tools` |
|
||||
| 8 | `#region: Tool Configuration` | `set_agent_tools` | before `_execute_tool_calls_concurrently` |
|
||||
| 9 | `#region: Tool Execution` | `_execute_tool_calls_concurrently` | before `_reread_file_items` |
|
||||
| 10 | `#region: File Context Building` | `_reread_file_items` | before `_estimate_message_tokens` |
|
||||
| 11 | `#region: Token Estimation` | `_estimate_message_tokens` | before `_send_gemini` |
|
||||
| 12 | `#region: Gemini Provider` | `_send_gemini` | before `_send_anthropic` |
|
||||
| 13 | `#region: Anthropic Provider` | `_send_anthropic` | before `_send_deepseek` |
|
||||
| 14 | `#region: DeepSeek Provider` | `_send_deepseek` | before `_send_minimax` |
|
||||
| 15 | `#region: MiniMax Provider` | `_send_minimax` | before `run_tier4_analysis` |
|
||||
| 16 | `#region: Tier 4 Analysis` | `run_tier4_analysis` | before `get_token_stats` |
|
||||
| 17 | `#region: Session & Public API` | `get_token_stats` | end of file |
|
||||
|
||||
- [ ] **Step 1: For each region, insert opening `#region: Name` on its own line before the first function in that group**
|
||||
|
||||
- [ ] **Step 2: For each region, insert closing `#endregion: Name` on its own line after the last function in that group**
|
||||
|
||||
- [ ] **Step 3: Run syntax check after all insertions**
|
||||
|
||||
Run: `uv run python -m py_compile src/ai_client.py`
|
||||
Expected: No errors
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ai_client.py
|
||||
git commit -m "refactor(ai_client): Add region blocks for organization"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Vertical Compaction Pass
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ai_client.py` (targeted sections)
|
||||
|
||||
- [ ] **Step 1: Identify dense conditionals for alignment**
|
||||
|
||||
Read `_classify_anthropic_error` (lines 326-351), `_classify_gemini_error` (353-377), `_classify_deepseek_error` (379-410), `_classify_minimax_error` (412-441).
|
||||
|
||||
- [ ] **Step 2: Apply vertical alignment to status/color mappings if present**
|
||||
|
||||
Look for patterns like:
|
||||
```python
|
||||
if status == 'running': col = (0.0, 1.0, 0.0, 1.0)
|
||||
elif status == 'starting': col = (1.0, 1.0, 0.0, 1.0)
|
||||
elif status == 'error': col = (1.0, 0.0, 0.0, 1.0)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run syntax check**
|
||||
|
||||
Run: `uv run python -m py_compile src/ai_client.py`
|
||||
Expected: No errors
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ai_client.py
|
||||
git commit -m "refactor(ai_client): Apply vertical compaction alignment"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Verify No Call Sites Broken
|
||||
|
||||
**Files:**
|
||||
- Test: `tests/test_ai_client_concurrency.py`, `tests/test_bias_efficacy.py`, others
|
||||
|
||||
- [ ] **Step 1: Run ai_client-related tests**
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client_concurrency.py tests/test_bias_efficacy.py tests/test_agent_capabilities_list_models.py -v --timeout=60`
|
||||
Expected: All pass
|
||||
|
||||
- [ ] **Step 2: If failures, diagnose and fix inline**
|
||||
|
||||
Do not skip — fix the issue or escalate.
|
||||
|
||||
- [ ] **Step 3: Commit final verification**
|
||||
|
||||
```bash
|
||||
git add src/ai_client.py
|
||||
git commit -m "test(ai_client): Verify no call sites broken after style refactor"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] **Step 1: Run full ai_client test suite**
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client*.py -v --timeout=120` (batch if many files)
|
||||
Expected: All pass
|
||||
|
||||
- [ ] **Step 2: Run lint check**
|
||||
|
||||
Run: `uv run ruff check src/ai_client.py`
|
||||
Expected: No errors (or only pre-existing warnings)
|
||||
|
||||
- [ ] **Step 3: Final commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "conductor(checkpoint): ai_client.py style curation complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Plan complete and saved to `docs/superpowers/plans/2026-05-13-ai-client-style-curation-plan.md`.**
|
||||
|
||||
**Two execution options:**
|
||||
|
||||
**1. Subagent-Driven (recommended)** - Dispatch a fresh subagent per task, review between tasks, fast iteration
|
||||
|
||||
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
||||
|
||||
**Which approach?**
|
||||
@@ -1,592 +0,0 @@
|
||||
# AI Server IPC 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:** Decouple heavy AI SDK imports from GUI via subprocess command queue, achieving ~0.5s instant GUI startup.
|
||||
|
||||
**Architecture:** Subprocess spawns `python -m src.ai_server` which loads google.genai/anthropic. GUI communicates via JSON-RPC over stdin/stdout pipe. Command queue pattern with response matching by UUID.
|
||||
|
||||
**Tech Stack:** Python subprocess, JSON-RPC, threading, queue-based IPC
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### New Files
|
||||
- `src/ai_server.py` - Subprocess AI server (stdin/stdout JSON-RPC)
|
||||
- `src/ai_client_proxy.py` - Queue client for GUI
|
||||
- `tests/test_ai_server.py` - Server tests
|
||||
- `tests/test_ai_client_proxy.py` - Proxy tests
|
||||
|
||||
### Modified Files
|
||||
- `src/ai_client.py` - Add proxy routing when AI_SERVER_ENABLED env var
|
||||
- `sloppy.py` - Set AI_SERVER_ENABLED=1
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create ai_client_proxy.py - Queue Client
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ai_client_proxy.py`
|
||||
- Test: `tests/test_ai_client_proxy.py`
|
||||
|
||||
- [ ] **Step 1: Write failing test for proxy basic operations**
|
||||
|
||||
```python
|
||||
# tests/test_ai_client_proxy.py
|
||||
import pytest
|
||||
from src.ai_client_proxy import AIProxyClient
|
||||
|
||||
def test_proxy_initialization():
|
||||
proxy = AIProxyClient()
|
||||
assert proxy._status == "disconnected"
|
||||
assert proxy._pending == {}
|
||||
|
||||
def test_proxy_status_states():
|
||||
proxy = AIProxyClient()
|
||||
assert proxy.status in ("disconnected", "init", "ready", "busy", "error")
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client_proxy.py -v`
|
||||
Expected: FAIL - module not found
|
||||
|
||||
- [ ] **Step 2: Implement minimal AIProxyClient**
|
||||
|
||||
```python
|
||||
# src/ai_client_proxy.py
|
||||
import json
|
||||
import uuid
|
||||
import threading
|
||||
import subprocess
|
||||
from typing import Any, Optional
|
||||
|
||||
class AIProxyClient:
|
||||
def __init__(self):
|
||||
self._process: Optional[subprocess.Popen] = None
|
||||
self._status: str = "disconnected"
|
||||
self._pending: dict[str, threading.Event] = {}
|
||||
self._reader_thread: Optional[threading.Thread] = None
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def start_server(self):
|
||||
self._process = subprocess.Popen(
|
||||
["python", "-m", "src.ai_server"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
self._status = "init"
|
||||
self._reader_thread = threading.Thread(target=self._read_loop, daemon=True)
|
||||
self._reader_thread.start()
|
||||
|
||||
def _read_loop(self):
|
||||
pass # stub
|
||||
|
||||
def stop(self):
|
||||
if self._process:
|
||||
self._process.terminate()
|
||||
self._process.wait(timeout=5)
|
||||
self._process = None
|
||||
self._status = "disconnected"
|
||||
|
||||
def send_command(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
request_id = str(uuid.uuid4())
|
||||
event = threading.Event()
|
||||
self._pending[request_id] = event
|
||||
command = {"id": request_id, "method": method, "params": params}
|
||||
self._process.stdin.write(json.dumps(command) + "\n")
|
||||
self._process.stdin.flush()
|
||||
event.wait(timeout=60)
|
||||
result = self._pending.pop(request_id, None)
|
||||
return result or {"error": "timeout"}
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client_proxy.py::test_proxy_initialization -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 3: Write test for command/response matching**
|
||||
|
||||
```python
|
||||
# Add to tests/test_ai_client_proxy.py
|
||||
|
||||
def test_send_command_returns_response():
|
||||
proxy = AIProxyClient()
|
||||
proxy._status = "ready"
|
||||
proxy._process = MockPopen()
|
||||
|
||||
response = proxy.send_command("list_models", {"provider": "gemini"})
|
||||
assert "result" in response or "error" in response
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client_proxy.py -v`
|
||||
Expected: FAIL - MockPopen not defined
|
||||
|
||||
- [ ] **Step 4: Implement command/response matching**
|
||||
|
||||
Add to `src/ai_client_proxy.py`:
|
||||
|
||||
```python
|
||||
def _read_loop(self):
|
||||
for line in self._process.stdout:
|
||||
try:
|
||||
response = json.loads(line.strip())
|
||||
rid = response.get("id")
|
||||
if rid in self._pending:
|
||||
self._pending[rid] = response
|
||||
self._pending[rid + "_event"].set()
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
def send_command(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
request_id = str(uuid.uuid4())
|
||||
event = threading.Event()
|
||||
self._pending[request_id] = None
|
||||
self._pending[request_id + "_event"] = event
|
||||
command = {"id": request_id, "method": method, "params": params}
|
||||
self._process.stdin.write(json.dumps(command) + "\n")
|
||||
self._process.stdin.flush()
|
||||
if not event.wait(timeout=60):
|
||||
return {"error": "timeout"}
|
||||
result = self._pending.pop(request_id, {})
|
||||
self._pending.pop(request_id + "_event", None)
|
||||
return result
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client_proxy.py -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Write test for status tracking**
|
||||
|
||||
```python
|
||||
# Add to tests/test_ai_client_proxy.py
|
||||
|
||||
def test_status_reflects_server_state():
|
||||
proxy = AIProxyClient()
|
||||
assert proxy.status == "disconnected"
|
||||
proxy._status = "ready"
|
||||
assert proxy.status == "ready"
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client_proxy.py -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ai_client_proxy.py tests/test_ai_client_proxy.py
|
||||
git commit -m "feat(ai-server): Add AIProxyClient queue communication layer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Create ai_server.py - Subprocess Server
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ai_server.py`
|
||||
- Test: `tests/test_ai_server.py`
|
||||
|
||||
- [ ] **Step 1: Write failing test for server startup**
|
||||
|
||||
```python
|
||||
# tests/test_ai_server.py
|
||||
import pytest
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
def test_server_starts_and_loads():
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "src.ai_server"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
# Server should output ready marker
|
||||
line = proc.stdout.readline()
|
||||
proc.stdin.close()
|
||||
proc.stdout.close()
|
||||
proc.wait(timeout=5)
|
||||
assert "ready" in line.lower() or proc.returncode == 0
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_server.py::test_server_starts_and_loads -v`
|
||||
Expected: FAIL - module not found
|
||||
|
||||
- [ ] **Step 2: Implement minimal ai_server.py**
|
||||
|
||||
```python
|
||||
# src/ai_server.py
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import sys
|
||||
|
||||
def main():
|
||||
# Signal ready
|
||||
print(json.dumps({"type": "ready"}))
|
||||
sys.stdout.flush()
|
||||
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
cmd = json.loads(line.strip())
|
||||
print(json.dumps({"id": cmd.get("id"), "result": {}}))
|
||||
sys.stdout.flush()
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_server.py::test_server_starts_and_loads -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 3: Write test for list_models command**
|
||||
|
||||
```python
|
||||
# Add to tests/test_ai_server.py
|
||||
|
||||
def test_list_models_returns_models():
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "src.ai_server"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
# Read ready
|
||||
proc.stdout.readline()
|
||||
# Send list_models command
|
||||
cmd = json.dumps({"id": "1", "method": "list_models", "params": {"provider": "gemini"}})
|
||||
proc.stdin.write(cmd + "\n")
|
||||
proc.stdin.flush()
|
||||
# Read response
|
||||
resp = proc.stdout.readline()
|
||||
result = json.loads(resp)
|
||||
assert "result" in result or "error" in result
|
||||
proc.stdin.close()
|
||||
proc.stdout.close()
|
||||
proc.wait()
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_server.py::test_list_models_returns_models -v`
|
||||
Expected: FAIL - list_models not implemented
|
||||
|
||||
- [ ] **Step 4: Implement list_models command**
|
||||
|
||||
Update `src/ai_server.py`:
|
||||
|
||||
```python
|
||||
# src/ai_server.py
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import sys
|
||||
|
||||
_PROVIDERS = {
|
||||
"gemini": ["gemini-2.5-flash-lite", "gemini-3-flash-preview"],
|
||||
"anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022"],
|
||||
}
|
||||
|
||||
def handle_command(cmd: dict) -> dict:
|
||||
method = cmd.get("method", "")
|
||||
params = cmd.get("params", {})
|
||||
|
||||
if method == "list_models":
|
||||
provider = params.get("provider", "gemini")
|
||||
return {"id": cmd.get("id"), "result": {"models": _PROVIDERS.get(provider, [])}}
|
||||
|
||||
return {"id": cmd.get("id"), "error": f"Unknown method: {method}"}
|
||||
|
||||
def main():
|
||||
print(json.dumps({"type": "ready"}))
|
||||
sys.stdout.flush()
|
||||
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
cmd = json.loads(line.strip())
|
||||
response = handle_command(cmd)
|
||||
print(json.dumps(response))
|
||||
sys.stdout.flush()
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_server.py::test_list_models_returns_models -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Write test for google.genai loading**
|
||||
|
||||
```python
|
||||
# Add to tests/test_ai_server.py
|
||||
|
||||
def test_server_loads_google_genai():
|
||||
import time
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "src.ai_server"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
start = time.time()
|
||||
ready_line = proc.stdout.readline()
|
||||
elapsed = time.time() - start
|
||||
proc.stdin.close()
|
||||
proc.stdout.close()
|
||||
proc.wait(timeout=10)
|
||||
# Should load in reasonable time (allow up to 5s for SDK)
|
||||
assert elapsed < 5, f"Server took {elapsed}s to start"
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_server.py::test_server_loads_google_genai -v`
|
||||
Expected: FAIL - needs implementation
|
||||
|
||||
- [ ] **Step 6: Implement google.genai loading**
|
||||
|
||||
Update `src/ai_server.py`:
|
||||
|
||||
```python
|
||||
# src/ai_server.py
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
_PROVIDERS = {
|
||||
"gemini": ["gemini-2.5-flash-lite", "gemini-3-flash-preview"],
|
||||
"anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022"],
|
||||
}
|
||||
|
||||
_google_genai = None
|
||||
_anthropic = None
|
||||
|
||||
def _ensure_google_genai():
|
||||
global _google_genai
|
||||
if _google_genai is None:
|
||||
from google import genai
|
||||
_google_genai = genai
|
||||
return _google_genai
|
||||
|
||||
def handle_command(cmd: dict) -> dict:
|
||||
method = cmd.get("method", "")
|
||||
params = cmd.get("params", {})
|
||||
|
||||
if method == "list_models":
|
||||
provider = params.get("provider", "gemini")
|
||||
if provider == "gemini":
|
||||
_ensure_google_genai()
|
||||
return {"id": cmd.get("id"), "result": {"models": _PROVIDERS.get(provider, [])}}
|
||||
|
||||
if method == "send":
|
||||
_ensure_google_genai()
|
||||
return {"id": cmd.get("id"), "result": {"status": "processed"}}
|
||||
|
||||
return {"id": cmd.get("id"), "error": f"Unknown method: {method}"}
|
||||
|
||||
def main():
|
||||
print(json.dumps({"type": "ready"}))
|
||||
sys.stdout.flush()
|
||||
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
cmd = json.loads(line.strip())
|
||||
response = handle_command(cmd)
|
||||
print(json.dumps(response))
|
||||
sys.stdout.flush()
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.stdout.flush()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_server.py::test_server_loads_google_genai -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ai_server.py tests/test_ai_server.py
|
||||
git commit -m "feat(ai-server): Add ai_server subprocess with google.genai lazy loading"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Wire AIProxyClient into ai_client.py
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ai_client.py` (add proxy routing)
|
||||
- Modify: `sloppy.py` (enable AI server)
|
||||
|
||||
- [ ] **Step 1: Write test for proxy integration**
|
||||
|
||||
```python
|
||||
# tests/test_ai_client_integration.py
|
||||
import pytest
|
||||
import os
|
||||
|
||||
def test_ai_client_uses_proxy_when_enabled(monkeypatch):
|
||||
os.environ["AI_SERVER_ENABLED"] = "1"
|
||||
# Import should trigger proxy initialization
|
||||
from src import ai_client
|
||||
assert hasattr(ai_client, "_proxy")
|
||||
assert ai_client._proxy is not None
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client_integration.py -v`
|
||||
Expected: FAIL - AI_SERVER_ENABLED not checked
|
||||
|
||||
- [ ] **Step 2: Add proxy to ai_client.py**
|
||||
|
||||
Modify `src/ai_client.py` - add near top after imports:
|
||||
|
||||
```python
|
||||
# At top of ai_client.py, after other imports
|
||||
_ai_proxy = None
|
||||
|
||||
def _get_proxy():
|
||||
global _ai_proxy
|
||||
if _ai_proxy is None and os.environ.get("AI_SERVER_ENABLED"):
|
||||
from src.ai_client_proxy import AIProxyClient
|
||||
_ai_proxy = AIProxyClient()
|
||||
_ai_proxy.start_server()
|
||||
return _ai_proxy
|
||||
```
|
||||
|
||||
Modify `_list_gemini_models()`:
|
||||
|
||||
```python
|
||||
def _list_gemini_models() -> list[str]:
|
||||
proxy = _get_proxy()
|
||||
if proxy and proxy.status == "ready":
|
||||
result = proxy.send_command("list_models", {"provider": "gemini"})
|
||||
if "result" in result:
|
||||
return result["result"].get("models", [])
|
||||
# Fallback to direct import
|
||||
global _gemini_client
|
||||
_ensure_gemini_client()
|
||||
return [m.name for m in _gemini_client.models.list()]
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Test the integration**
|
||||
|
||||
Run: `uv run pytest tests/test_ai_client_integration.py -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 4: Modify sloppy.py to enable AI server**
|
||||
|
||||
```python
|
||||
# sloppy.py - add near top
|
||||
os.environ["AI_SERVER_ENABLED"] = "1"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ai_client.py sloppy.py tests/test_ai_client_integration.py
|
||||
git commit -m "feat(ai-server): Wire AIProxyClient into ai_client"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add GUI Status Indicator and Panel Tinting
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` - add AI status tracking
|
||||
- Modify: `src/app_controller.py` - track AI server status
|
||||
|
||||
- [ ] **Step 1: Write test for AI status in GUI**
|
||||
|
||||
```python
|
||||
# tests/test_ai_status_gui.py
|
||||
def test_gui_shows_ai_status():
|
||||
from src.gui_2 import App
|
||||
# App should have ai_status property
|
||||
app = App.__new__(App)
|
||||
assert hasattr(app, "ai_status") or "ai_status" in dir(app)
|
||||
```
|
||||
|
||||
Run: `uv run pytest tests/test_ai_status_gui.py -v`
|
||||
Expected: FAIL - ai_status not in App
|
||||
|
||||
- [ ] **Step 2: Add ai_status to App.__init__**
|
||||
|
||||
In `src/gui_2.py` `App.__init__`, add:
|
||||
|
||||
```python
|
||||
self.ai_status = "disconnected" # disconnected, init, ready, busy, error
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add status panel rendering**
|
||||
|
||||
In `src/gui_2.py` `_render_provider_panel()` or similar, add tint indicator:
|
||||
|
||||
```python
|
||||
# At top of panel
|
||||
if getattr(self, 'ai_status', 'ready') != 'ready':
|
||||
imgui.push_style_color(imgui.COLOR_WINDOW_BACKGROUND, 0.5, 0.5, 0.5, 1.0)
|
||||
imgui.text_colored(imgui.Vec4(1, 0.5, 0, 1), "[AI: Initializing...]")
|
||||
# ... rest of panel ...
|
||||
if self.ai_status != 'ready':
|
||||
imgui.pop_style_color()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Test tinted panel**
|
||||
|
||||
Run: `uv run pytest tests/test_ai_status_gui.py -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/gui_2.py tests/test_ai_status_gui.py
|
||||
git commit -m "feat(gui): Add AI status indicator and panel tinting"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: End-to-End Test
|
||||
|
||||
- [ ] **Step 1: Full startup test**
|
||||
|
||||
```bash
|
||||
cd C:\projects\manual_slop
|
||||
timeout 30 uv run python sloppy.py &
|
||||
sleep 3
|
||||
# Check if GUI responds
|
||||
curl http://localhost:8999/status 2>/dev/null || echo "Hook API not ready"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify startup time**
|
||||
|
||||
```powershell
|
||||
Measure-Command { uv run python sloppy.py } | Select TotalSeconds
|
||||
```
|
||||
|
||||
Target: < 2 seconds to GUI visible
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat(ai-server): Complete AI server IPC integration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Startup time**: GUI should appear in < 2 seconds
|
||||
2. **AI panels**: Should show "Initializing..." tint until AI server ready
|
||||
3. **AI functionality**: After ~1.2s, AI panels become active
|
||||
4. **No regressions**: Existing tests pass
|
||||
@@ -1,623 +0,0 @@
|
||||
# Hot Reloader Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement selective, state-preserving hot-reload for `src/gui_2.py` with delegation pattern refactor, manual trigger via `Ctrl+Alt+R` and GUI button, and visual error tint feedback on failure.
|
||||
|
||||
**Architecture:** Separating code into **delegators** (thin `App` wrappers in `gui_2.py`) and **delegation targets** (actual render logic as module-level functions). State preservation via `copy.deepcopy` capture/restore around `importlib.reload`. Error tint overlay on reload failure using ImGui draw list.
|
||||
|
||||
**Tech Stack:** Python `importlib.reload`, `copy.deepcopy`, Dear PyGui / ImGui draw lists, `traceback`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
|------|----------------|
|
||||
| `src/hot_reloader.py` | New — `HotModule` dataclass and `HotReloader` class with registry, capture/restore, reload API |
|
||||
| `src/gui_2.py` | Refactor render methods from `App._render_xxx` to module-level `render_xxx(app)` |
|
||||
| `src/app_controller.py` | Delegation wrappers — `App._render_xxx` becomes `import src.gui_2 as g2; g2.render_xxx(self)` |
|
||||
| `tests/test_hot_reloader.py` | Unit tests for `HotModule`, `HotReloader` registry and reload |
|
||||
| `tests/test_hot_reload_integration.py` | Integration tests via `live_gui` fixture |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create `src/hot_reloader.py` — HotModule Dataclass and HotReloader Core
|
||||
|
||||
**Files:**
|
||||
- Create: `src/hot_reloader.py`
|
||||
- Test: `tests/test_hot_reloader.py`
|
||||
|
||||
- [ ] **Step 1: Write failing test for HotModule dataclass**
|
||||
|
||||
```python
|
||||
# tests/test_hot_reloader.py
|
||||
import pytest
|
||||
from dataclasses import dataclass
|
||||
from src.hot_reloader import HotModule, HotReloader
|
||||
|
||||
def test_hot_module_dataclass_fields():
|
||||
hm = HotModule(
|
||||
name="test_module",
|
||||
file_path="/path/to/test_module.py",
|
||||
state_keys=["attr1", "attr2"],
|
||||
delegation_targets=["method_a", "method_b"],
|
||||
)
|
||||
assert hm.name == "test_module"
|
||||
assert hm.file_path == "/path/to/test_module.py"
|
||||
assert hm.state_keys == ["attr1", "attr2"]
|
||||
assert hm.delegation_targets == ["method_a", "method_b"]
|
||||
|
||||
def test_hot_reloader_register_and_get():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
hm = HotModule(name="test_mod", file_path="/fake/path.py", state_keys=[], delegation_targets=[])
|
||||
HotReloader.register(hm)
|
||||
assert "test_mod" in HotReloader.HOT_MODULES
|
||||
assert HotReloader.HOT_MODULES["test_mod"] is hm
|
||||
|
||||
def test_hot_reloader_register_duplicate_raises():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
hm1 = HotModule(name="dup", file_path="/a.py", state_keys=[], delegation_targets=[])
|
||||
HotReloader.register(hm1)
|
||||
with pytest.raises(ValueError, match="already registered"):
|
||||
HotReloader.register(hm1)
|
||||
|
||||
def test_hot_reloader_is_error_state():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
HotReloader.last_error = None
|
||||
HotReloader.is_error_state = False
|
||||
assert HotReloader.is_error_state is False
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py -v`
|
||||
Expected: FAIL — `src/hot_reloader.py` does not exist
|
||||
|
||||
- [ ] **Step 3: Write minimal HotModule dataclass and HotReloader skeleton**
|
||||
|
||||
```python
|
||||
# src/hot_reloader.py
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
@dataclass
|
||||
class HotModule:
|
||||
name: str
|
||||
file_path: str
|
||||
state_keys: list[str] = field(default_factory=list)
|
||||
delegation_targets: list[str] = field(default_factory=list)
|
||||
|
||||
class HotReloader:
|
||||
HOT_MODULES: dict[str, HotModule] = {}
|
||||
last_error: str | None = None
|
||||
is_error_state: bool = False
|
||||
|
||||
@classmethod
|
||||
def register(cls, module: HotModule) -> None:
|
||||
if module.name in cls.HOT_MODULES:
|
||||
raise ValueError(f"Module {module.name} already registered")
|
||||
cls.HOT_MODULES[module.name] = module
|
||||
|
||||
@classmethod
|
||||
def capture_state(cls, app: Any, state_keys: list[str]) -> dict[str, Any]:
|
||||
return {key: copy.deepcopy(getattr(app, key, None)) for key in state_keys if hasattr(app, key)}
|
||||
|
||||
@classmethod
|
||||
def restore_state(cls, app: Any, state: dict[str, Any]) -> None:
|
||||
for key, value in state.items():
|
||||
setattr(app, key, value)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hot_reloader.py tests/test_hot_reloader.py
|
||||
git commit -m "feat(hot-reload): Add HotModule dataclass and HotReloader registry"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Implement `HotReloader.reload()` and `HotReloader.reload_all()`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/hot_reloader.py`
|
||||
- Test: `tests/test_hot_reloader.py`
|
||||
|
||||
- [ ] **Step 1: Write failing test for reload method**
|
||||
|
||||
```python
|
||||
# tests/test_hot_reloader.py (add these tests)
|
||||
import importlib
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
def test_reload_unknown_module_returns_false():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
HotReloader.register(HotModule(name="nonexistent_mod", file_path="/nonexistent.py", state_keys=[], delegation_targets=[]))
|
||||
app = MagicMock()
|
||||
result = HotReloader.reload("nonexistent_mod", app)
|
||||
assert result is False
|
||||
assert HotReloader.is_error_state is True
|
||||
assert HotReloader.last_error is not None
|
||||
|
||||
def test_reload_success_clears_error_state():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
# Create a minimal test module that can be reloaded
|
||||
import sys
|
||||
import types
|
||||
test_mod = types.ModuleType("src._test_reload_mod")
|
||||
sys.modules["src._test_reload_mod"] = test_mod
|
||||
HotReloader.register(HotModule(name="src._test_reload_mod", file_path=str(Path(__file__).parent.parent / "src" / "_test_reload_mod.py"), state_keys=[], delegation_targets=[]))
|
||||
app = MagicMock()
|
||||
result = HotReloader.reload("src._test_reload_mod", app)
|
||||
assert result is True
|
||||
assert HotReloader.is_error_state is False
|
||||
assert HotReloader.last_error is None
|
||||
del sys.modules["src._test_reload_mod"]
|
||||
|
||||
def test_reload_captures_and_restores_state_on_failure():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
HotReloader.register(HotModule(name="bad_mod", file_path="/bad.py", state_keys=["_test_attr"], delegation_targets=[]))
|
||||
app = MagicMock()
|
||||
app._test_attr = "preserved_value"
|
||||
result = HotReloader.reload("bad_mod", app)
|
||||
assert result is False
|
||||
assert HotReloader.is_error_state is True
|
||||
# State should be restored even on failure
|
||||
|
||||
def test_reload_all():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
# Register two modules and verify reload_all calls both
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py -v`
|
||||
Expected: FAIL — `reload` method not implemented
|
||||
|
||||
- [ ] **Step 3: Write `reload()` and `reload_all()` implementations**
|
||||
|
||||
```python
|
||||
# src/hot_reloader.py (add after existing methods)
|
||||
@classmethod
|
||||
def reload(cls, module_name: str, app: Any) -> bool:
|
||||
if module_name not in cls.HOT_MODULES:
|
||||
cls.last_error = f"Module {module_name} not registered"
|
||||
cls.is_error_state = True
|
||||
return False
|
||||
|
||||
hm = cls.HOT_MODULES[module_name]
|
||||
state = cls.capture_state(app, hm.state_keys)
|
||||
|
||||
try:
|
||||
import importlib
|
||||
import sys
|
||||
if module_name in sys.modules:
|
||||
old_module = sys.modules[module_name]
|
||||
importlib.reload(old_module)
|
||||
else:
|
||||
importlib.import_module(module_name)
|
||||
cls.last_error = None
|
||||
cls.is_error_state = False
|
||||
return True
|
||||
except Exception:
|
||||
cls.restore_state(app, state)
|
||||
cls.last_error = traceback.format_exc()
|
||||
cls.is_error_state = True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def reload_all(cls, app: Any) -> bool:
|
||||
success = True
|
||||
for name in cls.HOT_MODULES:
|
||||
if not cls.reload(name, app):
|
||||
success = False
|
||||
return success
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hot_reloader.py tests/test_hot_reloader.py
|
||||
git commit -m "feat(hot-reload): Implement HotReloader.reload and reload_all"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Register `src.gui_2` with state_keys and delegation_targets
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app_controller.py` — add `HotModule` registration
|
||||
- Test: `tests/test_hot_reloader.py`
|
||||
|
||||
- [ ] **Step 1: Write test for src.gui_2 registration**
|
||||
|
||||
```python
|
||||
def test_gui_2_module_registered():
|
||||
from src.hot_reloader import HotReloader
|
||||
assert "src.gui_2" in HotReloader.HOT_MODULES
|
||||
hm = HotReloader.HOT_MODULES["src.gui_2"]
|
||||
assert len(hm.state_keys) > 0
|
||||
assert len(hm.delegation_targets) > 0
|
||||
assert "_render_main_interface" in hm.delegation_targets
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py::test_gui_2_module_registered -v`
|
||||
Expected: FAIL — registration not yet added
|
||||
|
||||
- [ ] **Step 3: Add src.gui_2 HotModule registration in app_controller.py**
|
||||
|
||||
Find the end of `AppController.__init__` or add a `_register_hot_modules()` function called at end of `__init__`:
|
||||
|
||||
```python
|
||||
# In src/app_controller.py, near end of AppController.__init__ or as standalone:
|
||||
from src.hot_reloader import HotReloader, HotModule
|
||||
from pathlib import Path
|
||||
|
||||
def _register_hot_modules() -> None:
|
||||
HotReloader.register(HotModule(
|
||||
name="src.gui_2",
|
||||
file_path=str(Path(__file__).parent / "gui_2.py"),
|
||||
state_keys=[
|
||||
"_active_discussion", "_disc_entries", "_disc_roles",
|
||||
"show_windows", "ui_discussion_split_h", "active_tickets",
|
||||
"show_preset_manager_window", "show_tool_preset_manager_window",
|
||||
"show_persona_editor_window", "_pending_patch",
|
||||
"_tool_log", "_comms_log",
|
||||
],
|
||||
delegation_targets=[
|
||||
"_render_main_interface", "_render_discussion_hub",
|
||||
"_render_discussion_panel", "_render_discussion_selector",
|
||||
"_render_discussion_entries", "_render_discussion_entry",
|
||||
"_render_files_and_media", "_render_files_panel",
|
||||
"_render_screenshots_panel", "_render_context_composition_panel",
|
||||
"_render_ai_settings_hub", "_render_provider_panel",
|
||||
"_render_persona_selector_panel", "_render_agent_tools_panel",
|
||||
"_render_rag_panel", "_render_system_prompts_panel",
|
||||
"_render_preset_manager_content", "_render_persona_editor_window",
|
||||
"_render_tool_preset_manager_content",
|
||||
"_render_operations_hub", "_render_tool_calls_panel",
|
||||
"_render_comms_history_panel", "_render_mma_dashboard",
|
||||
"_render_mma_modals", "_render_ticket_queue",
|
||||
"_render_task_dag_panel", "_render_approve_script_modal",
|
||||
"_render_patch_modal",
|
||||
],
|
||||
))
|
||||
```
|
||||
|
||||
Call `_register_hot_modules()` at the end of `AppController.__init__` after all state is initialized.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py::test_gui_2_module_registered -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app_controller.py
|
||||
git commit -m "feat(hot-reload): Register src.gui_2 with state_keys and delegation_targets"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Phase 1 — Refactor gui_2 render methods to module-level functions
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` — extract `App._render_xxx` to `render_xxx(app)`
|
||||
- Modify: `src/app_controller.py` — delegation wrappers
|
||||
- Test: `tests/test_hot_reloader.py`
|
||||
|
||||
**Refactor Pattern** (for each render method):
|
||||
|
||||
```python
|
||||
# BEFORE (in App class):
|
||||
def _render_main_interface(self) -> None:
|
||||
if self.perf_profiling_enabled:
|
||||
self.perf_monitor.start_component("_render_main_interface")
|
||||
with imscope.window("Main Interface", self.show_windows.get("main_interface", True)) as (exp, opened):
|
||||
if not exp: return
|
||||
# ... all render logic
|
||||
|
||||
# AFTER — App method becomes thin delegation wrapper:
|
||||
def _render_main_interface(self) -> None:
|
||||
import src.gui_2 as g2
|
||||
g2.render_main_interface(self)
|
||||
|
||||
# NEW module-level function:
|
||||
def render_main_interface(app: App) -> None:
|
||||
if app.perf_profiling_enabled:
|
||||
app.perf_monitor.start_component("_render_main_interface")
|
||||
with imscope.window("Main Interface", app.show_windows.get("main_interface", True)) as (exp, opened):
|
||||
if not exp: return
|
||||
# ... same render logic, replacing self with app
|
||||
```
|
||||
|
||||
**Methods to refactor** (in order):
|
||||
1. `_render_main_interface` (lines 769-857)
|
||||
2. `_render_discussion_hub` (lines 3235-3245)
|
||||
3. `_render_discussion_panel` (lines 3395-3415)
|
||||
4. `_render_discussion_selector` (lines 3430-3473)
|
||||
5. `_render_discussion_entries` (lines 3247-3259)
|
||||
6. `_render_discussion_entry` (lines 3261-3304)
|
||||
7. `_render_files_and_media` (lines 2529-2636)
|
||||
8. `_render_files_panel` (lines 2638-2716)
|
||||
9. `_render_screenshots_panel` (lines 2718-2750)
|
||||
10. `_render_context_composition_panel` (lines 2752-2765)
|
||||
11. `_render_ai_settings_hub` (lines 1754-1759)
|
||||
12. `_render_provider_panel` (lines 2377-2441)
|
||||
13. `_render_persona_selector_panel` (lines 2443-2523)
|
||||
14. `_render_agent_tools_panel` (lines 1850-1913)
|
||||
15. `_render_rag_panel` (lines 1761-1796)
|
||||
16. `_render_system_prompts_panel` (lines 1798-1848)
|
||||
17. `_render_preset_manager_content` (lines 1915-2002)
|
||||
18. `_render_persona_editor_window` (lines 2205-2375)
|
||||
19. `_render_tool_preset_manager_content` (lines 2014-2193)
|
||||
20. `_render_operations_hub` (lines 3750-3788)
|
||||
21. `_render_tool_calls_panel` (lines 3790-3853)
|
||||
22. `_render_comms_history_panel` (lines 3855-3971)
|
||||
23. `_render_mma_dashboard` (lines 4664-4695)
|
||||
24. `_render_mma_modals` (lines 4711-4785)
|
||||
25. `_render_ticket_queue` (lines 4365-4478)
|
||||
26. `_render_task_dag_panel` (lines 4480-4621)
|
||||
27. `_render_approve_script_modal` (lines 4269-4310)
|
||||
28. `_render_patch_modal` (lines 4135-4177)
|
||||
|
||||
- [ ] **Step 1: Write test for module-level function pattern**
|
||||
|
||||
```python
|
||||
def test_gui2_has_module_level_render_functions():
|
||||
import src.gui_2 as g2
|
||||
# After refactor, these should exist as module-level functions
|
||||
assert hasattr(g2, 'render_main_interface')
|
||||
assert hasattr(g2, 'render_discussion_hub')
|
||||
assert hasattr(g2, 'render_discussion_panel')
|
||||
# ... etc
|
||||
# Each should accept app: App as first parameter
|
||||
import inspect
|
||||
sig = inspect.signature(g2.render_main_interface)
|
||||
assert 'app' in sig.parameters
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py::test_gui2_has_module_level_render_functions -v`
|
||||
Expected: FAIL — module-level functions don't exist yet
|
||||
|
||||
- [ ] **Step 3: Refactor `_render_main_interface` first as pilot**
|
||||
|
||||
Extract the method body, replace `self` with `app`, create module-level `render_main_interface(app: App) -> None`, and convert `App._render_main_interface` to delegation wrapper.
|
||||
|
||||
- [ ] **Step 4: Run test after each method refactor**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py::test_gui2_has_module_level_render_functions -v`
|
||||
Continue until all module-level functions exist.
|
||||
|
||||
- [ ] **Step 5: Commit after completing all methods**
|
||||
|
||||
```bash
|
||||
git add src/gui_2.py src/app_controller.py
|
||||
git commit -m "refactor(hot-reload): Extract gui_2 render methods to module-level functions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Add `Ctrl+Alt+R` trigger and GUI Hot Reload button
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py:App.run` — capture keyboard shortcut
|
||||
- Modify: `src/gui_2.py:_render_mma_dashboard` — add Hot Reload button
|
||||
- Modify: `src/gui_2.py` — add `_trigger_hot_reload` and `_hot_reload_error` state
|
||||
- Test: `tests/test_hot_reload_integration.py`
|
||||
|
||||
- [ ] **Step 1: Write test for hot reload trigger**
|
||||
|
||||
```python
|
||||
def test_ctrl_alt_r_triggers_hot_reload(live_gui):
|
||||
client = ApiHookClient(port=8999)
|
||||
# Verify initial state
|
||||
initial_state = client.get_value("_hot_reload_error")
|
||||
# Note: Direct keyboard simulation via hooks not yet implemented
|
||||
# Test will verify button-based trigger
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write test for Hot Reload button in MMA dashboard**
|
||||
|
||||
```python
|
||||
def test_hot_reload_button_in_mma_dashboard(live_gui):
|
||||
client = ApiHookClient(port=8999)
|
||||
# Verify _render_mma_dashboard has a Hot Reload button
|
||||
# via exposed component state or via behavior
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add `_hot_reload_error` state to App.__init__**
|
||||
|
||||
In `src/gui_2.py:App.__init__`, add:
|
||||
|
||||
```python
|
||||
self._hot_reload_error = False
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add `_trigger_hot_reload()` method to App**
|
||||
|
||||
```python
|
||||
def _trigger_hot_reload(self) -> None:
|
||||
from src.hot_reloader import HotReloader
|
||||
success = HotReloader.reload_all(self)
|
||||
self._hot_reload_error = not success
|
||||
|
||||
def _render_hot_reload_button(self) -> None:
|
||||
imgui.push_style_color(imgui.Col_.button, imgui.get_color_u32_rgba(0.2, 0.2, 0.2, 1.0))
|
||||
if imgui.button("Hot Reload"):
|
||||
self._trigger_hot_reload()
|
||||
imgui.pop_style_color()
|
||||
if self._hot_reload_error:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(imgui.get_color_u32_rgba(1.0, 0.3, 0.2, 1.0), "Reload failed")
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add keyboard capture in `App.run` main loop**
|
||||
|
||||
In `src/gui_2.py:App.run`, inside the main `while running:` loop:
|
||||
|
||||
```python
|
||||
# Check for Ctrl+Alt+R
|
||||
io = imgui.get_io()
|
||||
if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.KEY_R):
|
||||
self._trigger_hot_reload()
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Integrate button into `_render_mma_global_controls` or `_render_mma_dashboard`**
|
||||
|
||||
Add `self._render_hot_reload_button()` call in `_render_mma_dashboard` or a nearby section.
|
||||
|
||||
- [ ] **Step 7: Run tests**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=60`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/gui_2.py src/app_controller.py
|
||||
git commit -m "feat(hot-reload): Add Ctrl+Alt+R trigger and GUI Hot Reload button"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Implement visual error tint on reload failure
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` — add `_render_error_tint()` and call from `render_main_interface`
|
||||
- Test: `tests/test_hot_reload_integration.py`
|
||||
|
||||
- [ ] **Step 1: Write test for error tint overlay**
|
||||
|
||||
```python
|
||||
def test_error_tint_renders_on_failure(live_gui):
|
||||
# Inject a failure state and verify tint appears
|
||||
pass
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `_render_error_tint(app: App)` function**
|
||||
|
||||
```python
|
||||
def _render_error_tint(app: App) -> None:
|
||||
if not getattr(app, '_hot_reload_error', False):
|
||||
return
|
||||
# NERV theme error tint
|
||||
tint_color = imgui.get_color_u32_rgba(1.0, 0.28, 0.25, 0.15)
|
||||
draw_list = imgui.get_background_draw_list()
|
||||
viewport = imgui.get_main_viewport()
|
||||
draw_list.add_rect_filled(
|
||||
viewport.pos,
|
||||
viewport.pos + viewport.size,
|
||||
tint_color,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Call `_render_error_tint(app)` at end of `render_main_interface`**
|
||||
|
||||
Add at the very start of `render_main_interface` since it's an overlay:
|
||||
|
||||
```python
|
||||
def render_main_interface(app: App) -> None:
|
||||
_render_error_tint(app)
|
||||
if app.perf_profiling_enabled:
|
||||
# ...
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=60`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/gui_2.py
|
||||
git commit -m "feat(hot-reload): Add visual error tint on reload failure"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: End-to-End Integration Test
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_hot_reload_integration.py`
|
||||
- Test: `tests/test_hot_reload_integration.py`
|
||||
|
||||
- [ ] **Step 1: Write e2e tests using live_gui fixture**
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from api_hook_client import ApiHookClient
|
||||
from simulation.sim_base import BaseSimulation, run_sim
|
||||
|
||||
class HotReloadSimulation(BaseSimulation):
|
||||
def run(self) -> None:
|
||||
# 1. Verify hot reload button is accessible
|
||||
# 2. Trigger hot reload via API hook
|
||||
# 3. Verify GUI still renders after reload
|
||||
# 4. Verify error tint clears on success
|
||||
pass
|
||||
|
||||
def test_hot_reload_e2e(live_gui):
|
||||
client = ApiHookClient(port=8999)
|
||||
# Trigger hot reload
|
||||
response = client.post("/api/gui", {"action": "hot_reload"})
|
||||
# Verify GUI still responds
|
||||
state = client.get("/api/gui_state")
|
||||
assert state is not None
|
||||
|
||||
def test_hot_reload_preserves_state(live_gui):
|
||||
client = ApiHookClient(port=8999)
|
||||
# Set some state
|
||||
# Trigger reload
|
||||
# Verify state preserved
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run e2e tests**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=120`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/test_hot_reload_integration.py
|
||||
git commit -m "test(hot-reload): Add e2e integration test"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria Verification
|
||||
|
||||
1. Pressing `Ctrl+Alt+R` reloads `src.gui_2` without losing App state — tested via manual trigger + state verification
|
||||
2. GUI "Hot Reload" button triggers same reload — tested via button click in live_gui
|
||||
3. Failed reload shows error tint, preserves last-good state — tested via mock failure injection
|
||||
4. New hot modules can be added via `HotReloader.register()` without modifying core — tested in unit tests
|
||||
5. No performance impact when not reloading (< 1ms overhead per frame) — verified via perf profiling
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (Per Spec)
|
||||
|
||||
- Automatic file watching (manual trigger only)
|
||||
- Hot-reloading of C extensions or native code
|
||||
- Cross-platform reload support (Windows focus)
|
||||
@@ -1,442 +0,0 @@
|
||||
# OpenCode Agent Definition Fix 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:** Fix OpenCode agent definition files so subagents spawned via @mention follow conductor workflow (1-space indentation, TDD, checkpoints, atomic commits)
|
||||
|
||||
**Architecture:** Rewrite `.opencode/agents/tier3-worker.md` and `.opencode/agents/tier4-qa.md` to add CRITICAL enforcement blocks, TDD phase structure, and pre-delegation checkpoint protocols. Tier 2 and Tier 1 agents are secondary targets.
|
||||
|
||||
**Tech Stack:** OpenCode agent definitions (Markdown with YAML frontmatter), Manual Slop MCP tools
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Modify:** `.opencode/agents/tier3-worker.md` - Add enforcement blocks, TDD structure
|
||||
- **Modify:** `.opencode/agents/tier4-qa.md` - Add architecture reference, strengthen analysis format
|
||||
- **Modify:** `.opencode/agents/tier2-tech-lead.md` - Add delegation enforcement, phase completion protocol
|
||||
- **Modify:** `.opencode/agents/tier1-orchestrator.md` - Add track initialization workflow
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Fix Tier 3 Worker Agent Definition
|
||||
|
||||
### Task 1.1: Add CRITICAL Indentation Block
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier3-worker.md`
|
||||
|
||||
- [ ] **Step 1: Read current tier3-worker.md header section**
|
||||
|
||||
Read `.opencode/agents/tier3-worker.md` lines 1-50 to identify insertion point after frontmatter.
|
||||
|
||||
- [ ] **Step 2: Add CRITICAL indentation block after "STRICT SYSTEM DIRECTIVE"**
|
||||
|
||||
Insert this block immediately after the STRICT SYSTEM DIRECTIVE line:
|
||||
|
||||
```markdown
|
||||
## CRITICAL: 1-Space Indentation for Python
|
||||
|
||||
**ALL Python code MUST use exactly 1 (ONE) space for indentation.**
|
||||
|
||||
VIOLATIONS:
|
||||
- Using 4 spaces or tabs will corrupt the codebase
|
||||
- Native edit tools destroy 1-space indentation - use MCP tools ONLY
|
||||
|
||||
MCP Edit Tools (SAFE):
|
||||
- `manual-slop_edit_file` - find/replace, preserves indentation
|
||||
- `manual-slop_py_update_definition` - replace function/class
|
||||
- `manual-slop_set_file_slice` - replace line range
|
||||
|
||||
DO NOT use native `edit` or `write` tools on Python files.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier3-worker.md
|
||||
git commit -m "fix(agents): Add CRITICAL 1-space indentation block to tier3-worker"
|
||||
```
|
||||
|
||||
### Task 1.2: Add Pre-Delegation Checkpoint Protocol
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier3-worker.md` (same file, different section)
|
||||
|
||||
- [ ] **Step 1: Read tier3-worker.md around "Task Start Checklist"**
|
||||
|
||||
Read lines 45-80 to find the Task Start Checklist section.
|
||||
|
||||
- [ ] **Step 2: Add Pre-Delegation Checkpoint Protocol before Task Start Checklist**
|
||||
|
||||
Insert this section:
|
||||
|
||||
```markdown
|
||||
## Pre-Delegation Checkpoint Protocol (MANDATORY)
|
||||
|
||||
Before implementing ANY code change:
|
||||
|
||||
1. **Stage your work:** `manual-slop_run_powershell` with `git add .`
|
||||
2. **Why:** Prevents work loss if the implementation fails or needs rollback
|
||||
3. **When:** Always - before touching any file that matters
|
||||
|
||||
This is NOT optional. It is the difference between recoverable and catastrophic failure.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier3-worker.md
|
||||
git commit -m "fix(agents): Add pre-delegation checkpoint protocol to tier3-worker"
|
||||
```
|
||||
|
||||
### Task 1.3: Add TDD Phase Enforcement
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier3-worker.md` (same file, different section)
|
||||
|
||||
- [ ] **Step 1: Read tier3-worker.md around "Task Execution Protocol"**
|
||||
|
||||
Read lines 60-120 to find the Task Execution Protocol section.
|
||||
|
||||
- [ ] **Step 2: Enhance Task Execution Protocol with TDD Phase Structure**
|
||||
|
||||
Replace the existing "Task Execution Protocol" section with:
|
||||
|
||||
```markdown
|
||||
## Task Execution Protocol (MANDATORY TDD)
|
||||
|
||||
### Phase 1: RED - Write Failing Test
|
||||
- Write a test that defines the expected behavior
|
||||
- Run: `manual-slop_run_powershell` with `uv run pytest tests/path/test.py -v`
|
||||
- Confirm: Test MUST fail before proceeding
|
||||
- DO NOT skip this phase
|
||||
|
||||
### Phase 2: GREEN - Implement to Pass
|
||||
- Implement the minimal code to make the test pass
|
||||
- Run tests again
|
||||
- Confirm: Test MUST pass
|
||||
- DO NOT skip this phase
|
||||
|
||||
### Phase 3: REFACTOR - Optional
|
||||
- With passing tests, improve code quality
|
||||
- DO NOT change behavior
|
||||
- Re-run tests to confirm still passing
|
||||
|
||||
### Commit Protocol (ATOMIC PER TASK)
|
||||
After each task completion:
|
||||
1. `manual-slop_run_powershell` with `git add .`
|
||||
2. `git commit -m "feat(scope): description"`
|
||||
3. DO NOT batch commits across tasks
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier3-worker.md
|
||||
git commit -m "fix(agents): Add TDD phase enforcement to tier3-worker"
|
||||
```
|
||||
|
||||
### Task 1.4: Add BLOCKED Protocol
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier3-worker.md` (same file, different section)
|
||||
|
||||
- [ ] **Step 1: Read tier3-worker.md end of file**
|
||||
|
||||
Read lines 120-150 to find where to insert BLOCKED protocol.
|
||||
|
||||
- [ ] **Step 2: Add BLOCKED Protocol section at end of file**
|
||||
|
||||
Append before the Limitations section:
|
||||
|
||||
```markdown
|
||||
## BLOCKED Protocol
|
||||
|
||||
If you cannot complete the task:
|
||||
|
||||
1. Start your response with: `BLOCKED:`
|
||||
2. Explain exactly why you cannot proceed
|
||||
3. List what information or changes would unblock you
|
||||
4. DO NOT attempt partial implementations that break the build
|
||||
|
||||
Examples of BLOCKED conditions:
|
||||
- Missing required context about the codebase
|
||||
- Task requires architectural decisions not in the spec
|
||||
- Target file/line range does not exist as described
|
||||
- Cyclic dependency discovered that wasn't documented
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier3-worker.md
|
||||
git commit -m "fix(agents): Add BLOCKED protocol to tier3-worker"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Fix Tier 4 QA Agent Definition
|
||||
|
||||
### Task 2.1: Add Architecture Reference to Tier 4 QA
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier4-qa.md`
|
||||
|
||||
- [ ] **Step 1: Read current tier4-qa.md**
|
||||
|
||||
Read `.opencode/agents/tier4-qa.md` lines 1-80 to understand current structure.
|
||||
|
||||
- [ ] **Step 2: Add Architecture Reference section after "Context Amnesia"**
|
||||
|
||||
Insert after Context Amnesia section:
|
||||
|
||||
```markdown
|
||||
## Architecture Reference
|
||||
|
||||
When analyzing errors, trace data flow through thread domains documented in:
|
||||
- `docs/guide_architecture.md`: Thread domains, event system, AI client, HITL mechanism
|
||||
- `docs/guide_mma.md`: 4-tier orchestration, DAG engine, worker lifecycle
|
||||
|
||||
Key threading model:
|
||||
- GUI main thread: UI rendering only
|
||||
- asyncio worker thread: AI communication
|
||||
- HookServer thread: API hook handling
|
||||
- NEVER write GUI state from background threads
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier4-qa.md
|
||||
git commit -m "fix(agents): Add architecture reference to tier4-qa"
|
||||
```
|
||||
|
||||
### Task 2.2: Strengthen Tier 4 QA Analysis Format
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier4-qa.md`
|
||||
|
||||
- [ ] **Step 1: Read tier4-qa.md around "Root Cause Analysis"**
|
||||
|
||||
Read lines 60-100 to find the Root Cause Analysis section.
|
||||
|
||||
- [ ] **Step 2: Enhance Root Cause Analysis with data flow tracing instructions**
|
||||
|
||||
Replace the existing Analysis Protocol section with enhanced version:
|
||||
|
||||
```markdown
|
||||
## Analysis Protocol
|
||||
|
||||
### 1. Understand the Error
|
||||
- Read the provided error output, test failure, or log carefully
|
||||
- Identify affected files from traceback
|
||||
- Do NOT assume - base analysis on evidence only
|
||||
|
||||
### 2. Investigate
|
||||
Use MCP tools to understand the context:
|
||||
- `manual-slop_read_file` - Read relevant source files
|
||||
- `manual-slop_py_find_usages` - Search for related patterns
|
||||
- `manual-slop_search_files` - Find related files
|
||||
- `manual-slop_get_git_diff` - Check recent changes
|
||||
|
||||
### 3. Root Cause Analysis (MANDATORY FORMAT)
|
||||
|
||||
Provide a structured analysis in this exact format:
|
||||
|
||||
```
|
||||
## Error Analysis
|
||||
|
||||
### Summary
|
||||
[One-sentence description of the error]
|
||||
|
||||
### Root Cause
|
||||
[Detailed explanation of WHY the error occurred - not just what went wrong]
|
||||
|
||||
### Evidence
|
||||
[File:line references supporting the analysis]
|
||||
|
||||
### Data Flow Trace
|
||||
[How data moved through the system to cause this error]
|
||||
[Reference specific thread domains if applicable: GUI main, asyncio worker, HookServer]
|
||||
|
||||
### Impact
|
||||
[What functionality is affected]
|
||||
|
||||
### Recommendations
|
||||
[Suggested fixes - but DO NOT implement them]
|
||||
```
|
||||
|
||||
### 4. DO NOT FIX
|
||||
- Your job is ANALYSIS ONLY
|
||||
- Do NOT modify any files
|
||||
- Do NOT write code
|
||||
- Return the analysis and let the controller decide
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier4-qa.md
|
||||
git commit -m "fix(agents): Strengthen tier4-qa analysis format"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Fix Tier 2 Tech Lead Agent Definition
|
||||
|
||||
### Task 3.1: Add Delegation Enforcement to Tier 2
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier2-tech-lead.md`
|
||||
|
||||
- [ ] **Step 1: Read current tier2-tech-lead.md**
|
||||
|
||||
Read `.opencode/agents/tier2-tech-lead.md` lines 1-100.
|
||||
|
||||
- [ ] **Step 2: Add CRITICAL Delegation Rules after STRICT SYSTEM DIRECTIVE**
|
||||
|
||||
Insert after STRICT SYSTEM DIRECTIVE:
|
||||
|
||||
```markdown
|
||||
## CRITICAL: Delegate Implementation - Do NOT Implement Directly
|
||||
|
||||
You are Tier 2 Tech Lead. Your role is ARCHITECTURE and DELEGATION.
|
||||
|
||||
**NEVER** implement code directly. ALWAYS delegate to Tier 3 via Task tool.
|
||||
|
||||
**Delegation Pattern:**
|
||||
1. Research with skeleton tools
|
||||
2. Draft surgical prompt with WHERE/WHAT/HOW/SAFETY
|
||||
3. Delegate to Tier 3 via Task tool
|
||||
4. Verify result
|
||||
|
||||
**Pre-Delegation Checkpoint:**
|
||||
Before delegating ANY non-trivial change:
|
||||
1. Run: `manual-slop_run_powershell` with `git add .`
|
||||
2. Reason: Prevents work loss if subagent fails
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier2-tech-lead.md
|
||||
git commit -m "fix(agents): Add delegation enforcement to tier2-tech-lead"
|
||||
```
|
||||
|
||||
### Task 3.2: Add Phase Completion Protocol to Tier 2
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier2-tech-lead.md`
|
||||
|
||||
- [ ] **Step 1: Read tier2-tech-lead.md around "TDD Protocol" section**
|
||||
|
||||
Read lines 100-150 to find the TDD Protocol section.
|
||||
|
||||
- [ ] **Step 2: Add Phase Completion Verification Protocol**
|
||||
|
||||
After TDD Protocol section, add:
|
||||
|
||||
```markdown
|
||||
## Phase Completion Protocol
|
||||
|
||||
When all tasks in a phase are complete:
|
||||
|
||||
1. **Run verification:** `/conductor-verify`
|
||||
2. **Present results:** Show user the results
|
||||
3. **Await confirmation:** Wait for user yes/no
|
||||
4. **Create checkpoint:** `git add . && git commit -m "conductor(checkpoint): Phase N complete"`
|
||||
5. **Attach note:** `git notes add -m "<verification report>" <commit_hash>`
|
||||
6. **Update plan:** Mark phase `[x]` with checkpoint SHA in plan.md
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier2-tech-lead.md
|
||||
git commit -m "fix(agents): Add phase completion protocol to tier2-tech-lead"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Fix Tier 1 Orchestrator Agent Definition
|
||||
|
||||
### Task 4.1: Add Track Initialization Workflow
|
||||
|
||||
**Files:**
|
||||
- Modify: `.opencode/agents/tier1-orchestrator.md`
|
||||
|
||||
- [ ] **Step 1: Read current tier1-orchestrator.md**
|
||||
|
||||
Read `.opencode/agents/tier1-orchestrator.md` lines 1-100.
|
||||
|
||||
- [ ] **Step 2: Add Track Initialization Protocol after Session Start Checklist**
|
||||
|
||||
Find the Session Start Checklist section and add after it:
|
||||
|
||||
```markdown
|
||||
## Track Initialization Protocol
|
||||
|
||||
When starting a new track:
|
||||
|
||||
1. **Read track context:**
|
||||
- `conductor/tracks.md` - active tracks
|
||||
- `conductor/tech-stack.md` - technology constraints
|
||||
- `conductor/product.md` - product vision
|
||||
|
||||
2. **Audit existing state:**
|
||||
- Use `manual-slop_py_get_code_outline` to map files
|
||||
- Use `manual-slop_get_git_diff` to check recent changes
|
||||
- Document "Current State Audit" in spec
|
||||
|
||||
3. **Create track spec:**
|
||||
- Follow spec template with: Overview, Current State Audit, Goals, Requirements
|
||||
- Include Architecture Reference section
|
||||
|
||||
4. **Initialize track directory:**
|
||||
- Create `conductor/tracks/{name}_{YYYYMMDD}/`
|
||||
- Write spec.md, plan.md, metadata.json
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .opencode/agents/tier1-orchestrator.md
|
||||
git commit -m "fix(agents): Add track initialization workflow to tier1-orchestrator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After all phases complete:
|
||||
|
||||
- [ ] **Step 1: Verify indentation block in tier3-worker.md**
|
||||
|
||||
Run: `grep -n "1-space" .opencode/agents/tier3-worker.md`
|
||||
Expected: Multiple matches including "CRITICAL"
|
||||
|
||||
- [ ] **Step 2: Verify TDD phase structure**
|
||||
|
||||
Run: `grep -n "RED\|GREEN\|REFACTOR" .opencode/agents/tier3-worker.md`
|
||||
Expected: All three phases present
|
||||
|
||||
- [ ] **Step 3: Verify checkpoint protocol**
|
||||
|
||||
Run: `grep -n "git add \." .opencode/agents/tier3-worker.md`
|
||||
Expected: Pre-delegation checkpoint present
|
||||
|
||||
- [ ] **Step 4: Verify tier4-qa architecture reference**
|
||||
|
||||
Run: `grep -n "guide_architecture" .opencode/agents/tier4-qa.md`
|
||||
Expected: Reference present
|
||||
|
||||
---
|
||||
|
||||
**Plan complete and saved to `docs/superpowers/plans/2026-05-16-opencode-agent-fix-plan.md`. Two execution options:**
|
||||
|
||||
**1. Subagent-Driven (recommended)** - Dispatch a fresh subagent per task, review between tasks, fast iteration
|
||||
|
||||
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
||||
|
||||
**Which approach?**
|
||||
@@ -1,458 +0,0 @@
|
||||
# Agent Config Refresh Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Apply the explicit divergence model to `AGENTS.md`, `CLAUDE.md`, and `GEMINI.md`. `AGENTS.md` becomes a thin pointer document (~1K). `CLAUDE.md` becomes a 1-line deprecation stub. `GEMINI.md` is lightly refreshed to remove any content that should have moved to `AGENTS.md`. Light verification pass (not rewrites) on the skill files in `.agents/`, `.gemini/`, and the legacy `.claude/commands/`.
|
||||
|
||||
**Architecture:** Single-agent sequential execution. Each top-level file is its own task. Skill file verification is a separate task that touches many files but only makes surgical edits where drift is found.
|
||||
|
||||
**Tech Stack:** Manual Slop's MCP research tools. Git for version control.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-comprehensive-documentation-refresh-design.md` §6 (Sub-Track 3).
|
||||
|
||||
**Depends on:** Sub-Track 1 (`docs_layer_refresh_20260602`) must complete first. The human-facing docs are the source of truth that the agent config files point to.
|
||||
|
||||
---
|
||||
|
||||
## Execution Constraints
|
||||
|
||||
These apply to every task in this plan:
|
||||
|
||||
- **No subagents.** Execute as a single agent.
|
||||
- **Pre-edit checkpoint:** Before any file edit, run `git add .`.
|
||||
- **Per-file atomic commits:** One file = one commit. The skill file verification task may have many commits (one per file) since there are many files, but each is still atomic.
|
||||
- **Commit message format:** `docs(<scope>): <imperative description>`.
|
||||
- **Git note format:** Attach a 3-8 line rationale to each commit.
|
||||
- **The "no content duplication" rule:** After this plan completes, a section header that appears in 2+ of `AGENTS.md`/`CLAUDE.md`/`GEMINI.md` must be either (a) a short pointer (≤3 lines) to the canonical home, or (b) a violation to fix.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Rewrite `AGENTS.md` to a Thin Pointer
|
||||
|
||||
**Files:**
|
||||
- Modify: `AGENTS.md` (current size ~5.4K)
|
||||
|
||||
**Target structure:** A thin orientation document of ~1K that:
|
||||
- Describes what Manual Slop is (1-2 sentences)
|
||||
- States the conductor convention (1 paragraph)
|
||||
- Lists the canonical locations for agent guidance (with file links)
|
||||
- Lists the 5-6 most critical project-wide anti-patterns
|
||||
- Points to `docs/Readme.md` for human-facing docs
|
||||
|
||||
**Specific target content (use as a starting template, adapt to current state):**
|
||||
|
||||
```markdown
|
||||
# AGENTS.md
|
||||
|
||||
## What This Is
|
||||
|
||||
Manual Slop is a local GUI orchestrator for LLM-driven coding sessions. It bridges high-latency AI reasoning with a low-latency ImGui render loop via a thread-safe async pipeline; every AI-generated payload passes through a human-auditable gate before execution.
|
||||
|
||||
## The Conductor Convention
|
||||
|
||||
All AI agents consuming this project must read `./conductor/workflow.md` and treat `./conductor/tracks.md` as the task registry. Track implementation follows the TDD protocol documented in `conductor/workflow.md` with per-file atomic commits and git notes.
|
||||
|
||||
## Guidance for AI Agents
|
||||
|
||||
Detailed agent guidance lives in the following locations — read these directly, do not duplicate content here:
|
||||
|
||||
- **Operational workflow:** `conductor/workflow.md`
|
||||
- **Code style and process:** `conductor/product-guidelines.md`
|
||||
- **Tech stack and constraints:** `conductor/tech-stack.md`
|
||||
- **Product context:** `conductor/product.md`
|
||||
- **MMA orchestrator role:** `mma-orchestrator/SKILL.md`
|
||||
- **Tier 1 (Orchestrator):** `.agents/skills/mma-tier1-orchestrator/SKILL.md`
|
||||
- **Tier 2 (Tech Lead):** `.agents/skills/mma-tier2-tech-lead/SKILL.md`
|
||||
- **Tier 3 (Worker):** `.agents/skills/mma-tier3-worker/SKILL.md`
|
||||
- **Tier 4 (QA):** `.agents/skills/mma-tier4-qa/SKILL.md`
|
||||
|
||||
## Human-Facing Documentation
|
||||
|
||||
For understanding, using, and maintaining the tool, see `docs/Readme.md` and the guides it indexes.
|
||||
|
||||
## Critical Anti-Patterns
|
||||
|
||||
- Do not read full files >50 lines without first using `py_get_skeleton` or `get_file_summary`
|
||||
- Do not modify the tech stack without updating `conductor/tech-stack.md` first
|
||||
- Do not implement code directly as Tier 2 — delegate to Tier 3 workers (or in the case of this docs track, do it yourself as a single agent)
|
||||
- Do not skip TDD — write failing tests before implementation
|
||||
- Do not batch commits — commit per-task for atomic rollback
|
||||
- Do not add comments to source code; documentation lives in `/docs`
|
||||
```
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Read the current `AGENTS.md`**
|
||||
|
||||
Use `manual-slop_read_file` (the file is small enough to read in full).
|
||||
|
||||
- [ ] **Step 1.3: Inventory what content currently lives in `AGENTS.md`**
|
||||
|
||||
For each major section in the current file, note:
|
||||
- **Keep here:** Project-level orientation, anti-patterns, the conductor convention
|
||||
- **Move to `conductor/workflow.md`:** Workflow details
|
||||
- **Move to `conductor/product-guidelines.md`:** Style rules
|
||||
- **Move to `conductor/tech-stack.md`:** Tech stack details
|
||||
- **Move to `conductor/product.md`:** Feature descriptions
|
||||
- **Move to `mma-orchestrator/SKILL.md`:** MMA role details
|
||||
- **Move to the relevant `.agents/skills/*/SKILL.md`:** Tier-specific guidance
|
||||
- **Move to `docs/Readme.md`:** Anything that documents the tool for humans (not agents)
|
||||
|
||||
- [ ] **Step 1.4: Verify the target skill files exist and have the content**
|
||||
|
||||
Before writing `AGENTS.md`, confirm:
|
||||
- `mma-orchestrator/SKILL.md` exists
|
||||
- `.agents/skills/mma-tier1-orchestrator/SKILL.md` exists
|
||||
- `.agents/skills/mma-tier2-tech-lead/SKILL.md` exists
|
||||
- `.agents/skills/mma-tier3-worker/SKILL.md` exists
|
||||
- `.agents/skills/mma-tier4-qa/SKILL.md` exists
|
||||
- `conductor/workflow.md` exists
|
||||
- `conductor/product-guidelines.md` exists
|
||||
- `conductor/tech-stack.md` exists
|
||||
- `conductor/product.md` exists
|
||||
- `docs/Readme.md` exists (and is updated per Sub-Track 1)
|
||||
|
||||
If any are missing, do NOT write `AGENTS.md` until they exist. (They should exist by the time Sub-Track 3 runs, since Sub-Track 1 has completed.)
|
||||
|
||||
- [ ] **Step 1.5: Write the new `AGENTS.md`**
|
||||
|
||||
Use the target structure above as a starting template. Adapt the language to match the project's style. Keep the file under 1.5K.
|
||||
|
||||
- [ ] **Step 1.6: Validate all cross-doc links in the new file**
|
||||
|
||||
For every `[text](./relative/path)` or file mention:
|
||||
- Use `manual-slop_search_files` to confirm the target exists
|
||||
- If broken, fix the link or remove the mention
|
||||
|
||||
- [ ] **Step 1.7: Confirm size is reasonable**
|
||||
|
||||
```powershell
|
||||
(Get-Item C:\projects\manual_slop\AGENTS.md).Length
|
||||
```
|
||||
|
||||
Should be ≤ 1500 bytes. If larger, trim.
|
||||
|
||||
- [ ] **Step 1.8: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add AGENTS.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(agents): rewrite as thin pointer document; defer to skill files and conductor docs"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Reduced from [N]K to ~1K. Content relocated to: [list canonical homes]. Anti-patterns compressed to 6 most critical." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Replace `CLAUDE.md` with a 1-Line Stub
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md` (current size ~6.7K)
|
||||
|
||||
**Target content:** A 1-line deprecation stub that preserves the file's existence for any external tooling that may reference it.
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Read the current `CLAUDE.md`**
|
||||
|
||||
- [ ] **Step 2.3: Verify no external scripts reference the CLAUDE.md content**
|
||||
|
||||
Use `manual-slop_py_find_usages` to grep for any code that reads CLAUDE.md's specific sections. If anything does, the stub approach still works (the script will get a 1-line file instead of the rich content, but it's a graceful failure).
|
||||
|
||||
If critical tooling depends on the rich content, flag this to the user before continuing.
|
||||
|
||||
- [ ] **Step 2.4: Write the stub**
|
||||
|
||||
```markdown
|
||||
# CLAUDE.md
|
||||
|
||||
This project is no longer actively used with Claude Code. For project context, see `AGENTS.md`. The conductor system in `./conductor/` is the cross-tool abstraction and works with any agent toolchain.
|
||||
```
|
||||
|
||||
- [ ] **Step 2.5: Confirm size is small**
|
||||
|
||||
```powershell
|
||||
(Get-Item C:\projects\manual_slop\CLAUDE.md).Length
|
||||
```
|
||||
|
||||
Should be ≤ 500 bytes.
|
||||
|
||||
- [ ] **Step 2.6: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add CLAUDE.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(claude): replace with deprecation stub; project no longer uses Claude Code"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Reduced from [N]K to stub. Preserves file existence for any legacy tooling. Project migrated to Gemini CLI + OpenCode." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Refresh `GEMINI.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `GEMINI.md` (current size ~3.8K)
|
||||
- Reference: `conductor/product.md` (now updated per Sub-Track 2), `conductor/tech-stack.md`, `src/gemini_cli_adapter.py`
|
||||
|
||||
**Scope:** This file should contain ONLY Gemini-CLI-specific content. Any content that was previously shared with `AGENTS.md` and has been relocated should now point to `AGENTS.md` instead.
|
||||
|
||||
- [ ] **Step 3.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Read the current `GEMINI.md`**
|
||||
|
||||
- [ ] **Step 3.3: Identify any content in `GEMINI.md` that is not Gemini-specific**
|
||||
|
||||
For each section, ask: "Would this content be useful to an agent that is NOT using Gemini CLI?" If yes, it should be in `AGENTS.md` or a conductor doc, not here.
|
||||
|
||||
Likely candidates to move out (or point to canonical home):
|
||||
- General MMA tier descriptions (should be in `AGENTS.md` or `mma-orchestrator/SKILL.md`)
|
||||
- General session startup checklist (should be in `AGENTS.md`)
|
||||
- General anti-patterns (should be in `AGENTS.md` or `conductor/product-guidelines.md`)
|
||||
|
||||
Content that should stay (Gemini-CLI-specific):
|
||||
- Gemini model routing per MMA tier
|
||||
- `activate_skill` usage for MMA roles
|
||||
- Gemini CLI bridge architecture (`gemini_cli_adapter.py`, `GEMINI_CLI_HOOK_CONTEXT`, synchronous HITL approval)
|
||||
- Gemini-specific caching/TTL behavior
|
||||
- Provider-specific quirks (cache rebuilding at 90% TTL, server-side context cache)
|
||||
- `gemini-3.1-pro-preview` / `gemini-3-flash-preview` / `gemini-2.5-flash-lite` model names
|
||||
|
||||
- [ ] **Step 3.4: Rewrite `GEMINI.md` to contain only Gemini-CLI-specific content**
|
||||
|
||||
For each non-Gemini-specific section, replace with a short pointer to the canonical home (e.g., "See `AGENTS.md` for the MMA tier overview. This document covers Gemini-CLI-specific implementation only.").
|
||||
|
||||
- [ ] **Step 3.5: Verify Gemini-specific content is accurate**
|
||||
|
||||
For each Gemini-specific claim:
|
||||
- Verify model names match `conductor/tech-stack.md`
|
||||
- Verify TTL/cache behavior matches `src/ai_client.py` and `src/gemini_cli_adapter.py`
|
||||
- Verify the bridge architecture description matches the actual code
|
||||
|
||||
- [ ] **Step 3.6: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 3.7: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add GEMINI.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(gemini): refresh and reduce to Gemini-CLI-specific content"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Replaced [N] sections with pointers to AGENTS.md. Kept [list of Gemini-specific sections]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Light Verification of `.agents/skills/*/SKILL.md` and `mma-orchestrator/SKILL.md`
|
||||
|
||||
**Files:**
|
||||
- Read: `mma-orchestrator/SKILL.md` (~9.3K)
|
||||
- Read: `.agents/skills/mma-tier1-orchestrator/SKILL.md` (~2.4K)
|
||||
- Read: `.agents/skills/mma-tier2-tech-lead/SKILL.md` (~3.8K)
|
||||
- Read: `.agents/skills/mma-tier3-worker/SKILL.md`
|
||||
- Read: `.agents/skills/mma-tier4-qa/SKILL.md` (~813 bytes)
|
||||
- Read: `.agents/skills/mma-orchestrator/SKILL.md` (~9.7K)
|
||||
|
||||
**Scope:** This is a LIGHT verification pass. For each file:
|
||||
1. Read it
|
||||
2. Confirm it covers the role accurately (cross-reference with the orchestrator skill's role description)
|
||||
3. If drift is found, make a surgical fix
|
||||
4. If the file is fundamentally outdated, FLAG for a separate track — do NOT rewrite wholesale here
|
||||
|
||||
- [ ] **Step 4.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Read the canonical orchestrator skill**
|
||||
|
||||
Use `manual-slop_read_file` on `mma-orchestrator/SKILL.md`. This is the source of truth for the MMA architecture.
|
||||
|
||||
- [ ] **Step 4.3: For each tier skill (1-4), verify role accuracy**
|
||||
|
||||
For each `.agents/skills/mma-tierN-*/SKILL.md`:
|
||||
- Read it (use `manual-slop_read_file`)
|
||||
- Cross-reference with the orchestrator skill's description of the tier
|
||||
- Note any drift: missing responsibilities, stale model names, wrong tool references
|
||||
|
||||
- [ ] **Step 4.4: For each drift found, make a surgical fix**
|
||||
|
||||
If the drift is small (a model name, a tool name, a single responsibility statement):
|
||||
- Make the fix in place
|
||||
- Commit per file
|
||||
|
||||
If the drift is large (the whole role description is outdated):
|
||||
- DO NOT rewrite in this plan
|
||||
- Note it in the commit message
|
||||
- Add a task to a SEPARATE follow-up track (do not create new tasks in this plan)
|
||||
|
||||
- [ ] **Step 4.5: For each file with a surgical fix, commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .agents/skills/mma-tierN-*/SKILL.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(skills): fix drift in [tierN] skill - [specific fix]"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Found drift: [description]. Surgical fix applied: [what changed]. Larger rewrites deferred to [reason]." $_ }
|
||||
```
|
||||
|
||||
- [ ] **Step 4.6: If no drift is found, commit an empty verification log**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop commit --allow-empty -m "docs(skills): verify all 5 skill files are coherent with orchestrator SKILL.md"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Read all 5 skill files. Cross-referenced with orchestrator SKILL.md. No drift found." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Light Verification of `.gemini/...` Mirrors
|
||||
|
||||
**Files:**
|
||||
- Read: `.gemini/skills/mma-orchestrator/SKILL.md`
|
||||
- Read: `.gemini/skills/mma-tier1-orchestrator/SKILL.md`
|
||||
- Read: `.gemini/skills/mma-tier2-tech-lead/SKILL.md`
|
||||
- Read: `.gemini/skills/mma-tier3-worker/SKILL.md`
|
||||
- Read: `.gemini/skills/mma-tier4-qa/SKILL.md`
|
||||
- Read: `.gemini/agents/tier1-orchestrator.md`
|
||||
- Read: `.gemini/agents/tier2-tech-lead.md`
|
||||
- Read: `.gemini/agents/tier3-worker.md`
|
||||
- Read: `.gemini/agents/tier4-qa.md`
|
||||
|
||||
**Scope:** Compare the `.gemini/` mirror files against their canonical counterparts (`.agents/...`). If drift is found, fix it. This is the same LIGHT verification as Task 4 but for the Gemini-specific mirrors.
|
||||
|
||||
- [ ] **Step 5.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 5.2: For each `.gemini/...` file, compare to its canonical counterpart**
|
||||
|
||||
For each file:
|
||||
- Read both the mirror and the canonical
|
||||
- Diff them
|
||||
- If different, determine which is the source of truth
|
||||
- If the mirror is stale, fix it to match the canonical (or vice versa, depending on the file's role)
|
||||
|
||||
- [ ] **Step 5.3: For each drift found, commit per file**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .gemini/[path]
|
||||
git -C C:\projects\manual_slop commit -m "docs(gemini-mirror): sync [file] with [canonical source]"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Mirror was out of sync. Updated to match canonical [path]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Light Verification of `.claude/commands/*.md` (Legacy)
|
||||
|
||||
**Files:**
|
||||
- Read: every file in `.claude/commands/` (9 files per the directory listing)
|
||||
|
||||
**Scope:** These are legacy files for backward compatibility. Lightly verify they still work and don't reference removed features. Do NOT rewrite — they are legacy.
|
||||
|
||||
- [ ] **Step 6.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 6.2: Read each file and check for broken references**
|
||||
|
||||
For each `.claude/commands/*.md`:
|
||||
- Use `manual-slop_read_file` (they're small)
|
||||
- Check for references to removed features (e.g., `gui_legacy.py`, `tkinter`, removed modules)
|
||||
- Check for links that no longer resolve
|
||||
|
||||
- [ ] **Step 6.3: For each broken reference, decide**
|
||||
|
||||
Options:
|
||||
- (a) Fix the broken reference (small edit)
|
||||
- (b) Add a "(DEPRECATED)" notice to the file
|
||||
- (c) Leave alone (legacy)
|
||||
|
||||
Default: (a) if it's a trivial fix (e.g., a link to a renamed file). (b) if the whole file is suspect. (c) if it's working and just old.
|
||||
|
||||
- [ ] **Step 6.4: For each file with a fix, commit per file**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .claude/commands/[file]
|
||||
git -C C:\projects\manual_slop commit -m "docs(claude-commands): fix broken reference in [file]"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Found broken reference to [X]. Fixed by [what]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Sub-Track 3 Verification (Drift Check)
|
||||
|
||||
- [ ] **Step 7.1: Section header drift audit**
|
||||
|
||||
For the section headers that should have been relocated, confirm each appears in at most one of `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`:
|
||||
|
||||
```powershell
|
||||
$headers = @("Session Startup", "Conductor System", "MMA 4-Tier", "Anti-Patterns", "Development Workflow", "Code Style", "MCP Tool")
|
||||
foreach ($h in $headers) {
|
||||
$inAgents = Select-String -Path C:\projects\manual_slop\AGENTS.md -Pattern $h -SimpleMatch -Quiet
|
||||
$inClaude = Select-String -Path C:\projects\manual_slop\CLAUDE.md -Pattern $h -SimpleMatch -Quiet
|
||||
$inGemini = Select-String -Path C:\projects\manual_slop\GEMINI.md -Pattern $h -SimpleMatch -Quiet
|
||||
$count = @($inAgents, $inClaude, $inGemini) | Where-Object { $_ } | Measure-Object | Select-Object -ExpandProperty Count
|
||||
if ($count -gt 1) { Write-Host "DRIFT: '$h' appears in $count files" }
|
||||
}
|
||||
```
|
||||
|
||||
If any header appears in 2+ files, the divergence model is violated. Either:
|
||||
- (a) The duplicate is a short pointer (≤3 lines) → OK
|
||||
- (b) The duplicate is full content → Fix by relocating
|
||||
|
||||
- [ ] **Step 7.2: Size sanity check**
|
||||
|
||||
```powershell
|
||||
(Get-Item C:\projects\manual_slop\AGENTS.md).Length
|
||||
(Get-Item C:\projects\manual_slop\CLAUDE.md).Length
|
||||
(Get-Item C:\projects\manual_slop\GEMINI.md).Length
|
||||
```
|
||||
|
||||
- AGENTS.md should be ≤ 1.5K
|
||||
- CLAUDE.md should be ≤ 500 bytes
|
||||
- GEMINI.md can be larger (Gemini-specific content is allowed) but should not contain duplicated agent-orientation content
|
||||
|
||||
- [ ] **Step 7.3: Link validation**
|
||||
|
||||
For every `[text](./relative/path)` in each of the 3 files, verify the target exists.
|
||||
|
||||
- [ ] **Step 7.4: Per-file commit verification**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop log --oneline -- AGENTS.md CLAUDE.md GEMINI.md .agents/ .gemini/ .claude/ | head -30
|
||||
```
|
||||
|
||||
Confirm every file has its own commit history (not batched).
|
||||
|
||||
- [ ] **Step 7.5: Git note verification**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop log --format="%H" -- AGENTS.md CLAUDE.md GEMINI.md .agents/ .gemini/ .claude/ | Select-Object -First 20 | ForEach-Object { $note = git -C C:\projects\manual_slop notes show $_ 2>$null; if (-not $note) { Write-Host "MISSING NOTE: $_" } }
|
||||
```
|
||||
|
||||
- [ ] **Step 7.6: Phase completion checkpoint**
|
||||
|
||||
Per the project's Phase Completion Verification protocol in `conductor/workflow.md`:
|
||||
- Run the automated test suite
|
||||
- Present the verification report to the user
|
||||
- Get user sign-off
|
||||
- Create the checkpoint commit: `conductor(checkpoint): Sub-Track 3 (agent config refresh) complete`
|
||||
- Attach the verification report as a git note
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (after writing this plan)
|
||||
|
||||
1. **Spec coverage:** Spec §6 has 6 phases (3.1-3.6). Tasks 1-3 cover 3.1-3.3. Tasks 4-6 cover 3.4-3.5 (skills, mirrors, legacy commands). Task 7 covers 3.6 (verification). ✓ All phases covered.
|
||||
2. **Placeholder scan:** No "TBD" / "TODO" markers. The "If drift is found, fix it; if fundamentally outdated, flag for separate track" decision rule is explicit in Task 4.4. ✓
|
||||
3. **Type/symbol consistency:** File paths, section header names, and the drift audit script are all consistent. ✓
|
||||
4. **Risk check:** The stub approach for CLAUDE.md preserves compatibility. The "light verification, flag for separate track" pattern prevents scope creep. The drift audit script in Step 7.1 is concrete and runnable. ✓
|
||||
@@ -1,245 +0,0 @@
|
||||
# Clean Install Test Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add an opt-in pytest test that clones the Manual Slop repo to a temp dir, runs `uv sync`, launches `sloppy.py --enable-test-hooks`, and verifies the Hook API responds. Catches "works on my machine" failures by exercising the full install-and-launch path in an isolated environment.
|
||||
|
||||
**Architecture:** Standard subprocess-based integration test. `git clone` from the user's Gitea server to `tmp_path`. `uv sync` in the cloned dir. Launch the app as a background subprocess. Poll the Hook API endpoint with `requests` until ready. Test a write hook. Clean up the process tree.
|
||||
|
||||
**Tech Stack:** Python 3.11+, pytest, subprocess, requests, git CLI
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-clean-install-test-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Execution Constraints
|
||||
|
||||
- **No subagents.** Execute as a single agent.
|
||||
- **Pre-edit checkpoint:** `git add .` before any file edit.
|
||||
- **Per-file atomic commits.**
|
||||
- **Commit message format:** `<type>(<scope>): <imperative description>`.
|
||||
- **Git note format:** 3-8 line rationale per commit.
|
||||
- **Style baseline:** 1-space indent, no comments, type hints.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `tests/test_clean_install.py` | Create | Opt-in test (RUN_CLEAN_INSTALL_TEST=1) |
|
||||
| `pyproject.toml` | Modify | Add `clean_install` marker |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add the `clean_install` marker to `pyproject.toml`
|
||||
|
||||
**Files:**
|
||||
- Modify: `pyproject.toml`
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Read current markers section**
|
||||
|
||||
Use `manual-slop_py_get_code_outline` or grep for `markers` in `pyproject.toml`.
|
||||
|
||||
- [ ] **Step 1.3: Add the `clean_install` marker**
|
||||
|
||||
Find the existing markers list (e.g., under `[tool.pytest.ini_options]`) and add:
|
||||
|
||||
```toml
|
||||
markers = [
|
||||
"integration: integration tests requiring live GUI",
|
||||
"strict: tests that require strict mode",
|
||||
"clean_install: clean install verification (opt-in via RUN_CLEAN_INSTALL_TEST=1)",
|
||||
]
|
||||
```
|
||||
|
||||
If markers aren't already in `pyproject.toml`, add them. If they are, append the new entry.
|
||||
|
||||
- [ ] **Step 1.4: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add pyproject.toml
|
||||
git -C C:\projects\manual_slop commit -m "test(pytest): add clean_install marker"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Registers the clean_install marker so tests can be selected with pytest -m clean_install or filtered with -m 'not clean_install'." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Write the clean install test
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_clean_install.py`
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Create the test file**
|
||||
|
||||
```python
|
||||
# tests/test_clean_install.py
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
REPO_URL = "https://git.cozyair.dev/ed/manual_slop"
|
||||
STARTUP_TIMEOUT_SECONDS = 30
|
||||
READINESS_POLL_INTERVAL = 0.5
|
||||
HOOK_PORT = 8999
|
||||
|
||||
|
||||
@pytest.mark.clean_install
|
||||
def test_clean_install_runs_with_hooks(tmp_path):
|
||||
"""Clone the repo, install deps, launch sloppy.py, verify Hook API.
|
||||
|
||||
Opt-in: set RUN_CLEAN_INSTALL_TEST=1 to enable. Otherwise skipped.
|
||||
Requires network access to the configured REPO_URL.
|
||||
"""
|
||||
if os.environ.get("RUN_CLEAN_INSTALL_TEST") != "1":
|
||||
pytest.skip("Set RUN_CLEAN_INSTALL_TEST=1 to enable")
|
||||
|
||||
clone_dir = tmp_path / "manual_slop"
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
|
||||
subprocess.run(
|
||||
["git", "clone", REPO_URL, str(clone_dir)],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
check=True,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
["uv", "sync"],
|
||||
cwd=str(clone_dir),
|
||||
capture_output=True, text=True, timeout=180,
|
||||
check=True,
|
||||
)
|
||||
|
||||
creationflags = 0
|
||||
if os.name == "nt":
|
||||
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
|
||||
process = subprocess.Popen(
|
||||
["uv", "run", "sloppy.py", "--enable-test-hooks"],
|
||||
cwd=str(clone_dir),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
creationflags=creationflags,
|
||||
)
|
||||
|
||||
try:
|
||||
start = time.time()
|
||||
ready = False
|
||||
while time.time() - start < STARTUP_TIMEOUT_SECONDS:
|
||||
if process.poll() is not None:
|
||||
stderr_out = process.stderr.read(2000) if process.stderr else b""
|
||||
pytest.fail(f"Process exited early. stderr: {stderr_out.decode('utf-8', errors='replace')}")
|
||||
try:
|
||||
response = requests.get(
|
||||
f"http://127.0.0.1:{HOOK_PORT}/status",
|
||||
timeout=1.0,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
payload = response.json()
|
||||
if payload.get("status") in ("running", "ready", "ok"):
|
||||
ready = True
|
||||
break
|
||||
except (requests.ConnectionError, requests.Timeout):
|
||||
pass
|
||||
time.sleep(READINESS_POLL_INTERVAL)
|
||||
|
||||
assert ready, (
|
||||
f"Hook server did not respond within {STARTUP_TIMEOUT_SECONDS}s. "
|
||||
f"stderr: {process.stderr.read(2000).decode('utf-8', errors='replace') if process.stderr else 'N/A'}"
|
||||
)
|
||||
|
||||
response = requests.get(
|
||||
f"http://127.0.0.1:{HOOK_PORT}/api/gui/mma_status",
|
||||
timeout=5.0,
|
||||
)
|
||||
assert response.status_code == 200, f"mma_status returned {response.status_code}"
|
||||
data = response.json()
|
||||
assert isinstance(data, dict), f"mma_status returned non-dict: {data!r}"
|
||||
|
||||
finally:
|
||||
_cleanup_process(process)
|
||||
|
||||
|
||||
def _cleanup_process(process: subprocess.Popen) -> None:
|
||||
if os.name == "nt":
|
||||
subprocess.run(
|
||||
["taskkill", "/F", "/T", "/PID", str(process.pid)],
|
||||
capture_output=True,
|
||||
)
|
||||
else:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
```
|
||||
|
||||
- [ ] **Step 2.3: Run the test in skip mode**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_clean_install.py -v
|
||||
```
|
||||
|
||||
Expected: SKIPPED (RUN_CLEAN_INSTALL_TEST not set).
|
||||
|
||||
- [ ] **Step 2.4: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add tests/test_clean_install.py
|
||||
git -C C:\projects\manual_slop commit -m "test(clean-install): add opt-in clone-and-verify pytest test"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Clones the Gitea repo to tmp_path, runs uv sync, launches sloppy.py --enable-test-hooks, polls :8999/status until ready, then tests /api/gui/mma_status write hook. Robust Windows/Unix process cleanup. Skipped unless RUN_CLEAN_INSTALL_TEST=1." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Phase Completion Verification
|
||||
|
||||
- [ ] **Step 3.1: Confirm test is properly gated**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_clean_install.py -v
|
||||
```
|
||||
|
||||
Expected: 1 skipped.
|
||||
|
||||
- [ ] **Step 3.2: Manual opt-in run (if network is available)**
|
||||
|
||||
```powershell
|
||||
RUN_CLEAN_INSTALL_TEST=1 uv run pytest tests/test_clean_install.py -v
|
||||
```
|
||||
|
||||
Expected: 1 passed (or 1 failed with clear diagnostic if the clone target is unreachable).
|
||||
|
||||
- [ ] **Step 3.3: Create the checkpoint commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop commit --allow-empty -m "conductor(checkpoint): Clean install test complete"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Track complete. Opt-in test (RUN_CLEAN_INSTALL_TEST=1) added. Verifies clone + uv sync + launch + hook API. Marked with @pytest.mark.clean_install." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** All design tasks have a plan task. ✓
|
||||
- **Placeholder scan:** Test code is complete. ✓
|
||||
- **Type consistency:** `tmp_path`, `process`, `response` used consistently. ✓
|
||||
- **Robust cleanup:** `_cleanup_process` handles Windows + Unix. ✓
|
||||
@@ -1,652 +0,0 @@
|
||||
# Command Palette Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement the Command Palette feature (Phase 2 of `command_palette_and_performance_20260602`) with fuzzy search, command registry, and Ctrl+Shift+P activation, plus comprehensive unit and integration tests.
|
||||
|
||||
**Architecture:** Module-level functions in `src/command_palette.py` (per the project's UI delegation pattern). Static command definitions in `src/commands.py` registered via decorator. Thin `_render_command_palette(self)` wrapper in `App` class. Fuzzy matcher is a pure function for testability.
|
||||
|
||||
**Tech Stack:** Python 3.11+, imgui-bundle (Dear ImGui), pytest
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-command-palette-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Execution Constraints
|
||||
|
||||
These apply to every task:
|
||||
|
||||
- **No subagents.** Execute as a single agent.
|
||||
- **Pre-edit checkpoint:** Before any file edit, run `git add .` to stage current state.
|
||||
- **Per-file atomic commits:** One file = one commit. Never batch.
|
||||
- **Commit message format:** `<type>(<scope>): <imperative description>` (e.g., `feat(palette): add fuzzy matcher`).
|
||||
- **Git note format:** Attach a 3-8 line rationale to each commit.
|
||||
- **Style baseline:** `conductor/product-guidelines.md` — 1-space indent, no comments, type hints.
|
||||
- **Test framework:** pytest. Live GUI tests use the `live_gui` fixture.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/command_palette.py` | Create | `Command` dataclass, `CommandRegistry`, `fuzzy_match`, `render_palette_modal` |
|
||||
| `src/commands.py` | Create | ~30-50 command definitions across categories |
|
||||
| `src/gui_2.py` | Modify | Add `show_command_palette` flag, `_render_command_palette` wrapper, Ctrl+Shift+P handler |
|
||||
| `tests/test_command_palette.py` | Create | Unit tests for fuzzy matcher and registry |
|
||||
| `tests/test_command_palette_sim.py` | Create | Integration tests via `live_gui` |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Define the `Command` dataclass and `CommandRegistry`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/command_palette.py`
|
||||
|
||||
- [ ] **Step 1: Create the file with the dataclass and registry skeleton**
|
||||
|
||||
```python
|
||||
# src/command_palette.py
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Callable, List, Dict, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Command:
|
||||
id: str
|
||||
title: str
|
||||
category: str
|
||||
shortcut: Optional[str] = None
|
||||
description: str = ""
|
||||
enabled_when: Optional[str] = None
|
||||
action: Optional[Callable] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScoredCommand:
|
||||
command: Command
|
||||
score: float
|
||||
|
||||
|
||||
class CommandRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._commands: Dict[str, Command] = {}
|
||||
|
||||
def register(self, command_or_callable: Any) -> Any:
|
||||
if isinstance(command_or_callable, Command):
|
||||
cmd = command_or_callable
|
||||
else:
|
||||
cmd = Command(
|
||||
id=command_or_callable.__name__,
|
||||
title=command_or_callable.__name__.replace("_", " ").title(),
|
||||
category="uncategorized",
|
||||
action=command_or_callable,
|
||||
)
|
||||
if cmd.id in self._commands:
|
||||
raise ValueError(f"Command {cmd.id} already registered")
|
||||
self._commands[cmd.id] = cmd
|
||||
return command_or_callable
|
||||
|
||||
def all(self) -> List[Command]:
|
||||
return list(self._commands.values())
|
||||
|
||||
def get(self, command_id: str) -> Optional[Command]:
|
||||
return self._commands.get(command_id)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add src/command_palette.py
|
||||
git -C C:\projects\manual_slop commit -m "feat(palette): add Command dataclass and CommandRegistry"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Defines Command (id, title, category, shortcut, description, action) and CommandRegistry (register, all, get). Decorator-based registration for functions, explicit for Command instances." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Implement the fuzzy matcher
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/command_palette.py`
|
||||
- Test: `tests/test_command_palette.py`
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Write the failing test (TDD Red)**
|
||||
|
||||
Create `tests/test_command_palette.py`:
|
||||
|
||||
```python
|
||||
# tests/test_command_palette.py
|
||||
from src.command_palette import Command, ScoredCommand, fuzzy_match
|
||||
|
||||
|
||||
def _cmd(id: str, title: str) -> Command:
|
||||
return Command(id=id, title=title, category="test")
|
||||
|
||||
|
||||
def test_fuzzy_match_prefix_ranks_first():
|
||||
candidates = [
|
||||
_cmd("find", "Find in Selection"),
|
||||
_cmd("fold", "Fold All"),
|
||||
_cmd("config", "Configure Settings"),
|
||||
]
|
||||
results = fuzzy_match("fin", candidates, top_n=10)
|
||||
assert len(results) > 0
|
||||
assert results[0].command.id == "find"
|
||||
assert results[0].score > 0.5
|
||||
|
||||
|
||||
def test_fuzzy_match_subsequence_match():
|
||||
candidates = [_cmd("x", "Find")]
|
||||
results = fuzzy_match("fd", candidates, top_n=10)
|
||||
assert len(results) == 1
|
||||
assert results[0].command.id == "x"
|
||||
|
||||
|
||||
def test_fuzzy_match_no_match_returns_empty():
|
||||
candidates = [_cmd("x", "foo bar")]
|
||||
results = fuzzy_match("xyz", candidates, top_n=10)
|
||||
assert results == []
|
||||
|
||||
|
||||
def test_fuzzy_match_top_n_limits_results():
|
||||
candidates = [_cmd(f"cmd_{i}", f"Command {i}") for i in range(50)]
|
||||
results = fuzzy_match("cmd", candidates, top_n=10)
|
||||
assert len(results) == 10
|
||||
|
||||
|
||||
def test_fuzzy_match_score_higher_for_exact_prefix():
|
||||
candidates = [
|
||||
_cmd("a", "find"),
|
||||
_cmd("b", "Configure Find Settings"),
|
||||
]
|
||||
results = fuzzy_match("fin", candidates, top_n=10)
|
||||
# "find" should rank higher than "Configure Find Settings" (exact prefix vs. word boundary)
|
||||
assert results[0].command.id == "a"
|
||||
```
|
||||
|
||||
- [ ] **Step 2.3: Run tests to confirm they fail**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_command_palette.py -v
|
||||
```
|
||||
|
||||
Expected: `ImportError` or `AttributeError` for `fuzzy_match`.
|
||||
|
||||
- [ ] **Step 2.4: Implement `fuzzy_match`**
|
||||
|
||||
Add to `src/command_palette.py`:
|
||||
|
||||
```python
|
||||
def fuzzy_match(query: str, candidates: List[Command], top_n: int = 20) -> List[ScoredCommand]:
|
||||
query_lower = query.lower()
|
||||
scored: List[ScoredCommand] = []
|
||||
for cmd in candidates:
|
||||
title_lower = cmd.title.lower()
|
||||
if not _is_subsequence(query_lower, title_lower):
|
||||
continue
|
||||
score = _compute_score(query_lower, title_lower)
|
||||
scored.append(ScoredCommand(command=cmd, score=score))
|
||||
scored.sort(key=lambda r: r.score, reverse=True)
|
||||
return scored[:top_n]
|
||||
|
||||
|
||||
def _is_subsequence(query: str, target: str) -> bool:
|
||||
qi = 0
|
||||
for ch in target:
|
||||
if qi < len(query) and ch == query[qi]:
|
||||
qi += 1
|
||||
return qi == len(query)
|
||||
|
||||
|
||||
def _compute_score(query: str, target: str) -> float:
|
||||
score = 0.0
|
||||
if target.startswith(query):
|
||||
score += 1.0
|
||||
elif _starts_at_word_boundary(query, target):
|
||||
score += 0.5
|
||||
if _is_contiguous(query, target):
|
||||
score += 0.3
|
||||
gaps = _count_gaps(query, target)
|
||||
score -= 0.1 * gaps
|
||||
return score
|
||||
|
||||
|
||||
def _starts_at_word_boundary(query: str, target: str) -> bool:
|
||||
if not target.startswith(query):
|
||||
return False
|
||||
return len(query) == 0 or not query[0].isalnum() or len(target) == len(query) or not target[len(query)].isalnum()
|
||||
|
||||
|
||||
def _is_contiguous(query: str, target: str) -> bool:
|
||||
return query in target
|
||||
|
||||
|
||||
def _count_gaps(query: str, target: str) -> int:
|
||||
qi = 0
|
||||
gaps = 0
|
||||
last_match = -1
|
||||
for ti, ch in enumerate(target):
|
||||
if qi < len(query) and ch == query[qi]:
|
||||
if last_match >= 0 and ti - last_match > 1:
|
||||
gaps += ti - last_match - 1
|
||||
last_match = ti
|
||||
qi += 1
|
||||
return gaps
|
||||
```
|
||||
|
||||
- [ ] **Step 2.5: Run tests to confirm they pass**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_command_palette.py -v
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2.6: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add src/command_palette.py tests/test_command_palette.py
|
||||
git -C C:\projects\manual_slop commit -m "feat(palette): add fuzzy_match with subsequence matching and scoring"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Pure function. Subsequence match + 4-component score: exact prefix (+1.0), word boundary (+0.5), contiguous (+0.3), gap penalty (-0.1 per gap). Tested with prefix ranking, subsequence match, no-match, top_n limit, exact-prefix priority." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Define static commands
|
||||
|
||||
**Files:**
|
||||
- Create: `src/commands.py`
|
||||
|
||||
- [ ] **Step 3.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Create the commands file**
|
||||
|
||||
```python
|
||||
# src/commands.py
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
from src.command_palette import CommandRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.gui_2 import App
|
||||
|
||||
|
||||
registry = CommandRegistry()
|
||||
|
||||
|
||||
@registry.register
|
||||
def reset_session(app: "App") -> None:
|
||||
"""Reset Session — Reset the AI session and clear discussion history."""
|
||||
from src import ai_client
|
||||
ai_client.reset_session()
|
||||
if hasattr(app, "_handle_reset_session"):
|
||||
app._handle_reset_session()
|
||||
|
||||
|
||||
@registry.register
|
||||
def clear_discussion(app: "App") -> None:
|
||||
"""Clear Discussion — Clear all entries in the current discussion."""
|
||||
if hasattr(app, "discussion_history"):
|
||||
app.discussion_history = []
|
||||
|
||||
|
||||
@registry.register
|
||||
def toggle_diagnostics(app: "App") -> None:
|
||||
"""Toggle Diagnostics — Show/hide the Diagnostics panel."""
|
||||
if hasattr(app, "show_diagnostics"):
|
||||
app.show_diagnostics = not app.show_diagnostics
|
||||
|
||||
|
||||
@registry.register
|
||||
def add_all_files_to_context(app: "App") -> None:
|
||||
"""Add All Files to Context — Add all tracked files to the context."""
|
||||
if hasattr(app, "_add_all_files_to_context"):
|
||||
app._add_all_files_to_context()
|
||||
|
||||
|
||||
@registry.register
|
||||
def open_project(app: "App") -> None:
|
||||
"""Open Project — Open a different project TOML."""
|
||||
if hasattr(app, "_show_project_picker"):
|
||||
app._show_project_picker()
|
||||
|
||||
|
||||
@registry.register
|
||||
def save_project(app: "App") -> None:
|
||||
"""Save Project — Save the current project state to TOML."""
|
||||
if hasattr(app, "_save_project_state"):
|
||||
app._save_project_state()
|
||||
|
||||
|
||||
@registry.register
|
||||
def trigger_hot_reload(app: "App") -> None:
|
||||
"""Hot Reload — Reload the GUI module to pick up code changes."""
|
||||
from src.hot_reloader import HotReloader
|
||||
HotReloader.reload("src.gui_2", app)
|
||||
|
||||
|
||||
@registry.register
|
||||
def show_documentation(app: "App") -> None:
|
||||
"""Show Documentation — Open the documentation index in the browser."""
|
||||
import webbrowser
|
||||
webbrowser.open("https://git.cozyair.dev/ed/manual_slop/")
|
||||
|
||||
|
||||
@registry.register
|
||||
def switch_to_dark_theme(app: "App") -> None:
|
||||
"""Switch to Dark Theme."""
|
||||
from src import theme_2
|
||||
theme_2.apply_dark_theme()
|
||||
|
||||
|
||||
@registry.register
|
||||
def switch_to_light_theme(app: "App") -> None:
|
||||
"""Switch to Light Theme."""
|
||||
from src import theme_2
|
||||
theme_2.apply_light_theme()
|
||||
|
||||
|
||||
@registry.register
|
||||
def switch_to_nerv_theme(app: "App") -> None:
|
||||
"""Switch to NERV Theme."""
|
||||
from src.theme_nerv import apply_nerv
|
||||
apply_nerv()
|
||||
```
|
||||
|
||||
- [ ] **Step 3.3: Add a test that the registry is populated**
|
||||
|
||||
Add to `tests/test_command_palette.py`:
|
||||
|
||||
```python
|
||||
def test_commands_registry_has_core_commands():
|
||||
from src.commands import registry
|
||||
all_ids = {c.id for c in registry.all()}
|
||||
assert "reset_session" in all_ids
|
||||
assert "clear_discussion" in all_ids
|
||||
assert "trigger_hot_reload" in all_ids
|
||||
assert "show_documentation" in all_ids
|
||||
```
|
||||
|
||||
- [ ] **Step 3.4: Run tests**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_command_palette.py -v
|
||||
```
|
||||
|
||||
Expected: All tests pass.
|
||||
|
||||
- [ ] **Step 3.5: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add src/commands.py tests/test_command_palette.py
|
||||
git -C C:\projects\manual_slop commit -m "feat(palette): define 11 core commands in commands.py"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "11 commands: reset_session, clear_discussion, toggle_diagnostics, add_all_files_to_context, open_project, save_project, trigger_hot_reload, show_documentation, switch_to_dark/light/nerv_theme. Each has defensive hasattr checks for the App methods they call." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Implement `render_palette_modal`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/command_palette.py`
|
||||
|
||||
- [ ] **Step 4.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Add the modal render function**
|
||||
|
||||
Add to `src/command_palette.py`:
|
||||
|
||||
```python
|
||||
from imgui_bundle import imgui
|
||||
|
||||
|
||||
def render_palette_modal(app: "App", commands: List[Command]) -> None:
|
||||
if not getattr(app, "show_command_palette", False):
|
||||
return
|
||||
|
||||
viewport = imgui.get_main_viewport()
|
||||
center = viewport.get_center()
|
||||
imgui.set_next_window_pos((center.x - 300, center.y - 200))
|
||||
imgui.set_next_window_size((600, 400))
|
||||
|
||||
opened = [True]
|
||||
if not imgui.begin("Command Palette##manual_slop", flags=imgui.WindowFlags_.no_resize | imgui.WindowFlags_.no_collapse)[0]:
|
||||
app.show_command_palette = False
|
||||
imgui.end()
|
||||
return
|
||||
|
||||
if not hasattr(app, "_command_palette_query"):
|
||||
app._command_palette_query = ""
|
||||
if not hasattr(app, "_command_palette_selected"):
|
||||
app._command_palette_selected = 0
|
||||
|
||||
io = imgui.get_io()
|
||||
if imgui.is_key_pressed(imgui.Key.escape):
|
||||
app.show_command_palette = False
|
||||
imgui.end()
|
||||
return
|
||||
|
||||
imgui.set_next_item_width(-1)
|
||||
changed, app._command_palette_query = imgui.input_text("##query", app._command_palette_query, 256)
|
||||
imgui.set_keyboard_focus_here()
|
||||
|
||||
results = fuzzy_match(app._command_palette_query, commands, top_n=20)
|
||||
|
||||
if imgui.begin_child("##results", (0, -1)):
|
||||
for i, scored in enumerate(results):
|
||||
is_selected = (i == app._command_palette_selected)
|
||||
label = f"[{scored.command.category}] {scored.command.title}"
|
||||
if imgui.selectable(label, is_selected)[0]:
|
||||
app._command_palette_selected = i
|
||||
if is_selected:
|
||||
imgui.set_item_default_focus()
|
||||
if imgui.is_key_pressed(imgui.Key.enter) or imgui.is_key_pressed(imgui.Key.keypad_enter):
|
||||
if scored.command.action:
|
||||
scored.command.action(app)
|
||||
app.show_command_palette = False
|
||||
app._command_palette_query = ""
|
||||
app._command_palette_selected = 0
|
||||
if not results:
|
||||
imgui.text_disabled("No matching commands.")
|
||||
imgui.end_child()
|
||||
|
||||
imgui.end()
|
||||
```
|
||||
|
||||
- [ ] **Step 4.3: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add src/command_palette.py
|
||||
git -C C:\projects\manual_slop commit -m "feat(palette): add render_palette_modal with fuzzy search and keyboard nav"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Centered 600x400 modal. Search input with focus on open. Up/Down arrow nav via _command_palette_selected. Enter executes action and closes. Escape closes. Stores query/selected on app to persist across frames. Per delegation pattern: takes app as param." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Wire the palette into `App` class
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py`
|
||||
|
||||
- [ ] **Step 5.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 5.2: Add `show_command_palette` to `App.__init__`**
|
||||
|
||||
Use `manual-slop_py_get_skeleton` first to see the current `App.__init__` structure, then add:
|
||||
|
||||
```python
|
||||
# In App.__init__ (find a good spot near other UI flags)
|
||||
self.show_command_palette: bool = False
|
||||
```
|
||||
|
||||
- [ ] **Step 5.3: Add the thin wrapper method to `App`**
|
||||
|
||||
Add a method (near other render methods):
|
||||
|
||||
```python
|
||||
def _render_command_palette(self) -> None:
|
||||
"""Thin wrapper that delegates to module-level function. Per UI delegation pattern."""
|
||||
from src.command_palette import render_palette_modal
|
||||
from src.commands import registry
|
||||
render_palette_modal(self, registry.all())
|
||||
```
|
||||
|
||||
- [ ] **Step 5.4: Call the wrapper from the main render loop**
|
||||
|
||||
Find the main render loop (look for `def render(self)` or similar) and add near the top of modal renders:
|
||||
|
||||
```python
|
||||
# Near the top of the render method
|
||||
self._render_command_palette()
|
||||
```
|
||||
|
||||
- [ ] **Step 5.5: Add the Ctrl+Shift+P keyboard handler**
|
||||
|
||||
In the input handling section (look for existing keyboard handlers), add:
|
||||
|
||||
```python
|
||||
# In the keyboard input handling block
|
||||
io = imgui.get_io()
|
||||
if (io.key_ctrl and io.key_shift
|
||||
and not io.key_alt and not io.key_super
|
||||
and imgui.is_key_pressed(imgui.Key.p)):
|
||||
self.show_command_palette = not self.show_command_palette
|
||||
if self.show_command_palette:
|
||||
# Reset state on open
|
||||
if hasattr(self, '_command_palette_query'):
|
||||
self._command_palette_query = ""
|
||||
if hasattr(self, '_command_palette_selected'):
|
||||
self._command_palette_selected = 0
|
||||
```
|
||||
|
||||
- [ ] **Step 5.6: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add src/gui_2.py
|
||||
git -C C:\projects\manual_slop commit -m "feat(gui): wire Command Palette into App class with Ctrl+Shift+P"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Added show_command_palette flag, _render_command_palette thin wrapper, Ctrl+Shift+P keyboard handler. Wrapper delegates to module-level render_palette_modal. Keyboard handler resets state on open." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Write integration tests via `live_gui`
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_command_palette_sim.py`
|
||||
|
||||
- [ ] **Step 6.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 6.2: Create the integration test file**
|
||||
|
||||
```python
|
||||
# tests/test_command_palette_sim.py
|
||||
import time
|
||||
import pytest
|
||||
|
||||
|
||||
def test_ctrl_shift_p_opens_palette(live_gui):
|
||||
"""Verify the keyboard shortcut opens the palette modal."""
|
||||
client = live_gui[1]
|
||||
client.press_key_combo("Ctrl+Shift+P")
|
||||
time.sleep(0.5)
|
||||
state = client.get_window_state("command_palette")
|
||||
assert state["visible"] is True
|
||||
|
||||
|
||||
def test_palette_filters_as_user_types(live_gui):
|
||||
"""Verify the palette filters commands as the user types."""
|
||||
client = live_gui[1]
|
||||
client.press_key_combo("Ctrl+Shift+P")
|
||||
time.sleep(0.3)
|
||||
client.type_in_palette("reset")
|
||||
time.sleep(0.3)
|
||||
results = client.get_palette_results()
|
||||
titles = [r["title"].lower() for r in results]
|
||||
assert any("reset" in t for t in titles)
|
||||
|
||||
|
||||
def test_escape_closes_palette(live_gui):
|
||||
"""Verify Escape closes the palette without executing anything."""
|
||||
client = live_gui[1]
|
||||
client.press_key_combo("Ctrl+Shift+P")
|
||||
time.sleep(0.3)
|
||||
client.press_key("Escape")
|
||||
time.sleep(0.3)
|
||||
state = client.get_window_state("command_palette")
|
||||
assert state["visible"] is False
|
||||
```
|
||||
|
||||
- [ ] **Step 6.3: Run the tests**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_command_palette_sim.py -v
|
||||
```
|
||||
|
||||
Expected: 3 tests pass (live_gui fixture handles process lifecycle).
|
||||
|
||||
- [ ] **Step 6.4: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add tests/test_command_palette_sim.py
|
||||
git -C C:\projects\manual_slop commit -m "test(palette): add live_gui integration tests for Ctrl+Shift+P, filter, escape"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "3 integration tests via live_gui: Ctrl+Shift+P opens palette, typing filters by fuzzy match, Escape closes. Each is independent; failures isolate to the specific behavior." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Phase Completion Verification
|
||||
|
||||
- [ ] **Step 7.1: Run the full palette test suite**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_command_palette.py tests/test_command_palette_sim.py -v
|
||||
```
|
||||
|
||||
Expected: All unit + integration tests pass.
|
||||
|
||||
- [ ] **Step 7.2: Manually verify in a running app**
|
||||
|
||||
```powershell
|
||||
uv run sloppy.py
|
||||
```
|
||||
|
||||
Press `Ctrl+Shift+P`. The palette should open. Type "reset". The "Reset Session" command should appear at the top. Press Enter. The palette should close. Verify the discussion history was cleared.
|
||||
|
||||
- [ ] **Step 7.3: Create the checkpoint commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop commit --allow-empty -m "conductor(checkpoint): Command Palette (Phase 2) complete"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Command Palette feature complete. 6 atomic per-file commits. Unit tests for fuzzy_match, registry, and core commands. Integration tests for Ctrl+Shift+P, filtering, escape. Manually verified in running app." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** All design sections have a task. ✓
|
||||
- **Placeholder scan:** No "TBD"/"TODO". ✓
|
||||
- **Type consistency:** `Command`, `ScoredCommand`, `CommandRegistry`, `fuzzy_match` names used consistently. ✓
|
||||
- **No subagent dispatch:** All tasks are single-agent. ✓
|
||||
@@ -1,339 +0,0 @@
|
||||
# Conductor Docs Refresh Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Sync the source-of-truth conductor docs (`conductor/product.md`, `conductor/product-guidelines.md`, `conductor/tech-stack.md`, `conductor/workflow.md`, `conductor/index.md`) to reflect the current state of the codebase after 4 months of evolution since the last docs refresh.
|
||||
|
||||
**Architecture:** Single-agent sequential execution. Each conductor doc is a self-contained task: audit current claims against the actual codebase, identify drift, rewrite stale sections, commit per-file, attach git note. No new conductor docs are written in this plan (those are out of scope per the spec).
|
||||
|
||||
**Tech Stack:** Manual Slop's MCP research tools. Git for version control. Markdown for documentation.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-comprehensive-documentation-refresh-design.md` §5 (Sub-Track 2).
|
||||
|
||||
**Depends on:** Sub-Track 1 (`docs_layer_refresh_20260602`) must complete first. The human-facing docs layer is the source of truth that this plan syncs to.
|
||||
|
||||
---
|
||||
|
||||
## Execution Constraints
|
||||
|
||||
These apply to every task in this plan:
|
||||
|
||||
- **No subagents.** Execute as a single agent. No Tier 3/Tier 4 delegation. No parallel workers.
|
||||
- **Pre-edit checkpoint:** Before any file edit, run `git add .` to stage current state.
|
||||
- **Per-file atomic commits:** One file = one commit. Never batch.
|
||||
- **Commit message format:** `docs(<scope>): <imperative description>`.
|
||||
- **Git note format:** Attach a 3-8 line rationale to each commit.
|
||||
- **Symbol parity:** Every class, function, module, or dep name in these docs must match the source.
|
||||
- **The conductor docs are the source of truth** for the rest of the project. Anything documented here must be verifiable in the codebase.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Sync `conductor/product.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/product.md` (current size ~23.1K)
|
||||
- Reference: `src/` directory (every module), `docs/Readme.md` (now updated per Sub-Track 1), `docs/guide_*.md`
|
||||
|
||||
**Scope:** This is the product vision / feature list document. Every feature claim must correspond to a real subsystem. Every subsystem should appear. No "planned" or "in progress" features should be listed as if they exist (unless they're in the active backlog).
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Read the current `conductor/product.md`**
|
||||
|
||||
Use `manual-slop_get_file_summary` first, then `manual-slop_get_file_slice` for sections.
|
||||
|
||||
- [ ] **Step 1.3: Inventory every src/ module**
|
||||
|
||||
Use `manual-slop_list_directory path="C:\projects\manual_slop\src"` to get the current module list. For each module, use `manual-slop_get_file_summary` to understand its purpose.
|
||||
|
||||
Map each module to a feature. Note: the modules in `src/` are the source of truth for what exists.
|
||||
|
||||
- [ ] **Step 1.4: Compare modules to feature claims in `product.md`**
|
||||
|
||||
Build a comparison:
|
||||
- **Modules in `src/` not mentioned in `product.md`:** [list] — these are missing features
|
||||
- **Features in `product.md` with no corresponding module:** [list] — these are stale claims
|
||||
- **Features with a module that does something different than `product.md` claims:** [list] — these are stale descriptions
|
||||
|
||||
- [ ] **Step 1.5: Add missing features**
|
||||
|
||||
For each module not mentioned in `product.md`:
|
||||
- Find the most relevant existing section in `product.md`
|
||||
- Add a sub-bullet or paragraph describing the feature
|
||||
- Use the gap analysis from Sub-Track 1's `gap_analysis.md` as a cross-reference
|
||||
|
||||
Pay particular attention to: RAG, Beads, Hot Reload, Discussion Metrics/Compression, Command Palette, Structural File Editor, NERV Theme, Persona Editor, Workspace Profiles, History (undo/redo), External MCP, Synced HITL Bridge, External Editor Integration.
|
||||
|
||||
- [ ] **Step 1.6: Remove or correct stale claims**
|
||||
|
||||
For each feature claim that no longer matches the code:
|
||||
- If the feature was removed, delete the claim
|
||||
- If the feature was renamed or restructured, update the description
|
||||
- If the feature was culled (per the Culling Candidates report), remove the claim
|
||||
|
||||
- [ ] **Step 1.7: Update the "Primary Use Cases" section**
|
||||
|
||||
If real-world usage patterns have evolved (e.g., the user mentioned Claude is no longer used), update the use cases to reflect actual current usage.
|
||||
|
||||
- [ ] **Step 1.8: Validate all cross-doc links**
|
||||
|
||||
For every `[text](./relative/path)` in the modified `product.md`, verify the target exists.
|
||||
|
||||
- [ ] **Step 1.9: Commit per-section (one commit per logical group of changes)**
|
||||
|
||||
For example:
|
||||
- If you added 5 new features, that's 1 commit: `docs(product): add RAG, Beads, Hot Reload, and 2 more features`
|
||||
- If you corrected 3 stale claims, that's 1 commit: `docs(product): correct stale claims for X, Y, Z`
|
||||
- If you removed 2 culled features, that's 1 commit: `docs(product): remove culled features X, Y`
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add conductor/product.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(product): <description>"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Sync rationale: [what was stale, what was added, what was removed]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Sync `conductor/tech-stack.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/tech-stack.md`
|
||||
- Reference: `pyproject.toml`, `requirements.txt`, `src/` (every module)
|
||||
|
||||
**Scope:** Every entry must be a real Python dep (verify in `pyproject.toml` or `requirements.txt`) or a real `src/` module. No "we use X" claims without verification.
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Read the current `conductor/tech-stack.md`**
|
||||
|
||||
- [ ] **Step 2.3: Read `pyproject.toml` to get the canonical dep list**
|
||||
|
||||
Use `manual-slop_read_file` (it's small).
|
||||
|
||||
- [ ] **Step 2.4: For each dep claim in `tech-stack.md`, verify it exists in `pyproject.toml`**
|
||||
|
||||
Use `manual-slop_py_find_usages` or grep to confirm. Flag any claim that doesn't match.
|
||||
|
||||
- [ ] **Step 2.5: For each `src/` module claim in `tech-stack.md`, verify it exists**
|
||||
|
||||
Use `manual-slop_search_files` to confirm.
|
||||
|
||||
- [ ] **Step 2.6: Add missing deps and modules**
|
||||
|
||||
Likely missing entries (verify via Step 2.3-2.5):
|
||||
- `chromadb` (RAG)
|
||||
- `dolt` or any Beads dep
|
||||
- `psutil` (performance monitoring)
|
||||
- `pywin32` (window frame manipulation)
|
||||
- `pydantic` (if used)
|
||||
- New `src/` modules: `rag_engine.py`, `beads_client.py`, `hot_reloader.py`, `history.py`, `workspace_manager.py`, `theme_nerv.py`, `theme_nerv_fx.py`, `personas.py`, `presets.py`, `context_presets.py`, `tool_bias.py`, `tool_presets.py`, `synthesis_formatter.py`, `imgui_scopes.py`, etc.
|
||||
|
||||
- [ ] **Step 2.7: Remove or correct stale claims**
|
||||
|
||||
- [ ] **Step 2.8: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 2.9: Commit per-section**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add conductor/tech-stack.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(tech-stack): <description>"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Sync rationale: [added X deps/modules, removed Y stale claims]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Sync `conductor/workflow.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/workflow.md` (current size ~25.2K)
|
||||
- Reference: 5-10 recent completed tracks (read their `plan.md` files), the test suite, the pre-commit/lint scripts
|
||||
|
||||
**Scope:** The task lifecycle, TDD protocol, phase completion verification, checkpointing. This is the document every conductor-driven agent reads. It must match actual practice.
|
||||
|
||||
- [ ] **Step 3.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Read the current `conductor/workflow.md`**
|
||||
|
||||
- [ ] **Step 3.3: Spot-check 5 recent completed tracks for workflow compliance**
|
||||
|
||||
Pick 5 tracks from `conductor/archive/` or `conductor/tracks/` (use `manual-slop_list_directory`). For each:
|
||||
- Read its `plan.md` and `spec.md`
|
||||
- Compare the actual workflow followed (commits, tests, checkpoint) to what `workflow.md` prescribes
|
||||
- Note any drift: did the track skip a phase? Add a step that isn't in `workflow.md`? Skip a verification step?
|
||||
|
||||
- [ ] **Step 3.4: Update `workflow.md` to match actual practice**
|
||||
|
||||
If a phase is being consistently skipped, either (a) update the docs to make it optional, or (b) flag the drift and ask the user. Default: (a) — update the docs to match reality, then enforce via a future lint script (which is out of scope for this track).
|
||||
|
||||
- [ ] **Step 3.5: Update the Phase Completion Verification section**
|
||||
|
||||
Verify the protocol description matches the actual steps in recent tracks. Update if needed.
|
||||
|
||||
- [ ] **Step 3.6: Update the Commit Guidelines section**
|
||||
|
||||
Verify the message format examples match recent commits. Run `git log --oneline -20` to see the recent style.
|
||||
|
||||
- [ ] **Step 3.7: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 3.8: Commit per-section**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add conductor/workflow.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(workflow): <description>"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Sync rationale: [drift findings and corrections]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Sync `conductor/product-guidelines.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/product-guidelines.md` (current size ~6.6K)
|
||||
- Reference: `src/app_controller.py`, `src/gui_2.py`, `src/imgui_scopes.py`, `scripts/check_imgui_scopes.py`, recent track style
|
||||
|
||||
**Scope:** The AI-Optimized Python Style (1-space indent, no comments, type hints), Modular Controller Pattern, UI Delegation Pattern, Mandatory ImGui Verification, Structural Dependency Mapping (SDM) docstrings. These rules must match the actual code in `src/`.
|
||||
|
||||
- [ ] **Step 4.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Read the current `conductor/product-guidelines.md`**
|
||||
|
||||
- [ ] **Step 4.3: Spot-check the style claims against actual `src/` code**
|
||||
|
||||
For each style rule, sample 3 random files in `src/` and verify the rule is followed:
|
||||
- **1-space indentation:** Use `manual-slop_get_file_slice` on a few files, look for any 4-space or tab-indented lines
|
||||
- **No comments:** Use `grep` (via `manual-slop_run_powershell`) to find `#` characters in `src/*.py` (excluding shebangs, type comments, etc.)
|
||||
- **Type hints:** Sample a few public function signatures
|
||||
- **Modular Controller Pattern:** Check `src/app_controller.py` for module-level functions
|
||||
- **UI Delegation Pattern:** Check `src/gui_2.py` for `render_xxx` module-level functions
|
||||
- **SDM docstrings:** Check 3 random functions for `[C: ...]` and `[M: ...]` tags
|
||||
|
||||
- [ ] **Step 4.4: Update the guidelines to match reality**
|
||||
|
||||
If a rule is no longer followed:
|
||||
- Option (a): Update the rule to be more accurate
|
||||
- Option (b): Flag the drift and ask the user
|
||||
|
||||
Default: (a) — but flag any major drift in the commit message so the user is aware.
|
||||
|
||||
- [ ] **Step 4.5: Verify the Mandatory ImGui Verification claim**
|
||||
|
||||
Check that `scripts/check_imgui_scopes.py` exists and works. If it's been replaced by `src/imgui_scopes.py` and a different linter, update the docs.
|
||||
|
||||
- [ ] **Step 4.6: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 4.7: Commit per-section**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add conductor/product-guidelines.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(guidelines): <description>"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Sync rationale: [style drift findings, corrections made]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update `conductor/index.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/index.md` (current size 334 bytes)
|
||||
- Reference: every file in `conductor/`
|
||||
|
||||
**Scope:** The conductor directory index. Must link to all major conductor files.
|
||||
|
||||
- [ ] **Step 5.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 5.2: Inventory `conductor/` directory**
|
||||
|
||||
Use `manual-slop_get_tree path="C:\projects\manual_slop\conductor" max_depth=2`.
|
||||
|
||||
- [ ] **Step 5.3: Identify any major conductor file not linked from `index.md`**
|
||||
|
||||
Likely candidates: `conductor/code_styleguides/`, `conductor/fix_test_suite_failures_20260516.md`, any new top-level files.
|
||||
|
||||
- [ ] **Step 5.4: Update `index.md` to link the missing files**
|
||||
|
||||
- [ ] **Step 5.5: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 5.6: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add conductor/index.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(conductor): update index with [list of newly-linked files]"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Added links to [list]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Sub-Track 2 Verification
|
||||
|
||||
- [ ] **Step 6.1: Dep coverage audit**
|
||||
|
||||
For every Python dep in `pyproject.toml`, confirm it appears in `conductor/tech-stack.md`. Use:
|
||||
|
||||
```powershell
|
||||
$deps = (Get-Content C:\projects\manual_slop\pyproject.toml | Select-String '^\s*"?[a-zA-Z][a-zA-Z0-9_-]*[><=~]' | ForEach-Object { ($_.ToString().Trim() -split '["><=~]')[0].Trim() } | Sort-Object -Unique)
|
||||
foreach ($dep in $deps) { if (-not (Select-String -Path C:\projects\manual_slop\conductor\tech-stack.md -Pattern $dep -SimpleMatch -Quiet)) { Write-Host "MISSING: $dep" } }
|
||||
```
|
||||
|
||||
- [ ] **Step 6.2: Module coverage audit**
|
||||
|
||||
For every `.py` file in `src/`, confirm it appears in `conductor/tech-stack.md` or `conductor/product.md`. Use:
|
||||
|
||||
```powershell
|
||||
Get-ChildItem C:\projects\manual_slop\src\*.py | ForEach-Object { $name = $_.BaseName; if (-not (Select-String -Path C:\projects\manual_slop\conductor\tech-stack.md, C:\projects\manual_slop\conductor\product.md -Pattern $name -SimpleMatch -Quiet)) { Write-Host "MISSING: $name" } }
|
||||
```
|
||||
|
||||
- [ ] **Step 6.3: Feature claim verification**
|
||||
|
||||
For 3 random features claimed in `conductor/product.md`, verify each has a corresponding module and that the module does what the claim says. Use `manual-slop_get_file_summary` on the relevant module.
|
||||
|
||||
- [ ] **Step 6.4: Per-file commit verification**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop log --oneline -- conductor/product.md conductor/product-guidelines.md conductor/tech-stack.md conductor/workflow.md conductor/index.md | head -20
|
||||
```
|
||||
|
||||
Confirm every file has its own commit history (not batched).
|
||||
|
||||
- [ ] **Step 6.5: Git note verification**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop log --format="%H" -- conductor/ | Select-Object -First 20 | ForEach-Object { $note = git -C C:\projects\manual_slop notes show $_ 2>$null; if (-not $note) { Write-Host "MISSING NOTE: $_" } }
|
||||
```
|
||||
|
||||
- [ ] **Step 6.6: Phase completion checkpoint**
|
||||
|
||||
Per the project's Phase Completion Verification protocol in `conductor/workflow.md`:
|
||||
- Run the automated test suite (or at least smoke tests)
|
||||
- Present the verification report to the user
|
||||
- Get user sign-off
|
||||
- Create the checkpoint commit: `conductor(checkpoint): Sub-Track 2 (conductor docs refresh) complete`
|
||||
- Attach the verification report as a git note
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (after writing this plan)
|
||||
|
||||
1. **Spec coverage:** Spec §5 has 6 phases (2.1-2.6). Tasks 1-5 cover 2.1-2.5. Task 6 covers 2.6 (verification). ✓ All phases covered.
|
||||
2. **Placeholder scan:** No "TBD" / "TODO" markers. Specific file paths, commit commands, and verification scripts throughout. ✓
|
||||
3. **Type/symbol consistency:** Module names and dep names referenced consistently. The audit scripts in Task 6 are concrete and runnable. ✓
|
||||
4. **Risk check:** Pre-edit checkpoints, per-file commits, git notes, and the audit scripts mitigate drift. The optional-vs-mandatory decision in Step 3.4 is flagged for the user. ✓
|
||||
@@ -1,590 +0,0 @@
|
||||
# Docker & Web Frontend Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Containerize Manual Slop for Unraid deployment. Add a web frontend via the imgui-bundle web backend (per the [explorer docs](https://imgui-bundle.pages.dev/explorer/)). Preserve the existing Hook API on :8999 for agent access.
|
||||
|
||||
**Architecture:** Python 3.11-slim base image with `uv` for dependency management. The container runs `sloppy.py` in web mode (via a new `--web-host` / `--web-port` arg), which uses the imgui-bundle Hello ImGui web backend to render ImGui to a WebGL canvas in the browser. Two exposed ports: 8080 (web client) and 8999 (hook API). Volumes for project data and app state.
|
||||
|
||||
**Tech Stack:** Docker, docker-compose, Python 3.11, imgui-bundle (web backend), FastAPI/Uvicorn (existing, for hooks)
|
||||
|
||||
- **[checkpoint: 99a84bd]**
|
||||
|
||||
- **No subagents.**
|
||||
- **Pre-edit checkpoint:** `git add .` before any file edit.
|
||||
- **Per-file atomic commits.**
|
||||
- **Commit message format:** `<type>(<scope>): <imperative description>`.
|
||||
- **Git note format:** 3-8 line rationale per commit.
|
||||
- **Style baseline:** 1-space indent, no comments, type hints.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `Dockerfile` | Create | Container build for Manual Slop |
|
||||
| `docker-compose.yml` | Create | Multi-container orchestration for Unraid |
|
||||
| `scripts/docker_build.sh` | Create | Build helper |
|
||||
| `scripts/docker_run.sh` | Create | Run helper with env var wiring |
|
||||
| `sloppy.py` | Modify | Add `--web-host` and `--web-port` args |
|
||||
| `docs/guide_docker_deployment.md` | Create | Unraid setup guide |
|
||||
| `tests/test_docker_build.py` | Create | Opt-in Docker build test |
|
||||
| `pyproject.toml` | Modify | Add `docker` marker |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add web args to `sloppy.py`
|
||||
|
||||
**Files:**
|
||||
- Modify: `sloppy.py`
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Read current `sloppy.py`**
|
||||
|
||||
Use `manual-slop_read_file` to see current contents.
|
||||
|
||||
- [ ] **Step 1.3: Add the new args**
|
||||
|
||||
Find the existing argument parser setup and add the web args. If there isn't an argparse, look for how args are handled. Add:
|
||||
|
||||
```python
|
||||
# In sloppy.py, after existing argument definitions
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Manual Slop entry point")
|
||||
# ... existing args ...
|
||||
parser.add_argument("--web-host", default=None, help="Enable web mode and bind to this host (e.g., 0.0.0.0)")
|
||||
parser.add_argument("--web-port", type=int, default=8080, help="Web mode port (default: 8080)")
|
||||
parser.add_argument("--enable-test-hooks", action="store_true", help="Enable the HookServer on :8999 for external automation")
|
||||
args = parser.parse_args()
|
||||
```
|
||||
|
||||
If the existing entry point uses a different mechanism, integrate the args there.
|
||||
|
||||
- [ ] **Step 1.4: Add the web mode branch**
|
||||
|
||||
After argument parsing, before the existing main launch, add:
|
||||
|
||||
```python
|
||||
if args.web_host is not None:
|
||||
from imgui_bundle import hello_imgui
|
||||
from src.api_hooks import HookServer
|
||||
|
||||
if args.enable_test_hooks:
|
||||
hook_server = HookServer()
|
||||
hook_server.start()
|
||||
|
||||
runner_params = hello_imgui.RunnerParams()
|
||||
runner_params.app_window_params.window_title = "Manual Slop (Web)"
|
||||
runner_params.app_window_params.borderless = True
|
||||
runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_docker_space
|
||||
runner_params.app_window_params.restore_previous_window_size = True
|
||||
|
||||
from src.gui_2 import App
|
||||
app = App()
|
||||
hello_imgui.run(runner_params, lambda: app.render_frame())
|
||||
```
|
||||
|
||||
- [ ] **Step 1.5: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add sloppy.py
|
||||
git -C C:\projects\manual_slop commit -m "feat(sloppy): add --web-host and --web-port args for web mode"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Adds --web-host (enables web mode) and --web-port (default 8080) args. When --web-host is set, launches via Hello ImGui web backend with full-screen docking. Preserves --enable-test-hooks for agent access via :8999." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Create the Dockerfile
|
||||
|
||||
**Files:**
|
||||
- Create: `Dockerfile`
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Create the Dockerfile**
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git curl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install uv
|
||||
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --frozen
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /projects /config
|
||||
VOLUME ["/projects", "/config"]
|
||||
|
||||
EXPOSE 8080 8999
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||||
CMD curl -f http://127.0.0.1:8999/status || exit 1
|
||||
|
||||
ENTRYPOINT ["uv", "run", "sloppy.py", "--enable-test-hooks", "--web-host=0.0.0.0", "--web-port=8080"]
|
||||
```
|
||||
|
||||
- [ ] **Step 2.3: Add a `.dockerignore`**
|
||||
|
||||
```dockerignore
|
||||
# .dockerignore
|
||||
.git
|
||||
__pycache__
|
||||
*.pyc
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
.venv
|
||||
.env
|
||||
tests/artifacts/
|
||||
tests/logs/
|
||||
logs/
|
||||
md_gen/
|
||||
*.log
|
||||
```
|
||||
|
||||
- [ ] **Step 2.4: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add Dockerfile .dockerignore
|
||||
git -C C:\projects\manual_slop commit -m "feat(docker): add Dockerfile and .dockerignore for containerized deployment"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Python 3.11-slim base, uv for dep management, copies pyproject + uv.lock first for layer caching. Exposes 8080 (web) and 8999 (hooks). Volumes for /projects and /config. Healthcheck on hook status. .dockerignore excludes test artifacts and git." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Create the docker-compose.yml
|
||||
|
||||
**Files:**
|
||||
- Create: `docker-compose.yml`
|
||||
|
||||
- [ ] **Step 3.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Create the compose file**
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
manual_slop:
|
||||
build: .
|
||||
image: manual_slop:latest
|
||||
container_name: manual_slop
|
||||
ports:
|
||||
- "8999:8999"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /mnt/user/projects:/projects:rw
|
||||
- /mnt/user/appdata/manual_slop:/config:rw
|
||||
environment:
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||
- DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-}
|
||||
- MINIMAX_API_KEY=${MINIMAX_API_KEY:-}
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:8999/status"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
- [ ] **Step 3.3: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add docker-compose.yml
|
||||
git -C C:\projects\manual_slop commit -m "feat(docker): add docker-compose.yml for Unraid deployment"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Two ports (8999 hooks, 8080 web), two volumes (projects, appdata config), env vars for 4 providers, healthcheck on hook status. Unraid-friendly paths." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Create the build/run scripts
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/docker_build.sh`
|
||||
- Create: `scripts/docker_run.sh`
|
||||
|
||||
- [ ] **Step 4.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Create `scripts/docker_build.sh`**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# scripts/docker_build.sh
|
||||
# Build the Manual Slop Docker image.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
docker build -t manual_slop:latest .
|
||||
```
|
||||
|
||||
- [ ] **Step 4.3: Create `scripts/docker_run.sh`**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# scripts/docker_run.sh
|
||||
# Run the Manual Slop container.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
- [ ] **Step 4.4: Make scripts executable and commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add scripts/docker_build.sh scripts/docker_run.sh
|
||||
git -C C:\projects\manual_slop commit -m "feat(docker): add build and run shell scripts"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Two thin shell scripts: build (docker build -t manual_slop:latest .) and run (docker compose up -d). Will be made executable in chmod step during deployment." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Add the `docker` marker to `pyproject.toml`
|
||||
|
||||
**Files:**
|
||||
- Modify: `pyproject.toml`
|
||||
|
||||
- [ ] **Step 5.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 5.2: Add the marker**
|
||||
|
||||
Find the markers list (from Task 1 of the clean-install plan) and add:
|
||||
|
||||
```toml
|
||||
markers = [
|
||||
"integration: integration tests requiring live GUI",
|
||||
"strict: tests that require strict mode",
|
||||
"clean_install: clean install verification (opt-in via RUN_CLEAN_INSTALL_TEST=1)",
|
||||
"docker: docker build and run test (opt-in via RUN_DOCKER_TEST=1)",
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 5.3: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add pyproject.toml
|
||||
git -C C:\projects\manual_slop commit -m "test(pytest): add docker marker"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Registers the docker marker. Tests using Docker can be filtered with -m docker or -m 'not docker'." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Write the Docker build test
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_docker_build.py`
|
||||
|
||||
- [ ] **Step 6.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 6.2: Create the test file**
|
||||
|
||||
```python
|
||||
# tests/test_docker_build.py
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
IMAGE_NAME = "manual_slop:test"
|
||||
CONTAINER_NAME = "manual_slop_test_container"
|
||||
WEB_PORT = 18080
|
||||
HOOK_PORT = 18999
|
||||
|
||||
|
||||
@pytest.mark.docker
|
||||
def test_docker_image_builds(tmp_path):
|
||||
"""Build the Docker image. Slow; opt-in."""
|
||||
if os.environ.get("RUN_DOCKER_TEST") != "1":
|
||||
pytest.skip("Set RUN_DOCKER_TEST=1 to enable")
|
||||
if not _docker_available():
|
||||
pytest.skip("Docker not available in this environment")
|
||||
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
result = subprocess.run(
|
||||
["docker", "build", "-t", IMAGE_NAME, "."],
|
||||
cwd=repo_root,
|
||||
capture_output=True, text=True, timeout=600,
|
||||
)
|
||||
assert result.returncode == 0, f"Docker build failed: {result.stderr}"
|
||||
|
||||
|
||||
@pytest.mark.docker
|
||||
def test_docker_container_starts_and_responds():
|
||||
"""Run the container, verify web and hook endpoints respond."""
|
||||
if os.environ.get("RUN_DOCKER_TEST") != "1":
|
||||
pytest.skip("Set RUN_DOCKER_TEST=1 to enable")
|
||||
if not _docker_available():
|
||||
pytest.skip("Docker not available in this environment")
|
||||
|
||||
_cleanup_container()
|
||||
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker", "run", "-d",
|
||||
"--name", CONTAINER_NAME,
|
||||
"-p", f"{WEB_PORT}:8080",
|
||||
"-p", f"{HOOK_PORT}:8999",
|
||||
IMAGE_NAME,
|
||||
],
|
||||
cwd=repo_root,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, f"Docker run failed: {result.stderr}"
|
||||
|
||||
try:
|
||||
start = time.time()
|
||||
ready = False
|
||||
while time.time() - start < 90:
|
||||
try:
|
||||
r = requests.get(f"http://127.0.0.1:{HOOK_PORT}/status", timeout=1)
|
||||
if r.status_code == 200:
|
||||
ready = True
|
||||
break
|
||||
except (requests.ConnectionError, requests.Timeout):
|
||||
pass
|
||||
time.sleep(1)
|
||||
assert ready, "Container hook API did not respond within 90s"
|
||||
|
||||
r = requests.get(f"http://127.0.0.1:{WEB_PORT}/", timeout=5)
|
||||
assert r.status_code == 200
|
||||
body = r.content.lower()
|
||||
assert b"<html" in body or b"<!doctype" in body, "Web endpoint did not return HTML"
|
||||
|
||||
finally:
|
||||
_cleanup_container()
|
||||
|
||||
|
||||
def _docker_available() -> bool:
|
||||
return shutil.which("docker") is not None
|
||||
|
||||
|
||||
def _cleanup_container() -> None:
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", CONTAINER_NAME],
|
||||
capture_output=True,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 6.3: Run in skip mode**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_docker_build.py -v
|
||||
```
|
||||
|
||||
Expected: 2 skipped.
|
||||
|
||||
- [ ] **Step 6.4: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add tests/test_docker_build.py
|
||||
git -C C:\projects\manual_slop commit -m "test(docker): add opt-in build and container-run tests"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Two opt-in tests: build image (RUN_DOCKER_TEST=1), run container and verify hook + web endpoints. Skipped by default. Docker daemon required." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Write the Unraid deployment guide
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/guide_docker_deployment.md`
|
||||
|
||||
- [ ] **Step 7.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 7.2: Create the guide**
|
||||
|
||||
```markdown
|
||||
# Docker Deployment Guide (Unraid)
|
||||
|
||||
[Top](../README.md) | [Architecture](guide_architecture.md) | [Tools & IPC](guide_tools.md)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers deploying Manual Slop on Unraid (or any Docker host) using the containerized image. The deployment provides:
|
||||
- A web-accessible ImGui GUI (browser-based, no local display required)
|
||||
- The Hook API on `:8999` for agent access
|
||||
- Persistent volumes for projects and app state
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Unraid 6.10+ (or any Docker host with compose support)
|
||||
- A project share mounted at `/mnt/user/projects` (or edit `docker-compose.yml` to match your path)
|
||||
- API keys for the providers you want to use
|
||||
|
||||
## Building the Image
|
||||
|
||||
From the repo root:
|
||||
```bash
|
||||
docker build -t manual_slop:latest .
|
||||
```
|
||||
|
||||
Or use the helper:
|
||||
```bash
|
||||
./scripts/docker_build.sh
|
||||
```
|
||||
|
||||
## Running the Container
|
||||
|
||||
Edit `docker-compose.yml` to set your volume paths and provider keys (via `.env` file or environment).
|
||||
|
||||
```bash
|
||||
# Create a .env file with your API keys
|
||||
cat > .env <<EOF
|
||||
GEMINI_API_KEY=your-key-here
|
||||
ANTHROPIC_API_KEY=your-key-here
|
||||
DEEPSEEK_API_KEY=your-key-here
|
||||
MINIMAX_API_KEY=your-key-here
|
||||
EOF
|
||||
|
||||
# Start the container
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Accessing the GUI
|
||||
|
||||
Open a browser and navigate to:
|
||||
- `http://<your-unraid-ip>:8080` for the web client
|
||||
- `http://<your-unraid-ip>:8999/status` for the hook API health check
|
||||
|
||||
The web client renders the ImGui panels via WebGL. The Hello ImGui web backend streams frame deltas over WebSocket.
|
||||
|
||||
## Agent Access
|
||||
|
||||
Agents interact with the running container via the Hook API on `:8999`. Examples:
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
curl http://<your-unraid-ip>:8999/status
|
||||
|
||||
# Get MMA state
|
||||
curl http://<your-unraid-ip>:8999/api/gui/mma_status
|
||||
```
|
||||
|
||||
See [guide_tools.md](guide_tools.md) for the full Hook API reference.
|
||||
|
||||
## Volumes
|
||||
|
||||
- `/projects` — Mounted from `/mnt/user/projects` by default. Your project workspaces live here. The `manual_slop.toml` per project is in this directory.
|
||||
- `/config` — Mounted from `/mnt/user/appdata/manual_slop` by default. App state: presets, personas, log directory, workspace profiles.
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
git pull
|
||||
docker build -t manual_slop:latest .
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
Back up `/config` to preserve presets, personas, and workspace profiles. Back up `/projects/<project>/conductor/` to preserve track history.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Port conflicts:** Edit `docker-compose.yml` to change the host port (e.g., `"18080:8080"` to use 18080 on the host).
|
||||
- **Permission errors:** Ensure the Unraid share has write permissions for the container's UID.
|
||||
- **Hook API not responding:** Check `docker logs manual_slop` for the startup output. The hook server should log "HookServer started on :8999".
|
||||
```
|
||||
|
||||
- [ ] **Step 7.3: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add docs/guide_docker_deployment.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(docker): add Unraid deployment guide"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Unraid-specific setup guide: prerequisites, build, run via compose, web access URLs, agent access via hook API, volumes, updating, backup, troubleshooting." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Phase Completion Verification
|
||||
|
||||
- [ ] **Step 8.1: Verify all files exist and are syntactically valid**
|
||||
|
||||
```powershell
|
||||
# Python syntax
|
||||
python -c "import ast; ast.parse(open('sloppy.py').read())"
|
||||
|
||||
# YAML syntax (if PyYAML available)
|
||||
python -c "import yaml; yaml.safe_load(open('docker-compose.yml').read())"
|
||||
|
||||
# Dockerfile syntax (if hadolint available)
|
||||
# hadolint Dockerfile || echo "hadolint not installed, skipping"
|
||||
|
||||
# Markdown lint (skip if not configured)
|
||||
```
|
||||
|
||||
- [ ] **Step 8.2: Run the test suite in skip mode**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_docker_build.py -v
|
||||
```
|
||||
|
||||
Expected: 2 skipped.
|
||||
|
||||
- [ ] **Step 8.3: If Docker is available, run the tests**
|
||||
|
||||
```powershell
|
||||
RUN_DOCKER_TEST=1 uv run pytest tests/test_docker_build.py -v
|
||||
```
|
||||
|
||||
- [ ] **Step 8.4: Create the checkpoint commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop commit --allow-empty -m "conductor(checkpoint): Docker & web frontend complete"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Track complete. Dockerfile, docker-compose.yml, build/run scripts, --web-host arg, Unraid deployment guide, opt-in Docker build test. imgui-bundle web backend integration pending Hello ImGui runner config tuning." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** All design sections have tasks. ✓
|
||||
- **Placeholder scan:** All code blocks are complete. ✓
|
||||
- **Type consistency:** Args named consistently (`--web-host`, `--web-port`, `--enable-test-hooks`). ✓
|
||||
- **Risk acknowledged:** The web backend integration is experimental and may need iteration after testing. The checkpoint note flags this. ✓
|
||||
@@ -1,656 +0,0 @@
|
||||
# Docs Layer Refresh Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Refresh all human-facing documentation (`Readme.md`, `docs/Readme.md`, all `docs/guide_*.md`, plus new guides for subsystems that emerged since Feb 2026) to textbook / Microsoft "purple-tomb" SDK documentation fidelity. No source code changes. No new tooling. Per-file atomic commits with git notes.
|
||||
|
||||
**Architecture:** Single-agent sequential execution. Each guide is a self-contained task: audit current state, identify gaps against `conductor/product.md`, rewrite affected sections in VEFontCache-Odin style (Philosophy → Architectural Boundaries → Implementation Logic → Verification), validate cross-doc links, commit per-file, attach git note. New guides are written only for subsystems explicitly listed in `conductor/product.md` that lack coverage.
|
||||
|
||||
**Tech Stack:** Manual Slop's MCP research tools (`manual-slop_get_file_summary`, `manual-slop_py_get_skeleton`, `manual-slop_py_get_code_outline`, `manual-slop_get_git_diff`, `manual-slop_search_files`). Git for version control. Markdown for documentation.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-comprehensive-documentation-refresh-design.md` §4 (Sub-Track 1).
|
||||
|
||||
---
|
||||
|
||||
## Execution Constraints
|
||||
|
||||
These apply to every task in this plan:
|
||||
|
||||
- **No subagents.** Execute as a single agent. No Tier 3/Tier 4 delegation. No parallel workers.
|
||||
- **Pre-edit checkpoint:** Before any file edit, run `git add .` to stage current state. Protects against any subsequent `git restore` mishap.
|
||||
- **Per-file atomic commits:** One file = one commit. Never batch.
|
||||
- **Commit message format:** `docs(<scope>): <imperative description>` (e.g., `docs(architecture): document RAG engine integration`).
|
||||
- **Git note format:** Attach a 3-8 line rationale to each commit via `git notes add -m "<note>" <sha>`. The note should list: source files cross-referenced, subsystems updated, any decisions made.
|
||||
- **Style baseline:** VEFontCache-Odin pattern. Philosophy → Architectural Boundaries → Implementation Logic → Verification. No "In this section..." filler. Symbol names match source exactly.
|
||||
- **Link validation:** For every `[text](./relative/path)` link in a modified file, verify the target exists via `manual-slop_search_files` or `manual-slop_list_directory`.
|
||||
- **"Textbook / purple-tomb fidelity" concretely means:** for every public class/function/event mentioned, the doc states its signature, threading constraints, side effects, and at least one usage example. Internal algorithms explained step-by-step. State machines include the full transition table.
|
||||
- **No new comments added to source code.** Docs live in `/docs`, not inline.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Inventory and Gap Analysis
|
||||
|
||||
**Files:**
|
||||
- Read: `conductor/product.md`
|
||||
- Read: `docs/Readme.md`
|
||||
- Read: each `docs/guide_*.md` (use `manual-slop_get_file_summary` for first pass)
|
||||
- Read: `docs/MMA_Support/*` (use `manual-slop_get_file_summary` for first pass)
|
||||
|
||||
**Output:** A gap table written into the parent track's `conductor/tracks/documentation_refresh_comprehensive_20260602/gap_analysis.md` (create the parent track directory in this task if it doesn't exist).
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Create the parent track directory**
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Path "C:\projects\manual_slop\conductor\tracks\documentation_refresh_comprehensive_20260602" -Force
|
||||
```
|
||||
|
||||
- [ ] **Step 1.3: Read `conductor/product.md` to get the canonical feature list**
|
||||
|
||||
Use `manual-slop_get_file_summary` first. If the file is summarized well, proceed to extract feature names. If you need the full list, use `manual-slop_get_file_slice` with a generous range.
|
||||
|
||||
Identify every distinct feature/subsystem mentioned (e.g., Beads mode, RAG, Hot Reload, 4-Tier MMA, 26 MCP tools, Multi-Provider, etc.).
|
||||
|
||||
- [ ] **Step 1.4: Read every `docs/guide_*.md` to map current coverage**
|
||||
|
||||
For each guide:
|
||||
- Use `manual-slop_get_file_summary` for a quick read
|
||||
- Note which subsystems from `conductor/product.md` it covers
|
||||
- Note which subsystems it does NOT cover
|
||||
|
||||
Files to inventory:
|
||||
- `docs/guide_architecture.md`
|
||||
- `docs/guide_mma.md`
|
||||
- `docs/guide_tools.md`
|
||||
- `docs/guide_simulations.md`
|
||||
- `docs/guide_context_curation.md`
|
||||
- `docs/guide_shaders_and_window.md`
|
||||
- `docs/guide_meta_boundary.md`
|
||||
|
||||
- [ ] **Step 1.5: Read `docs/Readme.md` to confirm what's indexed**
|
||||
|
||||
Verify which guides are linked from the documentation index. Flag any `docs/guide_*.md` that is NOT in the index (likely candidates: `guide_context_curation.md`, `guide_shaders_and_window.md`).
|
||||
|
||||
- [ ] **Step 1.6: Write the gap analysis**
|
||||
|
||||
Create `conductor/tracks/documentation_refresh_comprehensive_20260602/gap_analysis.md` with this structure:
|
||||
|
||||
```markdown
|
||||
# Docs Layer Gap Analysis
|
||||
|
||||
**Date:** 2026-06-02
|
||||
**Source of truth for features:** `conductor/product.md`
|
||||
**Source of truth for guides:** `docs/Readme.md` and `docs/guide_*.md`
|
||||
|
||||
## Existing Guides and Their Coverage
|
||||
|
||||
| Guide | Covers | Does NOT cover (gaps) |
|
||||
|---|---|---|
|
||||
| `docs/guide_architecture.md` | [list] | [list] |
|
||||
| `docs/guide_mma.md` | [list] | [list] |
|
||||
| ... | ... | ... |
|
||||
|
||||
## Subsystems Without Dedicated Guides
|
||||
|
||||
| Subsystem | Source module(s) | Recommendation |
|
||||
|---|---|---|
|
||||
| RAG | `src/rag_engine.py` | NEW: `docs/guide_rag.md` |
|
||||
| Beads | `src/beads_client.py` | NEW: `docs/guide_beads.md` |
|
||||
| ... | ... | ... |
|
||||
|
||||
## Existing Guides That Are Stale
|
||||
|
||||
| Guide | Last meaningful update | Stale sections |
|
||||
|---|---|---|
|
||||
| ... | ... | ... |
|
||||
|
||||
## Cross-Cutting Issues
|
||||
|
||||
- Files in `docs/MMA_Support/*` not linked from `docs/Readme.md`: [list]
|
||||
- Guides that reference each other via broken links: [list]
|
||||
- Symbols in docs that don't match current source: [list, if any found during the read pass]
|
||||
```
|
||||
|
||||
- [ ] **Step 1.7: Commit the gap analysis**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add conductor/tracks/documentation_refresh_comprehensive_20260602/gap_analysis.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(track): docs layer gap analysis for comprehensive refresh"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Subsystem coverage map for docs layer refresh. Each existing guide's coverage and gaps listed. New guides recommended for unlisted subsystems." $_ }
|
||||
```
|
||||
|
||||
- [ ] **Step 1.8: Record SHA in plan**
|
||||
|
||||
After committing, note the commit SHA. This gap analysis becomes the source of truth for all subsequent tasks in this plan.
|
||||
|
||||
```markdown
|
||||
Gap analysis commit SHA: [paste here]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Update the Documentation Index (`docs/Readme.md`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/Readme.md`
|
||||
- Test: Manual link validation (every `[text](./relative/path)` target exists)
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Read current `docs/Readme.md`**
|
||||
|
||||
Use `manual-slop_read_file` (file is small enough).
|
||||
|
||||
- [ ] **Step 2.3: Identify missing guide links**
|
||||
|
||||
Cross-reference the Guides table in `docs/Readme.md` against the list of `docs/guide_*.md` files (from Task 1). Flag every guide that is NOT in the index.
|
||||
|
||||
- [ ] **Step 2.4: Add the missing guide entries**
|
||||
|
||||
For each missing guide, add a row to the Guides table:
|
||||
|
||||
```markdown
|
||||
| [Context Curation](guide_context_curation.md) | [contents summary] |
|
||||
| [Shaders & Window](guide_shaders_and_window.md) | [contents summary] |
|
||||
```
|
||||
|
||||
The "[contents summary]" should be 1-2 lines describing the guide's scope, written in the same high-density style as the existing rows.
|
||||
|
||||
- [ ] **Step 2.5: If Task 1.6 recommended new guides, add placeholder rows for them too**
|
||||
|
||||
```markdown
|
||||
| [RAG](guide_rag.md) | [contents summary — written in Task N] |
|
||||
| [Beads](guide_beads.md) | [contents summary — written in Task N] |
|
||||
```
|
||||
|
||||
Use `[contents summary — written in Task N]` as a placeholder if the new guide has not been written yet. Update the row once the new guide is complete.
|
||||
|
||||
- [ ] **Step 2.6: Decide fate of `docs/MMA_Support/*`**
|
||||
|
||||
Options:
|
||||
- (a) Keep as legacy, add a "Legacy MMA Reference" section to the index that links them
|
||||
- (b) Merge into `docs/guide_mma.md` and delete the MMA_Support directory
|
||||
- (c) Leave as-is, not linked
|
||||
|
||||
Default: (a) — keep as legacy, add a "Legacy MMA Reference (Deprecated)" section at the bottom of the index. This preserves historical context without confusing new readers.
|
||||
|
||||
- [ ] **Step 2.7: Verify every link in the modified file resolves**
|
||||
|
||||
For every `[text](./relative/path)` in the updated `docs/Readme.md`:
|
||||
- Use `manual-slop_search_files` to confirm the target exists
|
||||
- If broken, either fix the link or remove the row
|
||||
|
||||
- [ ] **Step 2.8: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/Readme.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(index): link guide_context_curation and guide_shaders_and_window; add legacy MMA section"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Linked previously-unlinked guides. Added Legacy MMA Reference section per gap analysis decision. All cross-doc links verified." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Rewrite `docs/guide_architecture.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/guide_architecture.md` (current size ~34.5K)
|
||||
- Reference: `src/gui_2.py`, `src/ai_client.py`, `src/multi_agent_conductor.py`, `src/dag_engine.py`, `src/events.py`, `src/app_controller.py`, `src/api_hooks.py`
|
||||
|
||||
**Scope:** Cover all threading domains, all event types, all state machines, the Execution Clutch HITL flow, multi-provider AI client architecture, caching strategies, and any new subsystems that interact with these (RAG retrieval, Beads sync, Hot Reload lifecycle hooks).
|
||||
|
||||
- [ ] **Step 3.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Read the current guide**
|
||||
|
||||
```powershell
|
||||
# Use skeleton/summary first
|
||||
manual-slop_get_file_summary path="C:\projects\manual_slop\docs\guide_architecture.md"
|
||||
```
|
||||
|
||||
Then read sections as needed. The file is large (~34.5K) so use `manual-slop_get_file_slice` with explicit ranges.
|
||||
|
||||
- [ ] **Step 3.3: Audit the source code for changes since the last update**
|
||||
|
||||
For each major source file referenced:
|
||||
- Use `manual-slop_get_git_diff path="<file>" base_rev="HEAD~30"` to see what changed in the last 30 commits
|
||||
- Use `manual-slop_py_get_skeleton` and `manual-slop_py_get_code_outline` to map current structure
|
||||
- Note any new classes, methods, events, state machines
|
||||
|
||||
- [ ] **Step 3.4: Identify which sections need rewriting**
|
||||
|
||||
Based on the diff, mark each section as:
|
||||
- **Stale:** Needs rewrite
|
||||
- **Missing:** Subsystem not covered at all
|
||||
- **Current:** No changes needed
|
||||
|
||||
If you find that a major subsystem is missing (e.g., RAG integration with the event system, Beads lifecycle hooks, Hot Reload interaction with the threading model), add a new section to the guide.
|
||||
|
||||
- [ ] **Step 3.5: Rewrite stale and missing sections**
|
||||
|
||||
Apply the VEFontCache-Odin style:
|
||||
- **Philosophy:** Mental model. Why does this subsystem exist? What problem does it solve? What are the trade-offs?
|
||||
- **Architectural Boundaries:** Which thread/domain owns it? What are the interfaces? What cannot cross?
|
||||
- **Implementation Logic:** State machines with full transition tables. Locking strategy. Event flow with sequence. Public class/function signatures.
|
||||
- **Verification:** How is it tested? What edge cases are covered? What can fail?
|
||||
|
||||
For "textbook / purple-tomb fidelity": for every public class/function/event mentioned, state its signature, threading constraints, side effects, and at least one usage example.
|
||||
|
||||
- [ ] **Step 3.6: Validate all cross-doc links**
|
||||
|
||||
For every `[text](./relative/path)` in the modified file:
|
||||
- Use `manual-slop_search_files` to confirm target exists
|
||||
- If broken, fix the link
|
||||
|
||||
- [ ] **Step 3.7: Spot-check 3 random symbols against the source**
|
||||
|
||||
Pick 3 random public class/function names mentioned in the rewritten sections. Use `manual-slop_py_find_usages` to confirm the symbol exists in the source with the same name.
|
||||
|
||||
- [ ] **Step 3.8: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/guide_architecture.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(architecture): refresh for RAG, Beads, Hot Reload, and post-Feb-2026 changes"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Rewrote [list specific sections]. Cross-referenced [list source files]. New sections: [list]. Decisions: [list any judgment calls]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Rewrite `docs/guide_mma.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/guide_mma.md`
|
||||
- Reference: `src/multi_agent_conductor.py`, `src/dag_engine.py`, `src/conductor_tech_lead.py`, `src/mma_prompts.py`, `src/models.py`
|
||||
|
||||
**Scope:** The 4-Tier MMA hierarchy, Ticket/Track/WorkerContext data structures, DAG engine (Kahn's algorithm, cycle detection, transitive blocking propagation), ConductorEngine execution loop, Tier 2 ticket generation, Tier 3 worker lifecycle with Context Amnesia, Tier 4 QA integration, token firewalling, model escalation. Add: persona assignment, RAG-augmented worker context, Beads-backed track state, Hot Reload integration with worker spawning.
|
||||
|
||||
- [ ] **Step 4.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Read the current guide and source code**
|
||||
|
||||
Use `manual-slop_get_file_summary` for the guide, then `manual-slop_py_get_skeleton` for each source file.
|
||||
|
||||
- [ ] **Step 4.3: Audit changes since last update**
|
||||
|
||||
Use `manual-slop_get_git_diff` for each source file. Note: the MMA system has evolved substantially (personas, RAG, Beads, Hot Reload interaction).
|
||||
|
||||
- [ ] **Step 4.4: Identify stale / missing sections**
|
||||
|
||||
Pay particular attention to:
|
||||
- **Persona integration:** How are personas assigned to MMA tiers? Where in the worker lifecycle?
|
||||
- **RAG augmentation:** When does a worker receive RAG-retrieved context? How is the chunk selection made?
|
||||
- **Beads integration:** Are tracks stored in Beads when Beads mode is active? How does the transition work?
|
||||
- **Hot Reload interaction:** When a Tier 3 worker finishes and the GUI is reloaded, what happens to the worker's session?
|
||||
|
||||
- [ ] **Step 4.5: Rewrite stale and missing sections**
|
||||
|
||||
VEFontCache-Odin style. The DAG engine algorithms in particular should be explained step-by-step (Kahn's algorithm pseudocode, cycle detection using iterative DFS, cascade_blocks with transitive propagation).
|
||||
|
||||
- [ ] **Step 4.6: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 4.7: Spot-check 3 random symbols against the source**
|
||||
|
||||
- [ ] **Step 4.8: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/guide_mma.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(mma): refresh for personas, RAG augmentation, Beads integration, Hot Reload interaction"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Rewrote [sections]. Cross-referenced [files]. New sections: [list]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Rewrite `docs/guide_tools.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/guide_tools.md` (current size ~22.9K)
|
||||
- Reference: `src/mcp_client.py`, `src/api_hooks.py`, `src/api_hook_client.py`, `src/shell_runner.py`
|
||||
|
||||
**Scope:** All 26+ MCP tools (current count), the 3-layer security model (Allowlist Construction → Path Validation → Resolution Gate), the Hook API GET/POST endpoints, `ApiHookClient` method reference, the `/api/ask` synchronous HITL protocol, the shell runner. Add: any new tools added since Feb 2026 (RAG tools, Beads tools, tree-sitter Lua/GDScript/C# tools if implemented, persona tools, structural file editor tools).
|
||||
|
||||
- [ ] **Step 5.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 5.2: Read the current guide**
|
||||
|
||||
- [ ] **Step 5.3: Audit current tool inventory in `src/mcp_client.py`**
|
||||
|
||||
Use `manual-slop_py_get_skeleton` and `manual-slop_py_get_code_outline` to get the current list of registered tools. Count them. Compare to the count in the guide.
|
||||
|
||||
- [ ] **Step 5.4: For each tool, document or update its entry**
|
||||
|
||||
For every tool:
|
||||
- **Signature:** Exact function signature
|
||||
- **Security constraints:** Which of the 3 layers it passes through
|
||||
- **Side effects:** What does it mutate? What does it return?
|
||||
- **Edge cases:** Empty input, missing file, permission denied, etc.
|
||||
- **Usage example:** At least one example showing the call site and expected return
|
||||
|
||||
If a tool is missing from the guide, add a full entry. If an entry is stale, rewrite it.
|
||||
|
||||
- [ ] **Step 5.5: Update the Hook API endpoint reference**
|
||||
|
||||
Use `manual-slop_py_get_skeleton` on `src/api_hooks.py` to get the current list of routes. For each route, document request/response format.
|
||||
|
||||
- [ ] **Step 5.6: Update the `ApiHookClient` method reference**
|
||||
|
||||
Use `manual-slop_py_get_class_summary` for the `ApiHookClient` class. Document each public method.
|
||||
|
||||
- [ ] **Step 5.7: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 5.8: Spot-check 3 random symbols against the source**
|
||||
|
||||
- [ ] **Step 5.9: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/guide_tools.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(tools): refresh tool inventory, Hook API, and ApiHookClient reference"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Updated tool count to [N]. Rewrote [sections]. New tool entries: [list]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Rewrite `docs/guide_simulations.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/guide_simulations.md`
|
||||
- Reference: `tests/conftest.py`, `simulation/sim_base.py`, `simulation/workflow_sim.py`, `simulation/user_agent.py`, `tests/mock_gemini_cli.py`, `src/api_hook_client.py`
|
||||
|
||||
**Scope:** The `live_gui` pytest fixture (lifecycle, readiness polling, failure path, teardown, session isolation via `reset_ai_client`), the Puppeteer pattern (8-stage MMA simulation), the mock provider strategy, the VerificationLogger, process cleanup (`kill_process_tree`), visual verification patterns. Add: any new test infrastructure added since Feb 2026 (extended simulations, async tool tests, discussion metrics tests, structural file editor tests, command palette tests).
|
||||
|
||||
- [ ] **Step 6.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 6.2: Read the current guide**
|
||||
|
||||
- [ ] **Step 6.3: Audit current simulation infrastructure**
|
||||
|
||||
Use `manual-slop_get_file_summary` on `tests/conftest.py` and each `simulation/*.py` file. Note any new fixtures, base classes, or patterns.
|
||||
|
||||
- [ ] **Step 6.4: Identify stale / missing sections**
|
||||
|
||||
- [ ] **Step 6.5: Rewrite stale and missing sections**
|
||||
|
||||
VEFontCache-Odin style. Particular attention to:
|
||||
- The `live_gui` fixture's full lifecycle (startup → readiness polling → test execution → teardown)
|
||||
- The mock provider's JSON-L protocol (input mechanisms, response routing, output protocol)
|
||||
- Process cleanup for Windows (the `kill_process_tree` implementation)
|
||||
- The new extended simulation patterns (if any)
|
||||
|
||||
- [ ] **Step 6.6: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 6.7: Spot-check 3 random symbols against the source**
|
||||
|
||||
- [ ] **Step 6.8: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/guide_simulations.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(simulations): refresh test infrastructure, mock provider, and Puppeteer pattern"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Rewrote [sections]. New content: [list]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Refresh `docs/guide_context_curation.md` and `docs/guide_shaders_and_window.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/guide_context_curation.md` (current size ~8K)
|
||||
- Modify: `docs/guide_shaders_and_window.md` (current size ~3.4K)
|
||||
- Reference: `src/file_cache.py`, `src/summarize.py`, `src/outline_tool.py`, `src/summary_cache.py`, `src/fuzzy_anchor.py`, `src/shaders.py`, `src/bg_shader.py`, `src/imgui_scopes.py`
|
||||
|
||||
**Scope:** For `guide_context_curation.md`: skeleton/curated/targeted view algorithms, fuzzy anchor algorithm, summary cache strategy, file aggregation pipeline, view modes. For `guide_shaders_and_window.md`: shader pipeline, custom window frame manipulation, pywin32 integration, NERV theme FX.
|
||||
|
||||
- [ ] **Step 7.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 7.2: Read both guides**
|
||||
|
||||
- [ ] **Step 7.3: Audit changes in referenced source files**
|
||||
|
||||
- [ ] **Step 7.4: Rewrite stale sections in `guide_context_curation.md`**
|
||||
|
||||
- [ ] **Step 7.5: Commit `guide_context_curation.md`**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/guide_context_curation.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(context-curation): refresh view algorithms and fuzzy anchor"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Rewrote [sections]. Cross-referenced [files]." $_ }
|
||||
```
|
||||
|
||||
- [ ] **Step 7.6: Rewrite stale sections in `guide_shaders_and_window.md`**
|
||||
|
||||
- [ ] **Step 7.7: Commit `guide_shaders_and_window.md`**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/guide_shaders_and_window.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(shaders): refresh shader pipeline and window frame"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Rewrote [sections]. Cross-referenced [files]." $_ }
|
||||
```
|
||||
|
||||
- [ ] **Step 7.8: Validate all cross-doc links in both files**
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Refresh `docs/guide_meta_boundary.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/guide_meta_boundary.md` (current size ~4.6K)
|
||||
- Reference: `scripts/mma_exec.py`, `scripts/claude_mma_exec.py`, `scripts/tool_call.py`, `scripts/mcp_server.py`, `mma-orchestrator/SKILL.md`, `.gemini/`, `.claude/`, `.opencode/`
|
||||
|
||||
**Scope:** The Application domain (Strict HITL — `gui_2.py`, `ai_client.py`, `multi_agent_conductor.py`, `dag_engine.py`) vs the Meta-Tooling domain (agent skill files and orchestration scripts). Inter-Domain Bridges (`cli_tool_bridge.py`, `claude_tool_bridge.py`). The `GEMINI_CLI_HOOK_CONTEXT` environment variable. Safety model separation.
|
||||
|
||||
- [ ] **Step 8.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 8.2: Read the current guide and the meta-tooling files**
|
||||
|
||||
- [ ] **Step 8.3: Rewrite stale sections**
|
||||
|
||||
Pay attention to:
|
||||
- Is `claude_mma_exec.py` still in active use? (User indicated Claude is no longer the primary toolchain — may be vestigial.)
|
||||
- Is `cli_tool_bridge.py` still in active use?
|
||||
- Are there new meta-tooling scripts not yet documented?
|
||||
|
||||
- [ ] **Step 8.4: Validate all cross-doc links**
|
||||
|
||||
- [ ] **Step 8.5: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/guide_meta_boundary.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(meta-boundary): refresh Application vs Meta-Tooling domain separation"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Rewrote [sections]. Cross-referenced [files]." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Update `Readme.md` (top-level)
|
||||
|
||||
**Files:**
|
||||
- Modify: `Readme.md` (current size ~15.5K)
|
||||
- Reference: every `docs/guide_*.md` (for the new Subsystem Index)
|
||||
|
||||
**Scope:** Add a "Module by Domain" reference table (links to each guide), a "Subsystem Index" (cross-references each major feature to its dedicated guide), update setup prerequisites (Beads CLI, Dolt, ChromaDB, etc.), update the Tech Stack section to reflect current dependencies.
|
||||
|
||||
- [ ] **Step 9.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 9.2: Read the current `Readme.md`**
|
||||
|
||||
- [ ] **Step 9.3: Add a "Module by Domain" section**
|
||||
|
||||
Insert a table that links to each `docs/guide_*.md`:
|
||||
|
||||
```markdown
|
||||
## Module by Domain
|
||||
|
||||
For deep dives into each subsystem, see the dedicated guide:
|
||||
|
||||
| Subsystem | Guide |
|
||||
|---|---|
|
||||
| Threading, events, AI client, HITL | [docs/guide_architecture.md](docs/guide_architecture.md) |
|
||||
| 4-Tier MMA, DAG, worker lifecycle | [docs/guide_mma.md](docs/guide_mma.md) |
|
||||
| MCP tools, Hook API, shell runner | [docs/guide_tools.md](docs/guide_tools.md) |
|
||||
| Test infrastructure, mock provider, Puppeteer | [docs/guide_simulations.md](docs/guide_simulations.md) |
|
||||
| Skeleton/curated/targeted views, fuzzy anchors | [docs/guide_context_curation.md](docs/guide_context_curation.md) |
|
||||
| Shader pipeline, custom window frame | [docs/guide_shaders_and_window.md](docs/guide_shaders_and_window.md) |
|
||||
| Application vs Meta-Tooling domain | [docs/guide_meta_boundary.md](docs/guide_meta_boundary.md) |
|
||||
| [RAG, Beads, etc. once new guides exist] | [docs/guide_rag.md](docs/guide_rag.md) |
|
||||
```
|
||||
|
||||
- [ ] **Step 9.4: Add a "Subsystem Index" section**
|
||||
|
||||
A cross-reference of every major feature to its dedicated guide. Use the gap analysis from Task 1 as the source.
|
||||
|
||||
- [ ] **Step 9.5: Update setup prerequisites**
|
||||
|
||||
Check the current `pyproject.toml` (use `manual-slop_get_file_summary`) and confirm the README's prerequisites are accurate. Add any missing dependencies (e.g., `chromadb` for RAG, `dolt` for Beads).
|
||||
|
||||
- [ ] **Step 9.6: Update the Tech Stack section**
|
||||
|
||||
Add providers (MiniMax if not already there), new SDKs (any new ones), new tooling (e.g., ChromaDB).
|
||||
|
||||
- [ ] **Step 9.7: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add Readme.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(readme): add Module by Domain, Subsystem Index, and updated setup"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Added Module by Domain table. Added Subsystem Index. Updated prerequisites to include [list]. Updated Tech Stack." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Write New Guides for Unlisted Subsystems
|
||||
|
||||
**Files:**
|
||||
- Create (one per new guide): `docs/guide_<name>.md`
|
||||
- Reference: the relevant `src/<module>.py` and tests
|
||||
|
||||
**Scope:** Per the gap analysis from Task 1, write new guides for any subsystem that:
|
||||
1. Is explicitly listed in `conductor/product.md`
|
||||
2. Does NOT have a corresponding guide yet
|
||||
3. Is substantial enough to warrant a dedicated guide (>500 lines of code in the primary module)
|
||||
|
||||
**Likely candidates** (confirm via gap analysis):
|
||||
- RAG (`docs/guide_rag.md`) — `src/rag_engine.py`
|
||||
- Beads (`docs/guide_beads.md`) — `src/beads_client.py`
|
||||
- Hot Reload (`docs/guide_hot_reload.md`) — `src/hot_reloader.py`
|
||||
- Discussion Metrics & Compression (`docs/guide_discussion_metrics.md`) — `src/ai_client.py` (relevant section)
|
||||
- Command Palette (`docs/guide_command_palette.md`) — `src/gui_2.py` (relevant section)
|
||||
- Structural File Editor (`docs/guide_structural_editor.md`) — `src/gui_2.py` (relevant section)
|
||||
- NERV Theme (`docs/guide_nerv_theme.md`) — `src/theme_nerv.py`, `src/theme_nerv_fx.py`
|
||||
|
||||
- [ ] **Step 10.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 10.2: For each new guide (one per subsystem), follow this per-subsystem workflow**
|
||||
|
||||
For each subsystem in the gap analysis's "Subsystems Without Dedicated Guides" table:
|
||||
|
||||
**10.2.1: Read the source module(s)**
|
||||
- Use `manual-slop_py_get_skeleton` and `manual-slop_py_get_code_outline` first
|
||||
- Read specific sections via `manual-slop_get_file_slice` for implementation details
|
||||
|
||||
**10.2.2: Read any existing tests for the subsystem**
|
||||
- Use `manual-slop_get_file_summary` on the relevant `test_*.py` files
|
||||
- Note what's tested (gives you the public API surface)
|
||||
|
||||
**10.2.3: Write the new guide in VEFontCache-Odin style**
|
||||
- **Philosophy:** What problem does this subsystem solve? What design alternatives were considered?
|
||||
- **Architectural Boundaries:** Which thread/domain owns it? What are the public interfaces? What cannot cross?
|
||||
- **Implementation Logic:** State machines, algorithms step-by-step, public class/function signatures with side effects, threading constraints
|
||||
- **Verification:** How is it tested? What edge cases? What can fail?
|
||||
|
||||
**10.2.4: Commit per-guide**
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add docs/guide_<name>.md
|
||||
git -C C:\projects\manual_slop commit -m "docs(<name>): new guide for [subsystem]"
|
||||
git -C C:\projects\manual_slop log -1 --format="%H" | ForEach-Object { git -C C:\projects\manual_slop notes add -m "New guide covering [subsystem]. Cross-referenced [files]. Public API surface documented." $_ }
|
||||
```
|
||||
|
||||
**10.2.5: Update `docs/Readme.md` to link the new guide**
|
||||
- Go back to Task 2's modified `docs/Readme.md` and update the placeholder row for this guide with the real "[contents summary]"
|
||||
- Commit as a separate small commit: `docs(index): finalize [subsystem] guide link`
|
||||
|
||||
- [ ] **Step 10.3: Validate all cross-doc links across all new guides**
|
||||
|
||||
For every `[text](./relative/path)` in any new guide, verify the target exists.
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Sub-Track 1 Verification
|
||||
|
||||
**Files:**
|
||||
- Read: every modified `docs/guide_*.md` and `Readme.md`
|
||||
|
||||
- [ ] **Step 11.1: Cross-link audit**
|
||||
|
||||
For every modified markdown file, run a manual grep for `[text](./relative/path)` and verify each target exists via `manual-slop_search_files`.
|
||||
|
||||
- [ ] **Step 11.2: Symbol parity spot-check**
|
||||
|
||||
For 3 random public symbols mentioned in each rewritten guide, use `manual-slop_py_find_usages` to confirm the symbol exists with the same name in the source.
|
||||
|
||||
- [ ] **Step 11.3: Subsystem coverage check**
|
||||
|
||||
For every feature listed in `conductor/product.md`, confirm either (a) a dedicated guide exists, or (b) the feature is covered in a related guide with a link from `Readme.md`.
|
||||
|
||||
- [ ] **Step 11.4: Per-file commit verification**
|
||||
|
||||
Run `git log --oneline conductor/tracks/documentation_refresh_comprehensive_20260602/.. -- docs/ Readme.md` and confirm every file change has its own commit.
|
||||
|
||||
- [ ] **Step 11.5: Git note verification**
|
||||
|
||||
Run `git log --format="%H" -- docs/ Readme.md | ForEach-Object { git notes show $_ }` and confirm every commit has a non-empty git note.
|
||||
|
||||
- [ ] **Step 11.6: Phase completion checkpoint**
|
||||
|
||||
Per the project's Phase Completion Verification protocol in `conductor/workflow.md`:
|
||||
- Run the automated test suite (or at least the docs-related tests, if any)
|
||||
- Run the API hook verification if applicable
|
||||
- Present the verification report to the user
|
||||
- Get user sign-off
|
||||
- Create the checkpoint commit: `conductor(checkpoint): Sub-Track 1 (docs layer refresh) complete`
|
||||
- Attach the verification report as a git note
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (after writing this plan)
|
||||
|
||||
1. **Spec coverage:** Sections 4.3-4.6 of the spec (Phase 1.3-1.6) are covered by Tasks 3-11. Phase 1.1 (Inventory) is Task 1. Phase 1.2 (Index update) is Task 2. Phase 1.5 (New guides) is Task 10. Phase 1.6 (Verification) is Task 11. ✓ All phases covered.
|
||||
2. **Placeholder scan:** No "TBD" / "TODO" / "similar to Task N" markers. Specific file paths and commit commands throughout. ✓
|
||||
3. **Type/symbol consistency:** Tasks reference consistent file paths, module names, and symbol names throughout. The VEFontCache-Odin pattern is applied consistently. ✓
|
||||
4. **Risk check:** Pre-edit checkpoints, per-file commits, and git notes mitigate the codebase-evolving-underneath risk. Manual link validation mitigates broken links. User sign-off at the end of each sub-track is enforced via the Phase Completion Verification protocol. ✓
|
||||
@@ -1,427 +0,0 @@
|
||||
# Test Consolidation & TOML Sandboxing Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Audit tests for real-TOML usage, migrate offenders to sandboxed patterns, consolidate similar tests where it improves clarity, and enforce the rule going forward.
|
||||
|
||||
**Architecture:** `scripts/check_test_toml_paths.py` is the audit tool. `tests/conftest.py` gets a new autouse fixture `enforce_no_real_toml`. Migration follows the existing `isolate_workspace` pattern (already in conftest.py) using `tmp_path` + `monkeypatch`. Consolidation is judgment-call per area.
|
||||
|
||||
**Tech Stack:** Python 3.11+, pytest, regex (stdlib)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-test-consolidation-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Execution Constraints
|
||||
|
||||
- **No subagents.** Execute as a single agent.
|
||||
- **Pre-edit checkpoint:** `git add .` before any file edit.
|
||||
- **Per-file atomic commits:** One file = one commit.
|
||||
- **Commit message format:** `<type>(<scope>): <imperative description>`.
|
||||
- **Git note format:** 3-8 line rationale per commit.
|
||||
- **Style baseline:** 1-space indent, no comments, type hints.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `scripts/check_test_toml_paths.py` | Create | Greps for direct `./<name>.toml` references in tests; CI gate |
|
||||
| `tests/conftest.py` | Modify | Add `enforce_no_real_toml` autouse fixture |
|
||||
| `tests/test_enforce_no_real_toml.py` | Create | Tests for the enforcer itself |
|
||||
| Various `tests/test_*.py` | Modify | Migrate offenders to sandboxed pattern |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Build the audit script
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/check_test_toml_paths.py`
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Create the audit script**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# scripts/check_test_toml_paths.py
|
||||
"""Detect tests that read/write real TOML files. Used as a CI gate.
|
||||
|
||||
Run from repo root: python scripts/check_test_toml_paths.py
|
||||
Exits 0 if all tests use sandboxed paths, 1 otherwise.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TOML_BASENAMES = {
|
||||
"manual_slop", "config", "credentials",
|
||||
"presets", "personas", "tool_presets",
|
||||
"workspace_profiles", "tool_presets",
|
||||
}
|
||||
|
||||
PATTERNS = [
|
||||
re.compile(rf'Path\(["\'](?:{"|".join(TOML_BASENAMES)})\.toml["\']'),
|
||||
re.compile(rf'open\(["\'](?:{"|".join(TOML_BASENAMES)})\.toml["\']'),
|
||||
re.compile(rf'["\']\.{{1,2}}/(?:{"|".join(TOML_BASENAMES)})\.toml["\']'),
|
||||
re.compile(rf'Path\(["\']\.\./(?:{"|".join(TOML_BASENAMES)})\.toml["\']'),
|
||||
]
|
||||
|
||||
EXCLUDE_DIRS = {"artifacts", "logs", "__pycache__", "snapshots"}
|
||||
|
||||
|
||||
def find_violations(tests_dir: Path) -> list[tuple[Path, int, str]]:
|
||||
violations = []
|
||||
for test_file in tests_dir.rglob("test_*.py"):
|
||||
if any(excluded in test_file.parts for excluded in EXCLUDE_DIRS):
|
||||
continue
|
||||
try:
|
||||
content = test_file.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
for lineno, line in enumerate(content.splitlines(), start=1):
|
||||
for pattern in PATTERNS:
|
||||
if pattern.search(line):
|
||||
violations.append((test_file, lineno, line.strip()))
|
||||
break
|
||||
return violations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
tests_dir = repo_root / "tests"
|
||||
if not tests_dir.exists():
|
||||
print(f"Tests dir not found: {tests_dir}", file=sys.stderr)
|
||||
return 1
|
||||
violations = find_violations(tests_dir)
|
||||
if not violations:
|
||||
print("OK: No tests reference real TOML files.")
|
||||
return 0
|
||||
print(f"FAIL: {len(violations)} test(s) reference real TOML files:")
|
||||
for path, lineno, line in violations:
|
||||
rel = path.relative_to(repo_root)
|
||||
print(f" {rel}:{lineno}: {line}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
- [ ] **Step 1.3: Run the script (expect failures)**
|
||||
|
||||
```powershell
|
||||
python scripts/check_test_toml_paths.py
|
||||
```
|
||||
|
||||
Expected: Outputs a list of violations (since the migration hasn't happened yet).
|
||||
|
||||
- [ ] **Step 1.4: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add scripts/check_test_toml_paths.py
|
||||
git -C C:\projects\manual_slop commit -m "feat(tests): add check_test_toml_paths.py audit script"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Greps tests/ for direct ./<name>.toml references. Excludes artifacts, logs, __pycache__, snapshots. Exits 0 on clean, 1 on violations. Used as CI gate." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add the enforcement fixture
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/conftest.py`
|
||||
- Create: `tests/test_enforce_no_real_toml.py`
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Read current `tests/conftest.py` to find insertion point**
|
||||
|
||||
Use `manual-slop_py_get_code_outline` to see the existing fixtures (especially `isolate_workspace` at line 71).
|
||||
|
||||
- [ ] **Step 2.3: Add the `enforce_no_real_toml` fixture**
|
||||
|
||||
Add near the existing `isolate_workspace` fixture (around line 70-90):
|
||||
|
||||
```python
|
||||
# In tests/conftest.py, after isolate_workspace
|
||||
@pytest.fixture(autouse=True)
|
||||
def enforce_no_real_toml(request, tmp_path, monkeypatch):
|
||||
"""Snapshot any real TOML files in cwd, remove them for the test, restore after.
|
||||
|
||||
This prevents tests from accidentally reading/writing the user's real config.
|
||||
Tests must use tmp_path or monkeypatch to get their TOML data.
|
||||
"""
|
||||
from pathlib import Path as _P
|
||||
real_toml_basenames = [
|
||||
"manual_slop.toml", "config.toml", "credentials.toml",
|
||||
"presets.toml", "personas.toml", "tool_presets.toml",
|
||||
"workspace_profiles.toml",
|
||||
]
|
||||
snapshots: dict[_P, bytes] = {}
|
||||
cwd = _P.cwd()
|
||||
for name in real_toml_basenames:
|
||||
p = cwd / name
|
||||
if p.exists():
|
||||
snapshots[p] = p.read_bytes()
|
||||
p.unlink()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for p, content in snapshots.items():
|
||||
p.write_bytes(content)
|
||||
```
|
||||
|
||||
- [ ] **Step 2.4: Run the existing test suite to verify the fixture doesn't break things**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/ -x --timeout=60 -q
|
||||
```
|
||||
|
||||
Expected: Existing tests should still pass. If a test was relying on a real TOML being present, it'll fail and need migration (next task).
|
||||
|
||||
- [ ] **Step 2.5: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add tests/conftest.py
|
||||
git -C C:\projects\manual_slop commit -m "test(infra): add enforce_no_real_toml autouse fixture"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Autouse fixture in tests/conftest.py. Snapshots any real TOML in cwd before test, removes it, restores after. Tests must use tmp_path or monkeypatch to access TOML data." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Tests for the enforcer
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_enforce_no_real_toml.py`
|
||||
|
||||
- [ ] **Step 3.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Create the meta-test**
|
||||
|
||||
```python
|
||||
# tests/test_enforce_no_real_toml.py
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def test_check_script_exits_zero_on_clean_suite():
|
||||
"""The audit script returns 0 when no violations exist."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "scripts/check_test_toml_paths.py"],
|
||||
capture_output=True, text=True,
|
||||
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
)
|
||||
# If we're in a clean state, this should be 0.
|
||||
# If there are still violations, this documents the current state.
|
||||
assert result.returncode in (0, 1), f"Unexpected exit code: {result.returncode}"
|
||||
|
||||
|
||||
def test_enforce_fixture_runs_without_error():
|
||||
"""The fixture completes without raising."""
|
||||
# If we get here, the fixture ran successfully.
|
||||
assert True
|
||||
|
||||
|
||||
def test_real_toml_restored_after_test(tmp_path, monkeypatch):
|
||||
"""Verify the fixture restores a real TOML after the test."""
|
||||
cwd = Path.cwd()
|
||||
real_path = cwd / "_enforce_test_temp.toml"
|
||||
if real_path.exists():
|
||||
real_path.unlink()
|
||||
real_path.write_bytes(b"[test]\nkey='value'")
|
||||
try:
|
||||
# During this test, the fixture removes _enforce_test_temp.toml
|
||||
# (it's not in the real_toml_basenames list, so it stays)
|
||||
# Test that real_path still exists (fixture didn't touch it)
|
||||
assert real_path.exists()
|
||||
finally:
|
||||
if real_path.exists():
|
||||
real_path.unlink()
|
||||
```
|
||||
|
||||
- [ ] **Step 3.3: Run the meta-tests**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/test_enforce_no_real_toml.py -v
|
||||
```
|
||||
|
||||
Expected: 3 tests pass.
|
||||
|
||||
- [ ] **Step 3.4: Commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add tests/test_enforce_no_real_toml.py
|
||||
git -C C:\projects\manual_slop commit -m "test(infra): add tests for enforce_no_real_toml fixture"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Meta-tests for the enforcer: script exit codes, fixture completes, real-file state not affected by unrelated tmp files." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Migrate test offenders
|
||||
|
||||
**Files:**
|
||||
- Various `tests/test_*.py` (find via `check_test_toml_paths.py`)
|
||||
|
||||
- [ ] **Step 4.1: Generate the offender list**
|
||||
|
||||
```powershell
|
||||
python scripts/check_test_toml_paths.py 2>&1 | Tee-Object -FilePath scripts/_offenders.txt
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: For each offender, migrate**
|
||||
|
||||
Pick one violation at a time. For each:
|
||||
|
||||
1. Read the test file
|
||||
2. Identify the line with the violation
|
||||
3. Refactor to use `tmp_path` + `monkeypatch` (or the `isolate_workspace` fixture if it's already running)
|
||||
4. Run that test in isolation to verify it still passes
|
||||
5. Commit the change
|
||||
|
||||
**Example migration pattern:**
|
||||
|
||||
Before:
|
||||
```python
|
||||
def test_load_presets():
|
||||
presets_path = Path("presets.toml")
|
||||
data = tomllib.loads(presets_path.read_text())
|
||||
assert "default" in data
|
||||
```
|
||||
|
||||
After:
|
||||
```python
|
||||
def test_load_presets(tmp_path, monkeypatch):
|
||||
from src import paths
|
||||
presets_path = tmp_path / "presets.toml"
|
||||
presets_path.write_text("[presets]\ndefault = {}\n")
|
||||
monkeypatch.setattr(paths, "get_global_presets_path", lambda: presets_path)
|
||||
data = tomllib.loads(presets_path.read_text())
|
||||
assert "default" in data
|
||||
```
|
||||
|
||||
- [ ] **Step 4.3: Re-run the audit script after each batch**
|
||||
|
||||
```powershell
|
||||
python scripts/check_test_toml_paths.py
|
||||
```
|
||||
|
||||
Goal: 0 violations.
|
||||
|
||||
- [ ] **Step 4.4: Commit per-file**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add tests/test_<migrated_file>.py
|
||||
git -C C:\projects\manual_slop commit -m "test(<scope>): migrate <file> to sandboxed TOML pattern"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Migrated <N> tests in <file> from real TOML to tmp_path + monkeypatch. Verified with full test suite." $_ }
|
||||
```
|
||||
|
||||
(Repeat for each file.)
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Consolidate similar tests (judgment call)
|
||||
|
||||
**Files:**
|
||||
- Various `tests/test_*.py`
|
||||
|
||||
- [ ] **Step 5.1: Identify consolidation candidates**
|
||||
|
||||
Run an analysis of test files:
|
||||
|
||||
```powershell
|
||||
Get-ChildItem C:\projects\manual_slop\tests\test_*.py | Group-Object { $_.BaseName -replace '^test_', '' -replace '_.*$', '' } | Where-Object { $_.Count -gt 2 } | Select-Object Name, Count
|
||||
```
|
||||
|
||||
Look for groups with > 2 files (e.g., `test_ai_settings_*.py`, `test_*_provider.py`).
|
||||
|
||||
- [ ] **Step 5.2: For each candidate, evaluate**
|
||||
|
||||
For each candidate group, ask:
|
||||
- Are these tests testing the same thing?
|
||||
- Would merging them make the failure messages clearer?
|
||||
- Would merging them make it harder to find a specific test?
|
||||
- Is the test count reduction a real win, or just a number?
|
||||
|
||||
If the answer is "merging improves clarity," proceed. Otherwise, leave alone.
|
||||
|
||||
- [ ] **Step 5.3: Consolidation pattern (if proceeding)**
|
||||
|
||||
Before (3 files):
|
||||
```
|
||||
tests/test_provider_gemini_init.py
|
||||
tests/test_provider_anthropic_init.py
|
||||
tests/test_provider_deepseek_init.py
|
||||
```
|
||||
|
||||
After (1 file, parametrized):
|
||||
```python
|
||||
# tests/test_provider_init.py
|
||||
import pytest
|
||||
|
||||
@pytest.mark.parametrize("provider_name", ["gemini", "anthropic", "deepseek"])
|
||||
def test_provider_init(provider_name, tmp_path, monkeypatch):
|
||||
# Common test logic, parametrized
|
||||
...
|
||||
```
|
||||
|
||||
- [ ] **Step 5.4: Commit per consolidation**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop add tests/test_consolidated.py tests/test_removed_file1.py tests/test_removed_file2.py
|
||||
git -C C:\projects\manual_slop commit -m "test(consolidate): merge <topic> tests into parametrized single file"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Consolidated N test files into 1 parametrized test. <Rationale for clarity gain>." $_ }
|
||||
```
|
||||
|
||||
(Only if a consolidation was actually performed.)
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Phase Completion Verification
|
||||
|
||||
- [ ] **Step 6.1: Final audit**
|
||||
|
||||
```powershell
|
||||
python scripts/check_test_toml_paths.py
|
||||
```
|
||||
|
||||
Expected: "OK: No tests reference real TOML files." Exit 0.
|
||||
|
||||
- [ ] **Step 6.2: Full test suite**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/ -q --timeout=60
|
||||
```
|
||||
|
||||
Expected: All tests pass.
|
||||
|
||||
- [ ] **Step 6.3: Create the checkpoint commit**
|
||||
|
||||
```bash
|
||||
git -C C:\projects\manual_slop commit --allow-empty -m "conductor(checkpoint): Test consolidation & TOML sandboxing complete"
|
||||
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Track complete. Audit script passes (0 violations). enforce_no_real_toml fixture in place. N tests migrated, M consolidations performed (or 0 if none were warranted)." $_ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** All design phases have a task. ✓
|
||||
- **Placeholder scan:** Migration example is concrete. ✓
|
||||
- **Type consistency:** Fixture name `enforce_no_real_toml` used consistently. ✓
|
||||
- **Consolidation is opt-in:** Task 5 explicitly says "if merging improves clarity" — not forced. ✓
|
||||
@@ -1,985 +0,0 @@
|
||||
# UI Polish Track — 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:** Resolve five outstanding UI quality-of-life issues: GFM table rendering, Keep Pairs input width, Log Management refresh bug, Operations Hub vendor-state panel, and Files & Media directory tree grouping.
|
||||
|
||||
**Architecture:** Pure rendering-layer changes. No new infrastructure, no threading, no provider API changes. Each phase is a self-contained sub-track with its own Red/Green/Commit cycle.
|
||||
|
||||
**Tech Stack:** Python 3.11+, imgui-bundle (`imgui_md`, `imgui.begin_table`, `imgui.tree_node_ex`), pytest, the `live_gui` fixture from `tests/conftest.py`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-03-ui-polish-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Status | Responsibility |
|
||||
|------|--------|----------------|
|
||||
| `src/markdown_table.py` | **create** | Pure GFM table parser. No ImGui imports. |
|
||||
| `src/markdown_helper.py` | modify | Insert table interceptor in `MarkdownRenderer.render()`. |
|
||||
| `src/vendor_state.py` | **create** | Pure vendor-state aggregator. No ImGui imports. |
|
||||
| `src/app_controller.py` | modify | Add `vendor_quota` field + `set_vendor_quota()` callback. |
|
||||
| `src/ai_client.py` | modify | Wire `set_vendor_quota` into quota-bearing response paths. |
|
||||
| `src/gui_2.py` | modify | Five surgical edits (one per phase). |
|
||||
| `tests/test_markdown_table.py` | **create** | Parser unit tests. |
|
||||
| `tests/test_markdown_table_render.py` | **create** | live_gui render tests. |
|
||||
| `tests/test_vendor_state.py` | **create** | Aggregator unit tests. |
|
||||
| `tests/test_vendor_state_render.py` | **create** | live_gui render tests. |
|
||||
| `tests/test_log_management_refresh.py` | **create** | live_gui button-press test. |
|
||||
| `tests/test_files_and_media_tree.py` | **create** | live_gui directory-grouping test. |
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
- **1-space indentation** for all Python (per `conductor/code_styleguides/python.md`).
|
||||
- **No comments** in source files (per `AGENTS.md`).
|
||||
- All public functions get strict type hints.
|
||||
- New SDM tags `[C: ...]` and `[M: ...]` on every new public function/method.
|
||||
- Run `uv run python scripts/check_imgui_scopes.py` after every `gui_2.py` edit.
|
||||
|
||||
---
|
||||
|
||||
# Phase 1 — Markdown Table Pre-Processor
|
||||
|
||||
### Task 1.1: GFM Table Parser — First Failing Test
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_markdown_table.py`
|
||||
- Create: `src/markdown_table.py` (stub with `parse_tables() -> list[TableBlock]` returning `[]`)
|
||||
|
||||
- [ ] **Step 1: Stage current state**
|
||||
|
||||
```powershell
|
||||
git add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the failing test**
|
||||
|
||||
```python
|
||||
# tests/test_markdown_table.py
|
||||
from src.markdown_table import parse_tables, TableBlock
|
||||
|
||||
def test_parses_simple_two_column_table():
|
||||
text = (
|
||||
"| Name | Type |\n"
|
||||
"|-------|------|\n"
|
||||
"| foo | int |\n"
|
||||
"| bar | str |\n"
|
||||
)
|
||||
blocks = parse_tables(text)
|
||||
assert len(blocks) == 1
|
||||
block = blocks[0]
|
||||
assert block.headers == ["Name", "Type"]
|
||||
assert block.rows == [["foo", "int"], ["bar", "str"]]
|
||||
|
||||
def test_ignores_tables_inside_code_fence():
|
||||
text = (
|
||||
"```\n"
|
||||
"| not | a table |\n"
|
||||
"| --- | ------- |\n"
|
||||
"| x | y |\n"
|
||||
"```\n"
|
||||
)
|
||||
assert parse_tables(text) == []
|
||||
|
||||
def test_returns_empty_for_plain_markdown():
|
||||
text = "# Heading\n\nSome **bold** text.\n"
|
||||
assert parse_tables(text) == []
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_table.py -v`
|
||||
Expected: FAIL with `ImportError` or `AttributeError: module 'src.markdown_table' has no attribute 'parse_tables'`.
|
||||
|
||||
- [ ] **Step 4: Stub the module so the import succeeds**
|
||||
|
||||
```python
|
||||
# src/markdown_table.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TableBlock:
|
||||
headers: list[str]
|
||||
rows: list[list[str]]
|
||||
span: tuple[int, int]
|
||||
|
||||
def parse_tables(text: str) -> list[TableBlock]:
|
||||
return []
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run test, confirm it still fails (logic not implemented)**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_table.py -v`
|
||||
Expected: FAIL on `assert block.headers == ["Name", "Type"]`.
|
||||
|
||||
- [ ] **Step 6: Commit (Red)**
|
||||
|
||||
```powershell
|
||||
git add tests/test_markdown_table.py src/markdown_table.py
|
||||
git commit -m "test(markdown): add GFM table parser failing tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.2: GFM Table Parser — Implementation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/markdown_table.py`
|
||||
|
||||
- [ ] **Step 1: Implement parse_tables() to pass all tests**
|
||||
|
||||
```python
|
||||
# src/markdown_table.py
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
_TABLE_SEPARATOR = re.compile(r"^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$")
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TableBlock:
|
||||
headers: list[str]
|
||||
rows: list[list[str]]
|
||||
span: tuple[int, int]
|
||||
[C: src/markdown_helper.py:MarkdownRenderer.render]
|
||||
|
||||
def _split_row(line: str) -> list[str]:
|
||||
line = line.strip()
|
||||
if line.startswith("|"): line = line[1:]
|
||||
if line.endswith("|"): line = line[:-1]
|
||||
return [c.strip() for c in line.split("|")]
|
||||
|
||||
def _is_table_at(lines: list[str], i: int) -> bool:
|
||||
if i + 1 >= len(lines): return False
|
||||
if "|" not in lines[i]: return False
|
||||
return bool(_TABLE_SEPARATOR.match(lines[i + 1]))
|
||||
|
||||
def parse_tables(text: str) -> list[TableBlock]:
|
||||
lines = text.splitlines()
|
||||
in_fence = False
|
||||
blocks: list[TableBlock] = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if line.strip().startswith("```"):
|
||||
in_fence = not in_fence
|
||||
i += 1
|
||||
continue
|
||||
if in_fence:
|
||||
i += 1
|
||||
continue
|
||||
if _is_table_at(lines, i):
|
||||
headers = _split_row(lines[i])
|
||||
j = i + 2
|
||||
rows: list[list[str]] = []
|
||||
while j < len(lines) and "|" in lines[j] and not _TABLE_SEPARATOR.match(lines[j]):
|
||||
rows.append(_split_row(lines[j]))
|
||||
j += 1
|
||||
blocks.append(TableBlock(headers=headers, rows=rows, span=(i, j)))
|
||||
i = j
|
||||
continue
|
||||
i += 1
|
||||
return blocks
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test, confirm it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_table.py -v`
|
||||
Expected: 3 passed.
|
||||
|
||||
- [ ] **Step 3: Commit (Green)**
|
||||
|
||||
```powershell
|
||||
git add src/markdown_table.py
|
||||
git commit -m "feat(markdown): implement GFM table parser"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.3: Table Renderer — live_gui Test
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_markdown_table_render.py`
|
||||
- Modify: `src/markdown_table.py` (add `render_table(block: TableBlock) -> None`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# tests/test_markdown_table_render.py
|
||||
from imgui_bundle import imgui
|
||||
from src.markdown_table import parse_tables, render_table
|
||||
|
||||
def test_render_table_creates_imGui_table(live_gui):
|
||||
app = live_gui
|
||||
text = "| A | B |\n|---|---|\n| 1 | 2 |\n"
|
||||
blocks = parse_tables(text)
|
||||
assert len(blocks) == 1
|
||||
imgui.begin("test_window")
|
||||
try:
|
||||
render_table(blocks[0])
|
||||
finally:
|
||||
imgui.end()
|
||||
assert imgui.get_io().font_default is not None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test, confirm it fails (no render_table function)**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_table_render.py -v`
|
||||
Expected: FAIL with `ImportError: cannot import name 'render_table'`.
|
||||
|
||||
- [ ] **Step 3: Add the render_table function stub to src/markdown_table.py**
|
||||
|
||||
```python
|
||||
# append to src/markdown_table.py
|
||||
from imgui_bundle import imgui
|
||||
|
||||
def render_table(block: TableBlock) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test, confirm it passes (trivial stub)**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_table_render.py -v`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit (Red-Green trivial)**
|
||||
|
||||
```powershell
|
||||
git add tests/test_markdown_table_render.py src/markdown_table.py
|
||||
git commit -m "test(markdown): scaffold render_table test with trivial impl"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.4: Table Renderer — Real Implementation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/markdown_table.py` — replace the trivial `render_table` with a real implementation.
|
||||
|
||||
- [ ] **Step 1: Replace render_table with a real implementation**
|
||||
|
||||
```python
|
||||
def render_table(block: TableBlock) -> None:
|
||||
n_cols = len(block.headers)
|
||||
if n_cols == 0: return
|
||||
flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable
|
||||
if not imgui.begin_table("md_table", n_cols, flags):
|
||||
return
|
||||
imgui.table_headers_row()
|
||||
for h in block.headers:
|
||||
imgui.table_next_column()
|
||||
imgui.text(h)
|
||||
for row in block.rows:
|
||||
imgui.table_next_row()
|
||||
for c in row:
|
||||
imgui.table_next_column()
|
||||
imgui.text(c)
|
||||
imgui.end_table()
|
||||
[C: src/markdown_helper.py:MarkdownRenderer.render]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run all markdown_table tests**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_table.py tests/test_markdown_table_render.py -v`
|
||||
Expected: 4 passed.
|
||||
|
||||
- [ ] **Step 3: Run regression on existing markdown tests**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_helper.py -v` (or whatever the actual file is — check with `search_files`)
|
||||
Expected: All existing tests still pass.
|
||||
|
||||
- [ ] **Step 4: Commit (Green)**
|
||||
|
||||
```powershell
|
||||
git add src/markdown_table.py
|
||||
git commit -m "feat(markdown): implement table rendering with imgui.begin_table"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.5: Interceptor in MarkdownRenderer
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/markdown_helper.py` — `MarkdownRenderer.render` method.
|
||||
|
||||
- [ ] **Step 1: Write a failing test in tests/test_markdown_table_render.py**
|
||||
|
||||
Append to the file:
|
||||
```python
|
||||
def test_renderer_intercepts_table_blocks(live_gui):
|
||||
from src.markdown_helper import MarkdownRenderer
|
||||
text = "# Title\n\n| A | B |\n|---|---|\n| 1 | 2 |\n\nEnd.\n"
|
||||
imgui.begin("test_window_2")
|
||||
try:
|
||||
MarkdownRenderer().render(text, context_id="test")
|
||||
finally:
|
||||
imgui.end()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test, confirm it passes (interceptor not yet wired)**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_table_render.py -v`
|
||||
Expected: PASS (interceptor absent is currently the green state).
|
||||
|
||||
This is a *characterization* test, not a Red-Green test. We proceed to Step 3 knowing the new test will pass either way; the value of the test is that the failure mode is now defined.
|
||||
|
||||
- [ ] **Step 3: Modify MarkdownRenderer.render to call parse_tables and substitute placeholders**
|
||||
|
||||
```python
|
||||
# src/markdown_helper.py — replace the render method body
|
||||
def render(self, text: str, context_id: str = "default") -> None:
|
||||
if not text: return
|
||||
from src.markdown_table import parse_tables, render_table
|
||||
blocks = parse_tables(text)
|
||||
sentinel = "\x00TBL{}\x00"
|
||||
masked = text
|
||||
for idx, block in enumerate(blocks):
|
||||
start, end = block.span
|
||||
original_block = "\n".join(masked.splitlines()[start:end])
|
||||
masked = masked.replace(original_block, sentinel.format(idx), 1)
|
||||
parts = re.split(r"(```[\s\S]*?```)", masked)
|
||||
table_iter = iter(blocks)
|
||||
block_idx = 0
|
||||
for part in parts:
|
||||
if part.startswith("```") and part.endswith("```"):
|
||||
self._render_code_block(part, context_id, block_idx)
|
||||
block_idx += 1
|
||||
elif part.strip():
|
||||
sub_parts = re.split(r"(\x00TBL\d+\x00)", part)
|
||||
for sp in sub_parts:
|
||||
if sp.startswith("\x00TBL") and sp.endswith("\x00"):
|
||||
idx = int(sp[4:-1])
|
||||
try: render_table(blocks[idx])
|
||||
except Exception: imgui.text(sp)
|
||||
else:
|
||||
if sp.strip(): imgui_md.render(sp)
|
||||
[C: src/theme_2.py:render_post_fx]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the full markdown test suite**
|
||||
|
||||
Run: `uv run pytest tests/test_markdown_table.py tests/test_markdown_table_render.py tests/test_markdown_helper.py -v`
|
||||
Expected: All pass.
|
||||
|
||||
- [ ] **Step 5: Run imgui scope linter**
|
||||
|
||||
Run: `uv run python scripts/check_imgui_scopes.py src/markdown_helper.py`
|
||||
Expected: No errors.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```powershell
|
||||
git add src/markdown_helper.py
|
||||
git commit -m "feat(markdown): intercept GFM tables and render via imgui.begin_table"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Attach git note**
|
||||
|
||||
```powershell
|
||||
git notes add -m "Phase 1 Task 1.5: wired MarkdownRenderer.render to parse_tables + render_table. Placeholder scheme avoids nested imgui_md quirks. Regressions: 0." HEAD
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Phase 1 checkpoint**
|
||||
|
||||
```powershell
|
||||
git commit --allow-empty -m "conductor(checkpoint): Phase 1 complete"
|
||||
$env:PHASE1_SHA = (git log -1 --format="%H")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Phase 2 — Truncate / Keep Pairs Input Width
|
||||
|
||||
### Task 2.1: Test the width fix
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_discussion_truncate_layout.py`
|
||||
- Modify: `src/gui_2.py:3829`
|
||||
|
||||
- [ ] **Step 1: Write a layout assertion test**
|
||||
|
||||
```python
|
||||
# tests/test_discussion_truncate_layout.py
|
||||
from imgui_bundle import imgui
|
||||
|
||||
def test_truncate_keep_pairs_field_has_adequate_width(live_gui):
|
||||
app = live_gui
|
||||
imgui.begin("test_disc_truncate")
|
||||
try:
|
||||
imgui.text("Keep Pairs:"); imgui.same_line()
|
||||
imgui.set_next_item_width(80)
|
||||
_, _ = imgui.input_int("##trunc_pairs", 2, 1)
|
||||
finally:
|
||||
imgui.end()
|
||||
# The production code at gui_2.py:3829 must use width >= 140
|
||||
import inspect, src.gui_2
|
||||
src_text = inspect.getsource(src.gui_2)
|
||||
assert 'set_next_item_width(80)' not in src_text.split('def render_discussion_metadata')[0]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test, confirm it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_discussion_truncate_layout.py -v`
|
||||
Expected: FAIL because `set_next_item_width(80)` is currently in the source.
|
||||
|
||||
- [ ] **Step 3: Modify `src/gui_2.py:3829` to use width 140 and switch to drag_int**
|
||||
|
||||
Replace:
|
||||
```python
|
||||
imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80)
|
||||
ch, app.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", app.ui_disc_truncate_pairs, 1)
|
||||
```
|
||||
|
||||
With:
|
||||
```python
|
||||
imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(140)
|
||||
ch, app.ui_disc_truncate_pairs = imgui.drag_int("##trunc_pairs", app.ui_disc_truncate_pairs, 1, 1, 999)
|
||||
if app.ui_disc_truncate_pairs < 1: app.ui_disc_truncate_pairs = 1
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test, confirm it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_discussion_truncate_layout.py -v`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Run regression on discussion hub tests**
|
||||
|
||||
Run: `uv run pytest tests/test_gui_fast_render.py -k discussion -v`
|
||||
Expected: All pass.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```powershell
|
||||
git add src/gui_2.py tests/test_discussion_truncate_layout.py
|
||||
git commit -m "fix(gui): widen Keep Pairs input and switch to drag_int for clearer +/-"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Phase 2 checkpoint**
|
||||
|
||||
```powershell
|
||||
git commit --allow-empty -m "conductor(checkpoint): Phase 2 complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Phase 3 — Log Management `Refresh Registry` Bug
|
||||
|
||||
### Task 3.1: Failing test for refresh
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_log_management_refresh.py`
|
||||
- Modify: `src/gui_2.py:1675`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# tests/test_log_management_refresh.py
|
||||
import os, tempfile, toml
|
||||
from pathlib import Path
|
||||
from src import log_registry, paths
|
||||
|
||||
def test_refresh_registry_reloads_from_disk(live_gui):
|
||||
app = live_gui
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
reg_path = Path(tmp) / "log_registry.toml"
|
||||
# First version
|
||||
toml.dump({"s1": {"start_time": "2026-01-01"}}, reg_path.open("w"))
|
||||
reg = log_registry.LogRegistry(str(reg_path))
|
||||
reg.load_registry()
|
||||
app._log_registry = reg
|
||||
# Mutate the file out-of-band
|
||||
toml.dump({"s1": {"start_time": "2026-01-01"}, "s2": {"start_time": "2026-01-02"}}, reg_path.open("w"))
|
||||
# Simulate the button handler
|
||||
from src.gui_2 import render_log_management
|
||||
# Render the panel once
|
||||
import imgui_bundle
|
||||
imgui_bundle.imgui.begin("Log Management")
|
||||
try:
|
||||
render_log_management(app)
|
||||
finally:
|
||||
imgui_bundle.imgui.end()
|
||||
# The current code does NOT call load_registry, so s2 is not present.
|
||||
# Our fix must add the call. This assertion is what we want to be True.
|
||||
app._log_registry.load_registry()
|
||||
assert "s2" in app._log_registry.data
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test, confirm it passes (handler is irrelevant, we manually call load_registry at the end)**
|
||||
|
||||
Run: `uv run pytest tests/test_log_management_refresh.py -v`
|
||||
Expected: PASS even before the fix — this test is characterizing the registry's reload, not the button. We need a different approach.
|
||||
|
||||
- [ ] **Step 3: Replace test with a focused source-assertion test**
|
||||
|
||||
Replace the file content with:
|
||||
```python
|
||||
# tests/test_log_management_refresh.py
|
||||
import inspect
|
||||
from src import gui_2
|
||||
|
||||
def test_refresh_registry_button_calls_load_registry():
|
||||
src = inspect.getsource(gui_2)
|
||||
# The button handler MUST call .load_registry() — either in-place or after re-instantiation
|
||||
snippet = src[src.find("Refresh Registry"):src.find("Refresh Registry") + 400]
|
||||
assert "load_registry" in snippet, (
|
||||
"Refresh Registry button must invoke load_registry(); "
|
||||
"currently it only re-instantiates LogRegistry which leaves .data empty."
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test, confirm it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_log_management_refresh.py -v`
|
||||
Expected: FAIL with the assertion message.
|
||||
|
||||
- [ ] **Step 5: Fix `src/gui_2.py:1675`**
|
||||
|
||||
Replace:
|
||||
```python
|
||||
if imgui.button("Refresh Registry"): app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
|
||||
```
|
||||
|
||||
With:
|
||||
```python
|
||||
if imgui.button("Refresh Registry"):
|
||||
if app._log_registry is not None: app._log_registry.load_registry()
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run test, confirm it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_log_management_refresh.py -v`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Run regression on log management tests**
|
||||
|
||||
Run: `uv run pytest tests/test_log_management_ui.py tests/test_log_pruner.py -v`
|
||||
Expected: All pass.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```powershell
|
||||
git add src/gui_2.py tests/test_log_management_refresh.py
|
||||
git commit -m "fix(log): Refresh Registry button now calls load_registry() on the live instance"
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Phase 3 checkpoint**
|
||||
|
||||
```powershell
|
||||
git commit --allow-empty -m "conductor(checkpoint): Phase 3 complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Phase 4 — Operations Hub > Vendor State Panel
|
||||
|
||||
### Task 4.1: Vendor State Aggregator — Tests + Stub
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_vendor_state.py`
|
||||
- Create: `src/vendor_state.py` (stub)
|
||||
|
||||
- [ ] **Step 1: Stage and write the failing test**
|
||||
|
||||
```python
|
||||
# tests/test_vendor_state.py
|
||||
from src.vendor_state import get_vendor_state, VendorMetric
|
||||
|
||||
class _StubApp:
|
||||
current_provider = "Anthropic"
|
||||
current_model = "claude-opus-4"
|
||||
controller = None # set in fixture
|
||||
|
||||
def test_get_vendor_state_returns_core_metrics():
|
||||
class _C:
|
||||
token_tracker = type("TT", (), {"used": 78234, "limit": 200000, "cache_hits": 1200, "cache_misses": 80})()
|
||||
last_error = None
|
||||
vendor_quota = {"remaining_pct": 87}
|
||||
app = _StubApp()
|
||||
app.controller = _C()
|
||||
metrics = get_vendor_state(app)
|
||||
keys = {m.key for m in metrics}
|
||||
assert "provider_model" in keys
|
||||
assert "context_window" in keys
|
||||
assert "cache" in keys
|
||||
assert "quota" in keys
|
||||
assert "last_error" in keys
|
||||
|
||||
def test_missing_data_renders_em_dash_not_crash():
|
||||
class _C:
|
||||
token_tracker = None
|
||||
last_error = None
|
||||
vendor_quota = {}
|
||||
app = _StubApp()
|
||||
app.controller = _C()
|
||||
metrics = get_vendor_state(app)
|
||||
for m in metrics:
|
||||
assert m.value is not None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Stub `src/vendor_state.py`**
|
||||
|
||||
```python
|
||||
# src/vendor_state.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VendorMetric:
|
||||
key: str
|
||||
label: str
|
||||
value: str
|
||||
state: str
|
||||
tooltip: str
|
||||
[C: src/gui_2.py:render_vendor_state]
|
||||
|
||||
def get_vendor_state(app) -> list[VendorMetric]:
|
||||
return []
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run test, confirm it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_vendor_state.py -v`
|
||||
Expected: FAIL with `assert "provider_model" in keys` (empty list).
|
||||
|
||||
- [ ] **Step 4: Implement get_vendor_state**
|
||||
|
||||
```python
|
||||
def get_vendor_state(app) -> list[VendorMetric]:
|
||||
out: list[VendorMetric] = []
|
||||
out.append(VendorMetric(
|
||||
key="provider_model",
|
||||
label="Provider / Model",
|
||||
value=f"{app.current_provider} / {app.current_model}",
|
||||
state="info",
|
||||
tooltip="The vendor and model that will handle the next request."
|
||||
))
|
||||
ctrl = getattr(app, "controller", None)
|
||||
tt = getattr(ctrl, "token_tracker", None) if ctrl else None
|
||||
if tt and getattr(tt, "limit", 0):
|
||||
pct = 100.0 * getattr(tt, "used", 0) / tt.limit
|
||||
state = "warn" if pct > 75 else "ok"
|
||||
out.append(VendorMetric(
|
||||
key="context_window",
|
||||
label="Context Window",
|
||||
value=f"{tt.used:,} / {tt.limit:,} ({pct:.0f}%)",
|
||||
state=state,
|
||||
tooltip="Used vs total context window for the current session."
|
||||
))
|
||||
else:
|
||||
out.append(VendorMetric(
|
||||
key="context_window", label="Context Window", value="—", state="info",
|
||||
tooltip="No token tracker attached for the current provider."
|
||||
))
|
||||
if tt:
|
||||
hits = getattr(tt, "cache_hits", 0)
|
||||
miss = getattr(tt, "cache_misses", 0)
|
||||
total = hits + miss
|
||||
rate = (100.0 * hits / total) if total else 0.0
|
||||
out.append(VendorMetric(
|
||||
key="cache", label="Cache Hit Rate",
|
||||
value=f"{rate:.0f}% ({hits:,}/{total:,})",
|
||||
state="ok" if rate > 50 else "info",
|
||||
tooltip="Server-side prompt cache hit rate for the current session."
|
||||
))
|
||||
quota = (getattr(ctrl, "vendor_quota", {}) or {}) if ctrl else {}
|
||||
pct_left = quota.get("remaining_pct")
|
||||
if pct_left is None:
|
||||
out.append(VendorMetric(
|
||||
key="quota", label="Vendor Quota", value="—", state="info",
|
||||
tooltip="Vendor did not report quota for the current billing period."
|
||||
))
|
||||
else:
|
||||
out.append(VendorMetric(
|
||||
key="quota", label="Vendor Quota",
|
||||
value=f"{pct_left}% remaining",
|
||||
state="ok" if pct_left > 25 else "warn",
|
||||
tooltip="Approximate quota remaining for the current billing period."
|
||||
))
|
||||
err = getattr(ctrl, "last_error", None) if ctrl else None
|
||||
out.append(VendorMetric(
|
||||
key="last_error", label="Last Error",
|
||||
value=err.get("class", "none") if err else "none",
|
||||
state="error" if err else "ok",
|
||||
tooltip=err.get("message", "No error since session start.") if err else "No error since session start."
|
||||
))
|
||||
return out
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run test, confirm it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_vendor_state.py -v`
|
||||
Expected: 2 passed.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```powershell
|
||||
git add src/vendor_state.py tests/test_vendor_state.py
|
||||
git commit -m "feat(vendor-state): add pure aggregator with stable metric keys"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4.2: Add vendor_quota to AppController
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app_controller.py` — find a dataclass-style state block (search for `@dataclass` near `class AppController`)
|
||||
|
||||
- [ ] **Step 1: Find the AppController state block**
|
||||
|
||||
Run: `py_get_definition src/app_controller.py:AppController.__init__` (or `search_files` for `vendor_quota` in app_controller.py)
|
||||
Expected: The state is in `__init__` of `AppController` (probably around line 100-200).
|
||||
|
||||
- [ ] **Step 2: Add the field and method**
|
||||
|
||||
Add to `AppController.__init__`:
|
||||
```python
|
||||
self.vendor_quota: dict[str, Any] = {}
|
||||
self.last_error: dict[str, str] | None = None
|
||||
```
|
||||
|
||||
Add a new method on `AppController`:
|
||||
```python
|
||||
def set_vendor_quota(self, provider: str, remaining_pct: float) -> None:
|
||||
with self._state_lock:
|
||||
self.vendor_quota = {"provider": provider, "remaining_pct": remaining_pct}
|
||||
[C: src/ai_client.py:_*_request]
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Wire it into ai_client.py quota paths**
|
||||
|
||||
For each provider that returns quota-bearing responses (Anthropic, Gemini, DeepSeek, MiniMax), after a successful response, call:
|
||||
```python
|
||||
if hasattr(app, "controller") and app.controller is not None:
|
||||
app.controller.set_vendor_quota(provider, remaining_pct)
|
||||
```
|
||||
|
||||
The exact wire-up point depends on the existing response handler. Read the file with `py_get_skeleton` and patch at the call sites that currently write the comms log entry for a successful response.
|
||||
|
||||
- [ ] **Step 4: Run vendor_state tests + ai_client tests**
|
||||
|
||||
Run: `uv run pytest tests/test_vendor_state.py tests/test_ai_client.py -v`
|
||||
Expected: All pass.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```powershell
|
||||
git add src/app_controller.py src/ai_client.py
|
||||
git commit -m "feat(vendor-state): add vendor_quota state and provider wire-up"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4.3: Wire Vendor State into Operations Hub
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` — add `render_vendor_state` function, add new tab in `render_operations_hub`.
|
||||
|
||||
- [ ] **Step 1: Add render_vendor_state function (module level in gui_2.py)**
|
||||
|
||||
```python
|
||||
def render_vendor_state(app: App) -> None:
|
||||
from src.vendor_state import get_vendor_state
|
||||
metrics = get_vendor_state(app)
|
||||
if imgui.begin_table("vendor_state", 3, imgui.TableFlags_.row_bg | imgui.TableFlags_.borders):
|
||||
imgui.table_setup_column("Metric", imgui.TableColumnFlags_.width_fixed, 180)
|
||||
imgui.table_setup_column("Value", imgui.TableColumnFlags_.width_stretch)
|
||||
imgui.table_setup_column("State", imgui.TableColumnFlags_.width_fixed, 60)
|
||||
imgui.table_headers_row()
|
||||
state_colors = {"ok": vec4(120, 220, 120), "warn": vec4(240, 200, 80), "error": vec4(240, 80, 80), "info": vec4(180, 180, 180)}
|
||||
for m in metrics:
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column(); imgui.text(m.label)
|
||||
imgui.table_next_column(); imgui.text(m.value)
|
||||
if imgui.is_item_hovered(): imgui.set_tooltip(m.tooltip)
|
||||
imgui.table_next_column()
|
||||
imgui.text_colored(state_colors.get(m.state, vec4(180, 180, 180)), m.state)
|
||||
imgui.end_table()
|
||||
[C: src/gui_2.py:render_operations_hub]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the tab to render_operations_hub**
|
||||
|
||||
In `render_operations_hub` (around line 4023 in current `src/gui_2.py`), after the "Workspace Layouts" tab block, add:
|
||||
```python
|
||||
with imscope.tab_item("Vendor State") as (exp, _):
|
||||
if exp: render_vendor_state(app)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add a live_gui render test**
|
||||
|
||||
```python
|
||||
# tests/test_vendor_state_render.py
|
||||
from src.gui_2 import render_vendor_state
|
||||
|
||||
def test_render_vendor_state_creates_table(live_gui):
|
||||
app = live_gui
|
||||
import imgui_bundle
|
||||
imgui_bundle.imgui.begin("test_vendor")
|
||||
try:
|
||||
render_vendor_state(app)
|
||||
finally:
|
||||
imgui_bundle.imgui.end()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run regression on Operations Hub tests**
|
||||
|
||||
Run: `uv run pytest tests/test_gui_fast_render.py -k operations -v`
|
||||
Expected: All pass.
|
||||
|
||||
- [ ] **Step 5: Run imgui scope linter**
|
||||
|
||||
Run: `uv run python scripts/check_imgui_scopes.py src/gui_2.py`
|
||||
Expected: No errors.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```powershell
|
||||
git add src/gui_2.py tests/test_vendor_state_render.py
|
||||
git commit -m "feat(ops-hub): add Vendor State tab with quota + context + cache"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Phase 4 checkpoint**
|
||||
|
||||
```powershell
|
||||
git commit --allow-empty -m "conductor(checkpoint): Phase 4 complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Phase 5 — Files & Media > Files Directory Tree
|
||||
|
||||
### Task 5.1: Test + Refactor render_files_and_media
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_files_and_media_tree.py`
|
||||
- Modify: `src/gui_2.py:2689-2750`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# tests/test_files_and_media_tree.py
|
||||
import os, tempfile
|
||||
from src import models
|
||||
from src.gui_2 import render_files_and_media
|
||||
|
||||
def test_files_rendered_under_directory_grouping(live_gui):
|
||||
app = live_gui
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
f1 = os.path.join(tmp, "a.py"); f2 = os.path.join(tmp, "b.py")
|
||||
f3 = os.path.join(tmp, "sub", "c.py")
|
||||
os.makedirs(os.path.dirname(f3), exist_ok=True)
|
||||
for p in (f1, f2, f3): open(p, "w").close()
|
||||
app.files = [models.FileItem(path=f1), models.FileItem(path=f2), models.FileItem(path=f3)]
|
||||
import imgui_bundle
|
||||
imgui_bundle.imgui.begin("Files & Media")
|
||||
try:
|
||||
render_files_and_media(app)
|
||||
finally:
|
||||
imgui_bundle.imgui.end()
|
||||
# Assert no exception was raised. The directory grouping is structural.
|
||||
assert len(app.files) == 3
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test, confirm it passes (current code is flat, not yet grouped)**
|
||||
|
||||
Run: `uv run pytest tests/test_files_and_media_tree.py -v`
|
||||
Expected: PASS (the assertion is only about not crashing). The test is a regression guard for the refactor.
|
||||
|
||||
- [ ] **Step 3: Modify `src/gui_2.py:2689-2750` to wrap the inner loop in a directory group loop**
|
||||
|
||||
Replace the body of the `if imgui.collapsing_header("Files", ...)` block (lines ~2691-2750 in current `src/gui_2.py`) with:
|
||||
```python
|
||||
if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open):
|
||||
with imscope.group():
|
||||
if imgui.begin_table("files_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders | imgui.TableFlags_.row_bg):
|
||||
imgui.table_setup_column("Act", imgui.TableColumnFlags_.width_fixed, 60)
|
||||
imgui.table_setup_column("Path", imgui.TableColumnFlags_.width_stretch)
|
||||
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 70)
|
||||
imgui.table_headers_row()
|
||||
to_remove_idx = -1
|
||||
app.files.sort(key=lambda f: f.path.lower() if hasattr(f, 'path') else str(f).lower())
|
||||
from src import aggregate
|
||||
grouped = aggregate.group_files_by_dir(app.files)
|
||||
for dir_name, g_files in sorted(grouped.items()):
|
||||
with imscope.tree_node_ex(f"{dir_name}##files_dir", imgui.TreeNodeFlags_.default_open) as is_open:
|
||||
if is_open:
|
||||
for f_item in g_files:
|
||||
i = app.files.index(f_item)
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
fpath = f_item.path if hasattr(f_item, 'path') else str(f_item)
|
||||
in_context = any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files)
|
||||
is_cached = any(fpath in c for c in getattr(app, '_cached_files', []))
|
||||
if imgui.button(f"+##add_f_{i}"):
|
||||
if not in_context:
|
||||
from src import models
|
||||
new_item = models.FileItem(path=fpath)
|
||||
app.context_files.append(new_item)
|
||||
app._populate_auto_slices(new_item)
|
||||
imgui.same_line()
|
||||
if imgui.button(f"x##rem_f_{i}"):
|
||||
to_remove_idx = i
|
||||
imgui.table_set_column_index(1)
|
||||
imgui.text(fpath)
|
||||
if imgui.is_item_hovered(): imgui.set_tooltip(fpath)
|
||||
imgui.table_set_column_index(2)
|
||||
if in_context:
|
||||
imgui.text_colored(imgui.ImVec4(0.3, 0.8, 0.3, 1), "Active")
|
||||
elif is_cached:
|
||||
imgui.text_colored(imgui.ImVec4(0.3, 0.8, 1, 1), "Cached")
|
||||
else:
|
||||
imgui.text_disabled(" - ")
|
||||
imgui.end_table()
|
||||
if to_remove_idx != -1: app.files.pop(to_remove_idx)
|
||||
imgui.dummy(imgui.ImVec2(0, 5))
|
||||
if imgui.button("Add Files to Inventory"):
|
||||
r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy()
|
||||
for p in paths:
|
||||
if p not in [f.path if hasattr(f, 'path') else f for f in app.files]: app.files.append(models.FileItem(path=p))
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run all files-and-media tests**
|
||||
|
||||
Run: `uv run pytest tests/test_files_and_media_tree.py tests/test_gui_fast_render.py::test_render_files_and_media_fast -v`
|
||||
Expected: All pass.
|
||||
|
||||
- [ ] **Step 5: Run imgui scope linter**
|
||||
|
||||
Run: `uv run python scripts/check_imgui_scopes.py src/gui_2.py`
|
||||
Expected: No errors.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```powershell
|
||||
git add src/gui_2.py tests/test_files_and_media_tree.py
|
||||
git commit -m "feat(files-media): group files by directory using collapsible tree nodes"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Phase 5 final checkpoint**
|
||||
|
||||
```powershell
|
||||
git commit --allow-empty -m "conductor(checkpoint): UI Polish track complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- [x] All five user issues have at least one task.
|
||||
- [x] No `TBD`/`TODO` placeholders.
|
||||
- [x] Type signatures match across tasks (`VendorMetric.key`, `TableBlock.span`, etc.).
|
||||
- [x] Indentation in code blocks is 1-space.
|
||||
- [x] Every `gui_2.py` edit is followed by an imgui scope linter run.
|
||||
- [x] No inter-phase dependencies — phases can be reordered.
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Plan complete and saved to `docs/superpowers/plans/2026-06-03-ui-polish.md`.
|
||||
|
||||
**Two execution options:**
|
||||
|
||||
1. **Subagent-Driven (recommended)** — dispatch a fresh subagent per task, review between tasks, fast iteration.
|
||||
2. **Inline Execution** — execute tasks in this session with `executing-plans`, batch execution with checkpoints.
|
||||
|
||||
Awaiting your choice before proceeding to implementation.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,387 +0,0 @@
|
||||
# Live-GUI Fragility Fixes 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:** Fix 3 failing live_gui tests discovered in the 2026-06-05 batched test run (269/272 → 272/272) by repairing a regression in the defer-not-catch fix for `_capture_workspace_profile`, fixing a test mock for `imscope.window`, and adding a regression unit test.
|
||||
|
||||
**Architecture:** Surgical 1-line fix on the production code path (the str/bytes sentinel that violated the `WorkspaceProfile.ini_content: str` contract), a 2-line fix on the prior session test mock (add missing tuple-return for `imscope.window`), and a new unit test that encodes the str/bytes contract so future regressions are caught at unit-test speed.
|
||||
|
||||
**Tech Stack:** Python 3.11+, pytest 9.0, imgui-bundle (`imgui.save_ini_settings_to_memory()`), tomli_w, tomllib.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Change | Purpose |
|
||||
|---|---|---|
|
||||
| `src/gui_2.py` | Modify lines 601-609 | Fix `ini = b""` → `ini = ""` in defer branch + `except` handler. Add `str()` defensive wrap. |
|
||||
| `tests/test_prior_session_no_pop_imbalance.py` | Modify (add 2 lines) | Add `(True, True)` tuple-return mock for `imscope.window`. |
|
||||
| `tests/test_workspace_profile_serialization.py` | Create | New unit test for the `ini_content: str` round-trip contract. |
|
||||
| `conductor/tracks.md` | Modify (1 line, plan updates) | Register new track. |
|
||||
| `docs/superpowers/specs/2026-06-05-live-gui-fragility-fixes-design.md` | (already written) | Spec for this work. |
|
||||
|
||||
No new files needed in `src/`. No production-code refactoring. No changes to the workspace profile save/load architecture.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Fix `_capture_workspace_profile` str/bytes sentinel
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py:601-609`
|
||||
- Test: deferred to Task 3 (regression unit test)
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add .
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Read the current state of `_capture_workspace_profile`**
|
||||
|
||||
Read `src/gui_2.py:601-609` to confirm the current code.
|
||||
|
||||
- [ ] **Step 1.3: Apply the fix**
|
||||
|
||||
Replace the current `_capture_workspace_profile` defer-and-except block (lines 601-609) with:
|
||||
|
||||
```python
|
||||
def _capture_workspace_profile(self, name: str) -> models.WorkspaceProfile:
|
||||
if not getattr(self, "_ini_capture_ready", False):
|
||||
self._ini_capture_ready = True
|
||||
ini = ""
|
||||
else:
|
||||
try:
|
||||
ini = str(imgui.save_ini_settings_to_memory() or "")
|
||||
except Exception:
|
||||
ini = ""
|
||||
panel_states = {
|
||||
```
|
||||
|
||||
Use `manual-slop_py_update_definition` with the existing function name `_capture_workspace_profile` to do the surgical replacement. The body change is:
|
||||
- Line 604: `ini = b""` → `ini = ""`
|
||||
- Line 609: `ini = b""` → `ini = ""`
|
||||
- Line 607: `ini = imgui.save_ini_settings_to_memory()` → `ini = str(imgui.save_ini_settings_to_memory() or "")`
|
||||
|
||||
Use exactly 1-space indentation.
|
||||
|
||||
- [ ] **Step 1.4: Verify the file still parses**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py').read())"
|
||||
```
|
||||
|
||||
Expected: no error.
|
||||
|
||||
- [ ] **Step 1.5: Run the workspace-profile-related tests to verify the fix**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_workspace_manager.py tests/test_workspace_profiles_sim.py tests/test_auto_switch_sim.py -v --timeout=60
|
||||
```
|
||||
|
||||
Expected:
|
||||
- `test_workspace_manager.py` passes (it tests the manager's save/load semantics with mocked profiles).
|
||||
- `test_workspace_profiles_sim.py` passes (it uses `live_gui`).
|
||||
- `test_auto_switch_sim.py` passes (it uses `live_gui`).
|
||||
|
||||
If `test_workspace_profiles_sim.py` or `test_auto_switch_sim.py` fails, it should be ONLY because of session-state pollution from a prior run. The fix targets the underlying bug; the test infrastructure (live_gui fixture) is what makes these flake. Re-run individually if needed:
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_workspace_profiles_sim.py::test_workspace_profiles_restoration -v --timeout=60
|
||||
```
|
||||
|
||||
- [ ] **Step 1.6: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add src/gui_2.py
|
||||
git -C C:\projects\manual_slop commit -m "fix(gui_2): use str sentinel not bytes in _capture_workspace_profile"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "WorkspaceProfile.ini_content is str (src/models.py:799) and tomli_w rejects bytes. The d7487af4 defer fix used ini=b'' which crashed TOML serialization, so save_workspace_profile raised TypeError, profile was never saved, and load_workspace_profile became a no-op. Changed both ini=b'' to ini='' and added str() defensive wrap on the non-defer path. Fixes test_auto_switch_sim and test_workspace_profiles_restoration." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Fix prior session test mock for `imscope.window`
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_prior_session_no_pop_imbalance.py` (the mock setup loop ~line 75-78, where the test sets up imscope context managers)
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add .
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Read the current mock setup**
|
||||
|
||||
Read `tests/test_prior_session_no_pop_imbalance.py:60-95` to see the `mock_imscope` setup.
|
||||
|
||||
- [ ] **Step 2.3: Apply the fix**
|
||||
|
||||
Find the loop that sets `__enter__` and `__exit__` for all imscope context managers (it looks like this around line 70-80):
|
||||
|
||||
```python
|
||||
for sc in [mock_imscope.style_color, mock_imscope.style_var, mock_imscope.child, mock_imscope.tab_bar, mock_imscope.tab_item, mock_imscope.tree_node_ex, mock_imscope.group, mock_imscope.indent, mock_imscope.id, mock_imscope.text_wrap, mock_imscope.tooltip, mock_imscope.menu, mock_imscope.menu_bar, mock_imscope.popup, mock_imscope.popup_modal, mock_imscope.window, mock_imscope.table]:
|
||||
sc.return_value.__enter__ = MagicMock(side_effect=_scope_enter)
|
||||
sc.return_value.__exit__ = MagicMock(side_effect=_scope_exit)
|
||||
```
|
||||
|
||||
Note: `mock_imscope.window` is in this list. The default `MagicMock(side_effect=_scope_enter)` returns a bare `MagicMock` (non-iterable), but production code at `src/gui_2.py:2333` does `with imscope.window(...) as (opened, visible):` which expects a 2-tuple.
|
||||
|
||||
After the loop (around line 91, after the `mock_imscope.popup_modal.return_value.__enter__ = MagicMock(return_value=(True, None))` line), add:
|
||||
|
||||
```python
|
||||
mock_imscope.window.return_value.__enter__ = MagicMock(return_value=(True, True))
|
||||
```
|
||||
|
||||
This matches the pattern already used for `popup_modal` (which returns `(True, None)`). The `__exit__` from the loop above is preserved (returns `False`, indicating no exception).
|
||||
|
||||
Use `manual-slop_edit_file` with the exact `old_string` from the popup_modal mock line and the `new_string` with the additional window mock line right after it.
|
||||
|
||||
Use exactly 1-space indentation.
|
||||
|
||||
- [ ] **Step 2.4: Run the prior session test**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_prior_session_no_pop_imbalance.py -v --timeout=30
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2.5: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add tests/test_prior_session_no_pop_imbalance.py
|
||||
git -C C:\projects\manual_slop commit -m "test(prior_session): mock imscope.window with tuple-return matching popup_modal"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "The test's mock setup loop for imscope context managers set __enter__ to a bare MagicMock (non-iterable), but render_preset_manager_window at src/gui_2.py:2333 does 'with imscope.window(...) as (opened, visible):' which expects a 2-tuple. popup_modal already had the right setup; window was missing it. Added the tuple-return for window, matching popup_modal's pattern." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add regression unit test for `WorkspaceProfile` str/bytes contract
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_workspace_profile_serialization.py`
|
||||
|
||||
- [ ] **Step 3.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add .
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Write the test file**
|
||||
|
||||
Create `tests/test_workspace_profile_serialization.py`:
|
||||
|
||||
```python
|
||||
import io
|
||||
import tomllib
|
||||
import pytest
|
||||
import tomli_w
|
||||
from src.models import WorkspaceProfile
|
||||
|
||||
|
||||
def test_workspace_profile_empty_ini_content_roundtrips():
|
||||
"""WorkspaceProfile with ini_content='' (empty str) must round-trip through TOML.
|
||||
This is the str/bytes type contract that the defer-not-catch fix in d7487af4 violated
|
||||
(it used ini=b'' which tomli_w rejects with TypeError).
|
||||
"""
|
||||
profile = WorkspaceProfile(
|
||||
name="t",
|
||||
ini_content="",
|
||||
show_windows={"A": True, "B": False},
|
||||
panel_states={"x": 1, "y": 2.0, "z": True},
|
||||
)
|
||||
d = profile.to_dict()
|
||||
buf = io.BytesIO()
|
||||
tomli_w.dump({"t": d}, buf)
|
||||
buf.seek(0)
|
||||
back = tomllib.load(buf)
|
||||
loaded = WorkspaceProfile.from_dict("t", back["t"])
|
||||
assert loaded.ini_content == ""
|
||||
assert loaded.show_windows == {"A": True, "B": False}
|
||||
assert loaded.panel_states == {"x": 1, "y": 2.0, "z": True}
|
||||
|
||||
|
||||
def test_workspace_profile_with_actual_ini_content_roundtrips():
|
||||
"""WorkspaceProfile with real ini content (str) must round-trip through TOML.
|
||||
This mirrors how save_ini_settings_to_memory() returns a str at runtime.
|
||||
"""
|
||||
profile = WorkspaceProfile(
|
||||
name="real",
|
||||
ini_content="[Window][Debug]\nPos=10,20\n",
|
||||
show_windows={},
|
||||
panel_states={},
|
||||
)
|
||||
d = profile.to_dict()
|
||||
buf = io.BytesIO()
|
||||
tomli_w.dump({"real": d}, buf)
|
||||
buf.seek(0)
|
||||
back = tomllib.load(buf)
|
||||
loaded = WorkspaceProfile.from_dict("real", back["real"])
|
||||
assert loaded.ini_content == "[Window][Debug]\nPos=10,20\n"
|
||||
assert loaded.name == "real"
|
||||
assert loaded.show_windows == {}
|
||||
assert loaded.panel_states == {}
|
||||
|
||||
|
||||
def test_workspace_profile_bytes_ini_content_rejected_by_toml():
|
||||
"""Regression guard: a bytes ini_content must raise TypeError from tomli_w.
|
||||
This documents the type contract; if tomli_w ever gains bytes support, the
|
||||
contract should be revisited (e.g. by switching WorkspaceProfile.ini_content
|
||||
to bytes and updating the imgui.load_ini_settings_from_memory call site).
|
||||
"""
|
||||
profile = WorkspaceProfile(
|
||||
name="bad",
|
||||
ini_content=b"", # type: ignore[arg-type]
|
||||
show_windows={},
|
||||
panel_states={},
|
||||
)
|
||||
d = profile.to_dict()
|
||||
buf = io.BytesIO()
|
||||
with pytest.raises(TypeError, match="bytes"):
|
||||
tomli_w.dump({"bad": d}, buf)
|
||||
```
|
||||
|
||||
Use exactly 1-space indentation. No comments per project style.
|
||||
|
||||
- [ ] **Step 3.3: Run the test to verify it passes (Change 1 should already be applied)**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_workspace_profile_serialization.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: 3 passed (one per test).
|
||||
|
||||
- [ ] **Step 3.4: Verify the test would catch the regression**
|
||||
|
||||
Temporarily revert the fix in `src/gui_2.py:604` from `ini = ""` back to `ini = b""` and re-run:
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_workspace_profile_serialization.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: the first two tests should still pass (they test the dataclass round-trip directly, not the defer fix), and the third test confirms the type contract. The test that catches the regression is the integration test in Task 1 (which goes through the live_gui save flow).
|
||||
|
||||
Restore the fix:
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git diff src/gui_2.py # confirm only the in-scope fix is there
|
||||
```
|
||||
|
||||
If you reverted the fix, re-apply it via `manual-slop_edit_file` and verify the tests still pass.
|
||||
|
||||
- [ ] **Step 3.5: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add tests/test_workspace_profile_serialization.py
|
||||
git -C C:\projects\manual_slop commit -m "test(workspace_profile): add str/bytes TOML serialization contract test"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Encodes the WorkspaceProfile.ini_content: str contract. The d7487af4 defer fix used ini=b'' which tomli_w rejects with TypeError. This test would have caught the regression at unit-test speed (no live_gui needed). 3 tests: empty str round-trips, real ini content round-trips, bytes ini_content is rejected (documents the contract)." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Verify all 3 originally-failing tests now pass
|
||||
|
||||
**Files:** (no file changes; verification only)
|
||||
|
||||
- [ ] **Step 4.1: Run the 3 originally-failing tests**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_auto_switch_sim.py tests/test_workspace_profiles_sim.py tests/test_prior_session_no_pop_imbalance.py -v --timeout=60
|
||||
```
|
||||
|
||||
Expected: 3 passed (one file each).
|
||||
|
||||
- [ ] **Step 4.2: Run the regression unit test**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_workspace_profile_serialization.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: 3 passed.
|
||||
|
||||
- [ ] **Step 4.3: Run the full batched test suite**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run python scripts/run_tests_batched.py
|
||||
```
|
||||
|
||||
Expected: 273 files (272 + 1 new), all batches pass (273/273 = 100%).
|
||||
|
||||
- [ ] **Step 4.4: Commit plan update**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add conductor/tracks.md docs/superpowers/plans/2026-06-05-live-gui-fragility-fixes.md
|
||||
```
|
||||
|
||||
Then append the following entry to `conductor/tracks.md` (under the existing `regression_fixes_20260605` entry or as a new entry):
|
||||
|
||||
```markdown
|
||||
- [x] **Track: Live-GUI Fragility Fixes (post regression_fixes_20260605)** `[checkpoint: <sha>]`
|
||||
*Link: [./tracks/live_gui_fragility_fixes_20260605/](./tracks/live_gui_fragility_fixes_20260605/), Spec: [./../../docs/superpowers/specs/2026-06-05-live-gui-fragility-fixes-design.md](./../../docs/superpowers/specs/2026-06-05-live-gui-fragility-fixes-design.md), Plan: [./../../docs/superpowers/plans/2026-06-05-live-gui-fragility-fixes.md](./../../docs/superpowers/plans/2026-06-05-live-gui-fragility-fixes.md)*
|
||||
*Goal: Fix 3 remaining live_gui test failures (269/272 → 272/272). 1-line src fix in `_capture_workspace_profile` (str/bytes sentinel that broke TOML serialization), 2-line test mock fix for `imscope.window` tuple-return, 1 new regression unit test for the str/bytes contract. All atomic per-file commits. The d7487af4 defer fix had introduced a TypeError via `ini=b""`; the regression was traced to `WorkspaceProfile.ini_content: str` and tomli_w's bytes rejection.*
|
||||
```
|
||||
|
||||
(Replace `<sha>` with the actual checkpoint SHA from the last commit.)
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git -c core.autocrlf=false commit -m "conductor(plan): mark live_gui_fragility_fixes track complete"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Track complete. 273/273 tests pass (was 269/272 pre-track, 272/273 mid-track). 3 atomic per-file commits: src/gui_2.py, test_prior_session_no_pop_imbalance.py, new test_workspace_profile_serialization.py." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5 (OPTIONAL): Doc hardening of defer-not-catch sections
|
||||
|
||||
> **Skip this task if time is short.** Per user review 2026-06-05, this is deferred to the end. If you've reached the end of the track with time to spare, do it; otherwise, leave for a follow-up.
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/guide_gui_2.md` "Workspace Profile Defer-Not-Catch" section
|
||||
- Modify: `docs/guide_testing.md` "Early-Render C-Level Crashes" section
|
||||
- Modify: `conductor/workflow.md` "Defer-Not-Catch Pattern for Native Crashes" section
|
||||
|
||||
- [ ] **Step 5.1: Add a one-paragraph note to each of the three docs**
|
||||
|
||||
Add this note (paraphrased to fit each doc's voice) to each of the three defer-not-catch sections:
|
||||
|
||||
> "**Sentinel type contract.** When implementing a defer-not-catch guard, the early-return sentinel value must match the type contract of the downstream consumer. For `WorkspaceProfile.ini_content: str` (in this codebase), the sentinel must be `""` (str), not `b""` (bytes) — `tomli_w` rejects bytes, and `imgui.load_ini_settings_from_memory(ini_data: str, ...)` also expects str. A previous version of this fix used `b""` and silently broke the save flow via a `TypeError` raised by `tomli_w.dump`; tests passed unit-test-wise but failed in the live_gui save+load round-trip."
|
||||
|
||||
- [ ] **Step 5.2: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add docs/guide_gui_2.md docs/guide_testing.md conductor/workflow.md
|
||||
git -C C:\projects\manual_slop commit -m "docs: add sentinel-type-contract note to defer-not-catch sections"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Three doc updates: guide_gui_2.md, guide_testing.md, workflow.md. Added a 'Sentinel type contract' note to each defer-not-catch section warning that the early-return sentinel must match the downstream consumer's type contract (str not bytes for WorkspaceProfile.ini_content). Prevents future regressions of the kind introduced by d7487af4." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
After writing the complete plan, check against the spec:
|
||||
|
||||
**1. Spec coverage:**
|
||||
- Change 1 (`b""` → `""` fix): Task 1, Step 1.3. ✓
|
||||
- Change 2 (test mock fix): Task 2, Step 2.3. ✓
|
||||
- Change 3 (regression unit test): Task 3, Step 3.2. ✓
|
||||
- Change 4 (doc hardening, deferred): Task 5, marked OPTIONAL. ✓
|
||||
- Goals: 100% pass rate (Task 4, Step 4.3). ✓
|
||||
- Non-goals respected: no workspace profile refactor, no wait-for-ready framework, no sloppy.py startup changes. ✓
|
||||
|
||||
**2. Placeholder scan:** No "TBD"/"TODO"/"implement later" patterns. All code blocks are complete. ✓
|
||||
|
||||
**3. Type consistency:** `WorkspaceProfile.ini_content: str` referenced consistently. `b""` → `""` change is the single source of the fix. `_ini_capture_ready` flag is preserved. `str(...) or ""` wrap is documented. ✓
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
This plan is sized for **inline execution** (single agent, no subagents, per the user's stated preference). Execute Tasks 1-4 in order; skip Task 5 if time is short.
|
||||
|
||||
After each task's commit, attach the git note (the `$h` line in each task). After all tasks, run Task 4's full suite to confirm 100% pass.
|
||||
|
||||
If any task fails, stop and run `/conductor:implement --debug` or escalate to a Tier 4 QA analysis (per `conductor/workflow.md`).
|
||||
@@ -1,369 +0,0 @@
|
||||
# Live-GUI State Sync Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Eliminate the App/Controller dual-state bug for the 8 confirmed sync-bug fields. Single source of truth: the Controller. App exposes Controller fields as properties. Restore `test_auto_switch_sim`, `test_workspace_profiles_restoration`, and likely `test_undo_redo_lifecycle`.
|
||||
|
||||
**Architecture:** Add `@property` + `@X.setter` pairs on the `App` class for each sync-bug field. The getter reads `self.controller.X`; the setter writes `self.controller.X`. App-only fields (no Controller counterpart) remain as plain attributes. One regression test encodes the contract.
|
||||
|
||||
**Tech Stack:** Python 3.11+, properties (descriptor protocol), pytest 9.0.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Change | Purpose |
|
||||
|---|---|---|
|
||||
| `src/gui_2.py` | Modify (App class only) | Add 9 property pairs (8 sync-bug fields + `ui_ai_input`) |
|
||||
| `tests/test_app_controller_state_sync.py` | Create | Regression test for the delegation contract |
|
||||
|
||||
No new modules, no architectural refactor.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add the property pair for `ui_ai_input`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` (App class, near other property definitions if any, or after `__init__`)
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git status --short
|
||||
```
|
||||
|
||||
If `src/gui_2.py` has uncommitted changes, stop and ask the user.
|
||||
|
||||
- [ ] **Step 1.2: Read the App class around `__init__` to find a good insertion point**
|
||||
|
||||
Read `src/gui_2.py:130-200` to see how the App class is structured. The property should be at module/class level, ideally in a clearly delimited region. Check if there's an existing `#region: Properties` block or similar.
|
||||
|
||||
- [ ] **Step 1.3: Add the `ui_ai_input` property pair**
|
||||
|
||||
Find the existing `self.ui_ai_input = ...` line in `App.__init__` (search for it). After the `__init__` method ends, add:
|
||||
|
||||
```python
|
||||
@property
|
||||
def ui_ai_input(self) -> str:
|
||||
return self.controller.ui_ai_input
|
||||
|
||||
@ui_ai_input.setter
|
||||
def ui_ai_input(self, value: str) -> None:
|
||||
self.controller.ui_ai_input = value
|
||||
```
|
||||
|
||||
Use exactly 1-space indentation per project style. Use `manual-slop_py_update_definition` with the App class to add the property.
|
||||
|
||||
- [ ] **Step 1.4: Verify the file still parses**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py', encoding='utf-8').read()); print('OK')"
|
||||
```
|
||||
|
||||
Expected: `OK`.
|
||||
|
||||
- [ ] **Step 1.5: Commit (interim checkpoint)**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add src/gui_2.py
|
||||
git -C C:\projects\manual_slop commit -m "fix(gui_2): add ui_ai_input property delegating to controller (sync fix #1 of 9)"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Add @property/@setter for ui_ai_input on the App class. Getter reads self.controller.ui_ai_input; setter writes self.controller.ui_ai_input. This is the first of 9 sync-bug property pairs (ui_ai_input + 7 panel_states + show_windows). The dual state was the root cause of test_undo_redo_lifecycle: snapshot read app.ui_ai_input but set_value wrote controller.ui_ai_input." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add property pairs for `ui_separate_tier1` through `ui_separate_tier4`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` (App class)
|
||||
|
||||
- [ ] **Step 2.1: Add all 4 properties in a batch**
|
||||
|
||||
After the `ui_ai_input` property, add:
|
||||
|
||||
```python
|
||||
@property
|
||||
def ui_separate_tier1(self) -> bool:
|
||||
return self.controller.ui_separate_tier1
|
||||
|
||||
@ui_separate_tier1.setter
|
||||
def ui_separate_tier1(self, value: bool) -> None:
|
||||
self.controller.ui_separate_tier1 = value
|
||||
|
||||
@property
|
||||
def ui_separate_tier2(self) -> bool:
|
||||
return self.controller.ui_separate_tier2
|
||||
|
||||
@ui_separate_tier2.setter
|
||||
def ui_separate_tier2(self, value: bool) -> None:
|
||||
self.controller.ui_separate_tier2 = value
|
||||
|
||||
@property
|
||||
def ui_separate_tier3(self) -> bool:
|
||||
return self.controller.ui_separate_tier3
|
||||
|
||||
@ui_separate_tier3.setter
|
||||
def ui_separate_tier3(self, value: bool) -> None:
|
||||
self.controller.ui_separate_tier3 = value
|
||||
|
||||
@property
|
||||
def ui_separate_tier4(self) -> bool:
|
||||
return self.controller.ui_separate_tier4
|
||||
|
||||
@ui_separate_tier4.setter
|
||||
def ui_separate_tier4(self, value: bool) -> None:
|
||||
self.controller.ui_separate_tier4 = value
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Verify parse + commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py', encoding='utf-8').read()); print('OK')"
|
||||
cd C:\projects\manual_slop; git add src/gui_2.py
|
||||
git -C C:\projects\manual_slop commit -m "fix(gui_2): add ui_separate_tier1..4 property pairs (sync fix #2-5 of 9)"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Add 4 property pairs (ui_separate_tier1..4). These are the 4 fields that test_workspace_profiles_restoration and test_auto_switch_sim exercise. The save reads app.ui_separate_tier1, but set_value writes controller.ui_separate_tier1 -- the property bridges them." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add property pairs for `ui_separate_task_dag` and `ui_separate_usage_analytics`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` (App class)
|
||||
|
||||
- [ ] **Step 3.1: Add both properties**
|
||||
|
||||
```python
|
||||
@property
|
||||
def ui_separate_task_dag(self) -> bool:
|
||||
return self.controller.ui_separate_task_dag
|
||||
|
||||
@ui_separate_task_dag.setter
|
||||
def ui_separate_task_dag(self, value: bool) -> None:
|
||||
self.controller.ui_separate_task_dag = value
|
||||
|
||||
@property
|
||||
def ui_separate_usage_analytics(self) -> bool:
|
||||
return self.controller.ui_separate_usage_analytics
|
||||
|
||||
@ui_separate_usage_analytics.setter
|
||||
def ui_separate_usage_analytics(self, value: bool) -> None:
|
||||
self.controller.ui_separate_usage_analytics = value
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Verify + commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py', encoding='utf-8').read()); print('OK')"
|
||||
cd C:\projects\manual_slop; git add src/gui_2.py
|
||||
git -C C:\projects\manual_slop commit -m "fix(gui_2): add ui_separate_task_dag, ui_separate_usage_analytics property pairs (sync fix #6-7 of 9)"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Add 2 property pairs (ui_separate_task_dag, ui_separate_usage_analytics). These complete the 6 panel_states sync-bug fields. All ui_separate_X fields with Controller settable counterparts are now properties." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add property pair for `show_windows`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` (App class)
|
||||
|
||||
- [ ] **Step 4.1: Add the property (dict type)**
|
||||
|
||||
```python
|
||||
@property
|
||||
def show_windows(self) -> dict:
|
||||
return self.controller.show_windows
|
||||
|
||||
@show_windows.setter
|
||||
def show_windows(self, value: dict) -> None:
|
||||
self.controller.show_windows = value
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Verify + commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run python -c "import ast; ast.parse(open('src/gui_2.py', encoding='utf-8').read()); print('OK')"
|
||||
cd C:\projects\manual_slop; git add src/gui_2.py
|
||||
git -C C:\projects\manual_slop commit -m "fix(gui_2): add show_windows property pair (sync fix #8 of 9)"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Add show_windows property (dict). In-place mutations (app.show_windows['X'] = True) work because the property returns the same dict reference as the controller. Replacements (app.show_windows = new_dict) go through the setter." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Write the regression test
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_app_controller_state_sync.py`
|
||||
|
||||
- [ ] **Step 5.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git status --short
|
||||
```
|
||||
|
||||
- [ ] **Step 5.2: Read the App's `__init__` to find the minimum setup needed for property access**
|
||||
|
||||
Read `src/gui_2.py:130-180` to see App's `__init__`. We need to instantiate an App (or use `__new__` to skip `__init__`) and set up the minimum state for property access.
|
||||
|
||||
- [ ] **Step 5.3: Write the test file**
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from src import app_controller, gui_2
|
||||
|
||||
|
||||
def _make_minimal_app():
|
||||
app = gui_2.App.__new__(gui_2.App)
|
||||
app.controller = app_controller.AppController()
|
||||
app.controller._app = app
|
||||
return app
|
||||
|
||||
|
||||
def test_ui_ai_input_property_delegates_to_controller():
|
||||
app = _make_minimal_app()
|
||||
app.controller.ui_ai_input = "Hello"
|
||||
assert app.ui_ai_input == "Hello"
|
||||
app.ui_ai_input = "World"
|
||||
assert app.controller.ui_ai_input == "World"
|
||||
|
||||
|
||||
def test_ui_separate_tier1_property_delegates_to_controller():
|
||||
app = _make_minimal_app()
|
||||
app.controller.ui_separate_tier1 = True
|
||||
assert app.ui_separate_tier1 is True
|
||||
app.ui_separate_tier1 = False
|
||||
assert app.controller.ui_separate_tier1 is False
|
||||
|
||||
|
||||
def test_ui_separate_tier2_through_tier4_properties_delegate():
|
||||
app = _make_minimal_app()
|
||||
for attr in ("ui_separate_tier2", "ui_separate_tier3", "ui_separate_tier4"):
|
||||
setattr(app.controller, attr, True)
|
||||
assert getattr(app, attr) is True
|
||||
setattr(app, attr, False)
|
||||
assert getattr(app.controller, attr) is False
|
||||
|
||||
|
||||
def test_ui_separate_task_dag_and_usage_analytics_properties_delegate():
|
||||
app = _make_minimal_app()
|
||||
for attr in ("ui_separate_task_dag", "ui_separate_usage_analytics"):
|
||||
setattr(app.controller, attr, True)
|
||||
assert getattr(app, attr) is True
|
||||
setattr(app, attr, False)
|
||||
assert getattr(app.controller, attr) is False
|
||||
|
||||
|
||||
def test_show_windows_property_delegates_to_controller():
|
||||
app = _make_minimal_app()
|
||||
app.controller.show_windows = {"A": True, "B": False}
|
||||
assert app.show_windows == {"A": True, "B": False}
|
||||
app.show_windows = {"C": True}
|
||||
assert app.controller.show_windows == {"C": True}
|
||||
|
||||
|
||||
def test_show_windows_inplace_mutation_visible_to_controller():
|
||||
app = _make_minimal_app()
|
||||
app.controller.show_windows = {"A": False}
|
||||
app.show_windows["A"] = True
|
||||
assert app.controller.show_windows["A"] is True
|
||||
|
||||
|
||||
def test_app_only_panel_states_remain_plain_attributes():
|
||||
app = _make_minimal_app()
|
||||
for attr in ("ui_separate_context_preview", "ui_separate_message_panel",
|
||||
"ui_separate_response_panel", "ui_separate_tool_calls_panel",
|
||||
"ui_separate_external_tools", "ui_discussion_split_h"):
|
||||
assert not hasattr(type(app), attr), \
|
||||
f"{attr} should NOT be a property (no controller counterpart)"
|
||||
```
|
||||
|
||||
Use exactly 1-space indentation.
|
||||
|
||||
- [ ] **Step 5.4: Run the test**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_app_controller_state_sync.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: 7 passed.
|
||||
|
||||
- [ ] **Step 5.5: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add tests/test_app_controller_state_sync.py
|
||||
git -C C:\projects\manual_slop commit -m "test(app_controller): add state sync property regression tests"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "7 tests for the App->Controller state delegation contract. Covers ui_ai_input, ui_separate_tier1..4, ui_separate_task_dag, ui_separate_usage_analytics, show_windows (with both replacement and in-place mutation semantics). Also asserts that App-only fields (ui_separate_context_preview, etc.) are NOT properties." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Run the originally-failing tests to verify the fix
|
||||
|
||||
**Files:** (no file changes; verification only)
|
||||
|
||||
- [ ] **Step 6.1: Run the 3 originally-failing tests**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_auto_switch_sim.py tests/test_workspace_profiles_sim.py tests/test_undo_redo_sim.py -v --timeout=60
|
||||
```
|
||||
|
||||
Expected: all pass (or at minimum: the 2 profile tests pass; undo_redo may still fail if it's a flake unrelated to sync).
|
||||
|
||||
- [ ] **Step 6.2: If `test_undo_redo_sim` still fails, run it in isolation**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_undo_redo_sim.py::test_undo_redo_lifecycle -v --timeout=60
|
||||
```
|
||||
|
||||
If it passes in isolation, it's a flake. Document in the commit note and move on.
|
||||
|
||||
- [ ] **Step 6.3: Commit verification result**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git -c core.autocrlf=false commit --allow-empty -m "verify: state sync fix unblocks test_auto_switch_sim + test_workspace_profiles_restoration"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Verified: test_auto_switch_sim and test_workspace_profiles_restoration now pass. test_undo_redo_lifecycle [passes in isolation / still fails - see other notes]. The App/Controller state sync bug is resolved via the property approach." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Update tracks.md and conductor/index.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/tracks.md` (mark v2 sub-track complete or partial)
|
||||
- Modify: `conductor/index.md` (move v2 sub-track to recently-shipped or note next steps)
|
||||
|
||||
- [ ] **Step 7.1: Update tracks.md**
|
||||
|
||||
Find the live_gui_test_hardening_v2 entry and add a sub-task completion note. Or move to a dedicated entry.
|
||||
|
||||
- [ ] **Step 7.2: Update index.md**
|
||||
|
||||
- [ ] **Step 7.3: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add conductor/tracks.md conductor/index.md
|
||||
git -C C:\projects\manual_slop commit -m "conductor: live_gui_state_sync sub-track complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** All 8 sync-bug fields + `ui_ai_input` (9 total) have property pairs (Tasks 1-4). The regression test (Task 5) covers the delegation contract. Verification (Task 6) runs the originally-failing tests.
|
||||
- **Placeholders:** None.
|
||||
- **Type consistency:** `bool` for `ui_separate_*`, `str` for `ui_ai_input`, `dict` for `show_windows` — matches the existing Controller type hints.
|
||||
- **Risk:** Mid — 9 property pairs added to a 5532-line class. Per-field atomic commits with regression tests mitigate.
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
This plan is sized for **inline execution** (single agent, no subagents, per the user's stated preference). Execute Tasks 1-7 in order; each task ends with an atomic commit + git note.
|
||||
|
||||
After all tasks, the user runs `uv run python scripts/run_tests_batched.py` to confirm 100% pass on the 273-file suite.
|
||||
@@ -1,222 +0,0 @@
|
||||
# prior_session_test_harden_20260605 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:** Rewrite `tests/test_prior_session_no_pop_imbalance.py` to call `gui_2.render_prior_session_view(app_instance)` instead of `gui_2.render_main_interface(app_instance)`. Reduce mocks from 50+ to ~30. Preserve the push/pop balance assertion.
|
||||
|
||||
**Architecture:** Refactor the test scope from kitchen-sink to narrow path. The `render_prior_session_view` function is ~30 lines with a finite mockable set of imgui/imscope calls.
|
||||
|
||||
**Tech Stack:** Python 3.11+, pytest 9.0, unittest.mock.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Change | Purpose |
|
||||
|---|---|---|
|
||||
| `tests/test_prior_session_no_pop_imbalance.py` | Rewrite | Call narrow `render_prior_session_view`; remove 50+ kitchen-sink mocks; keep 30+ scoped mocks |
|
||||
|
||||
No production code changes.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Audit the mocks required by `render_prior_session_view`
|
||||
|
||||
**Files:**
|
||||
- Read: `src/gui_2.py` (the `render_prior_session_view` function, ~30 lines)
|
||||
|
||||
- [ ] **Step 1.1: Read the function**
|
||||
|
||||
Read `src/gui_2.py:render_prior_session_view` to list every imgui/imscope/theme/markdown_helper call it makes.
|
||||
|
||||
- [ ] **Step 1.2: Build the required-mock list**
|
||||
|
||||
From the function body, list:
|
||||
- `imscope.style_color`, `imscope.child`, `imscope.id` (3 context managers)
|
||||
- `imgui.Col_`, `imgui.button`, `imgui.same_line`, `imgui.text_colored`, `imgui.separator`, `imgui.get_content_region_avail`, `imgui.ImVec2`, `imgui.WindowFlags_` (~8 imgui calls)
|
||||
- `theme.get_color`, `theme.ai_text_style` (2 theme calls)
|
||||
- `markdown_helper.render` (1 call)
|
||||
|
||||
**Expected mocks:** ~14 unique mock setups (with side_effects for tracking, maybe 20-25 mock assignments total).
|
||||
|
||||
- [ ] **Step 1.3: Document the list inline**
|
||||
|
||||
Create a one-line comment in the test file or in a comment at the top:
|
||||
|
||||
```python
|
||||
# render_prior_session_view uses: imscope.{style_color, child, id}, imgui.{Col_, button, same_line, text_colored, separator, get_content_region_avail, ImVec2, WindowFlags_}, theme.{get_color, ai_text_style}, markdown_helper.render
|
||||
```
|
||||
|
||||
This becomes the contract.
|
||||
|
||||
- [ ] **Step 1.4: No commit yet (informational step)**
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Rewrite the test file
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_prior_session_no_pop_imbalance.py` (full rewrite)
|
||||
|
||||
- [ ] **Step 2.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git status --short
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Backup the original (optional safety)**
|
||||
|
||||
```powershell
|
||||
cp C:\projects\manual_slop\tests\test_prior_session_no_pop_imbalance.py C:\projects\manual_slop\tests\test_prior_session_no_pop_imbalance.py.bak
|
||||
```
|
||||
|
||||
(This is just a safety net; we won't commit the .bak.)
|
||||
|
||||
- [ ] **Step 2.3: Write the new test file**
|
||||
|
||||
Replace the entire content of `tests/test_prior_session_no_pop_imbalance.py` with:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# render_prior_session_view uses: imscope.{style_color, child, id}, imgui.{Col_, button, same_line, text_colored, separator, get_content_region_avail, ImVec2, WindowFlags_}, theme.{get_color, ai_text_style}, markdown_helper.render
|
||||
|
||||
def test_no_extraneous_pop_when_prior_session_renders():
|
||||
"""Verifies that imscope push/pop balance is maintained when the
|
||||
prior-session render path executes. Calls render_prior_session_view
|
||||
(the narrow function) instead of render_main_interface (kitchen sink).
|
||||
"""
|
||||
from src import gui_2
|
||||
|
||||
app_instance = MagicMock()
|
||||
app_instance.is_viewing_prior_session = True
|
||||
app_instance.perf_profiling_enabled = False
|
||||
app_instance.prior_disc_entries = [
|
||||
{"role": "User", "content": "test", "collapsed": False, "ts": "t1"}
|
||||
]
|
||||
|
||||
push_count = {"n": 0}
|
||||
pop_count = {"n": 0}
|
||||
def _track_push(*a, **k): push_count["n"] += 1
|
||||
def _track_pop(*a, **k): pop_count["n"] += 1
|
||||
|
||||
with patch("src.gui_2.imgui") as mock_imgui, \
|
||||
patch("src.gui_2.imscope") as mock_imscope, \
|
||||
patch("src.gui_2.theme") as mock_theme, \
|
||||
patch("src.gui_2.markdown_helper") as mock_md:
|
||||
|
||||
# imscope context managers: track style_color push/pop, default for child/id
|
||||
mock_imscope.style_color.return_value.__enter__.side_effect = _track_push
|
||||
mock_imscope.style_color.return_value.__exit__.side_effect = lambda *a: (pop_count.__setitem__("n", pop_count["n"] + 1) or False)
|
||||
mock_imscope.child.return_value.__enter__ = MagicMock()
|
||||
mock_imscope.child.return_value.__exit__ = MagicMock(return_value=False)
|
||||
mock_imscope.id.return_value.__enter__ = MagicMock()
|
||||
mock_imscope.id.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# imgui calls
|
||||
mock_imgui.Col_ = MagicMock()
|
||||
mock_imgui.button = MagicMock(return_value=False)
|
||||
mock_imgui.same_line = MagicMock()
|
||||
mock_imgui.text_colored = MagicMock()
|
||||
mock_imgui.separator = MagicMock()
|
||||
mock_imgui.get_content_region_avail = MagicMock(return_value=MagicMock(x=800.0, y=600.0))
|
||||
mock_imgui.ImVec2 = lambda *a: MagicMock(x=a[0], y=a[1])
|
||||
mock_imgui.WindowFlags_ = MagicMock()
|
||||
|
||||
# theme calls
|
||||
mock_theme.get_color = MagicMock(return_value=MagicMock())
|
||||
mock_theme.ai_text_style.return_value.__enter__ = MagicMock()
|
||||
mock_theme.ai_text_style.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# markdown helper
|
||||
mock_md.render = MagicMock()
|
||||
|
||||
gui_2.render_prior_session_view(app_instance)
|
||||
|
||||
assert push_count["n"] == pop_count["n"], f"Push/pop imbalance: pushes={push_count['n']}, pops={pop_count['n']}"
|
||||
```
|
||||
|
||||
Use exactly 1-space indentation. No comments unless the docstring is enough.
|
||||
|
||||
- [ ] **Step 2.4: Remove the backup**
|
||||
|
||||
```powershell
|
||||
Remove-Item C:\projects\manual_slop\tests\test_prior_session_no_pop_imbalance.py.bak
|
||||
```
|
||||
|
||||
- [ ] **Step 2.5: Run the test**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_prior_session_no_pop_imbalance.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: 1 passed.
|
||||
|
||||
- [ ] **Step 2.6: If it fails, diagnose the missing mock**
|
||||
|
||||
The test output will show the missing imgui call. Add the mock and re-run.
|
||||
|
||||
- [ ] **Step 2.7: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add tests/test_prior_session_no_pop_imbalance.py
|
||||
git -C C:\projects\manual_slop commit -m "test(prior_session): rewrite to test narrow render_prior_session_view"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Refactor test to call render_prior_session_view (narrow ~30-line function) instead of render_main_interface (kitchen sink). Reduced mocks from 50+ to ~20. Preserved the push/pop balance assertion. The imscope.window tuple-return issue is bypassed because render_prior_session_view doesn't call imscope.window." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Verify the test runs in the full batched suite
|
||||
|
||||
**Files:** (no file changes; verification only)
|
||||
|
||||
- [ ] **Step 3.1: Run the full test_prior_session_no_pop_imbalance.py**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_prior_session_no_pop_imbalance.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: 1 passed.
|
||||
|
||||
- [ ] **Step 3.2: Commit the verification (no-op)**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git -c core.autocrlf=false commit --allow-empty -m "verify: prior_session test passes in isolation"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Verified the rewritten test passes in isolation. The user will run the full batched suite to confirm 273/273 pass." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Update tracks.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/tracks.md` (note prior_session_test_harden sub-track complete)
|
||||
|
||||
- [ ] **Step 4.1: Add a brief note**
|
||||
|
||||
Find the live_gui_test_hardening_v2 entry and add: "Sub-track `prior_session_test_harden_20260605` complete: test rewritten to call narrow `render_prior_session_view` (50+ mocks → ~20 mocks)."
|
||||
|
||||
- [ ] **Step 4.2: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add conductor/tracks.md
|
||||
git -C C:\projects\manual_slop commit -m "conductor: prior_session_test_harden sub-track complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** Test rewritten to call `render_prior_session_view` (Task 2). Push/pop balance assertion preserved. Mocks reduced from 50+ to ~20.
|
||||
- **Placeholders:** None.
|
||||
- **Type consistency:** Mocks return MagicMock() with appropriate attributes; side_effects match the tracked contract.
|
||||
- **Risk:** Low — only the test file changes; production code is untouched.
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Inline execution. 4 tasks, atomic commits. User runs the full batched suite to confirm.
|
||||
@@ -1,669 +0,0 @@
|
||||
# Regression Fixes — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Fix all test failures observed in the 2026-06-05 full test suite run (272 files in 68 batches). Eleven batches failed. Includes one theme-track regression, four pre-existing non-live_gui failures, and sixteen live_gui failures (mix of startup slowness, real test bugs, and GUI crashes).
|
||||
|
||||
**Architecture:** Each task is a self-contained fix. Theme regression gets a test update. Pre-existing non-live_gui failures get either fixture updates or src changes. Live_gui failures need investigation of root cause (often GUI startup or session lifecycle bugs).
|
||||
|
||||
**Tech Stack:** Python 3.11+, pytest, imgui-bundle, FastAPI/Uvicorn (live_gui), Unittest.mock
|
||||
|
||||
---
|
||||
|
||||
## Failure Inventory
|
||||
|
||||
### A. Theme-Track Regression (1 test)
|
||||
|
||||
| Test | File | Error | Bisect Result |
|
||||
|---|---|---|---|
|
||||
| `test_render_mma_dashboard_progress` | `tests/test_gui_progress.py:80` | `TypeError: __eq__(): incompatible function arguments. The following argument types are supported: 1. __eq__(self, arg: imgui_bundle._imgui_bundle.imgui.ImVec4, /)` | **Theme-caused**, broke at commit `7ea52cbb` (compact TOML formatting and lift semantic colors) |
|
||||
|
||||
**Root cause:** Commit `7ea52cbb` changed `C_LBL` from a module-level `imgui.ImVec4` value to a function call:
|
||||
```python
|
||||
# Before
|
||||
C_LBL: imgui.ImVec4 = vec4(180, 180, 180)
|
||||
# After
|
||||
def C_LBL() -> imgui.ImVec4: return theme.get_color("text_disabled")
|
||||
```
|
||||
The test does `mock_imgui.text_colored.assert_any_call(C_LBL(), "Completed:")`. `C_LBL()` now calls `theme.get_color("text_disabled")` which uses the **real** `imgui.ImVec4` from `src/theme_2.py` (the test only patches `src.gui_2.imgui` and `src.imgui_scopes.imgui`, not `src.theme_2.imgui`). The real `ImVec4.__eq__` rejects the MagicMock argument from `assert_any_call`.
|
||||
|
||||
**Fix:** Adapt the test to mock `src.theme_2.imgui` properly. Per AGENTS.md: "DO NOT CREATE MOCK PATCHES TO PSEUDO API CALLS OR HOOKS BECAUSE THE APP SOURCE WAS CHANGED. ADAPT TESTS PROPERLY."
|
||||
|
||||
### B. Pre-Existing Non-live_gui Failures (4 tests)
|
||||
|
||||
| Test | File | Error | Bisect Result |
|
||||
|---|---|---|---|
|
||||
| `test_track_discussion_toggle` | `tests/test_gui_phase4.py:124` | `RuntimeError: IM_ASSERT( GImGui != 0 && ...)` in `src/markdown_helper.py:147` (`imgui.spacing()`) | **Pre-existing**, fails at commit `7df65dff` (pre-theme) |
|
||||
| `test_no_extraneous_pop_when_prior_session_renders` | `tests/test_prior_session_no_pop_imbalance.py:132` | `AttributeError: 'tuple' object has no attribute 'x'` in `src/shaders.py:10` | **Pre-existing**, fails at commit `7df65dff` |
|
||||
| `test_load_presets_from_project_list` | `tests/test_view_presets.py:95` | `AttributeError: 'AppController' object has no attribute 'persona_manager'` in `src/app_controller.py:2851` | **Pre-existing**, fails at commit `7df65dff` |
|
||||
| `test_load_presets_from_project_legacy_dict` | `tests/test_view_presets.py:112` | Same as above | **Pre-existing** |
|
||||
|
||||
**Root causes:**
|
||||
- `test_track_discussion_toggle`: `src/markdown_helper.py:147` calls `imgui.spacing()` in `flush_md()` after `imgui_md.render()`. Test mocks `imgui_md.render` to no-op but `imgui.spacing()` is not mocked, causing IM_ASSERT when no ImGui context exists.
|
||||
- `test_no_extraneous_pop_when_prior_session_renders`: `src/shaders.py:10` does `r, g, b, a = color.x, color.y, color.z, color.w` where `color` should be an `imgui.ImVec4`. Test's mock `color` is a `tuple` from `("ImVec4", a)` mock lambda.
|
||||
- `test_view_presets.py x2`: Test fixture doesn't initialize `ctrl.persona_manager` even though `_refresh_from_project` calls `self.persona_manager.load_all()`.
|
||||
|
||||
**Fixes:** Adapt the tests to mock the necessary calls properly (no mock-patches-for-changed-API shortcuts).
|
||||
|
||||
### C. Live_gui Failures (16 tests)
|
||||
|
||||
| Test | File | Failure Mode | Pattern |
|
||||
|---|---|---|---|
|
||||
| `test_auto_switch_sim` | `tests/test_auto_switch_sim.py:47` | `assert client.get_value('show_windows').get('Diagnostics', False) == True` | Workspace auto-switch logic not applying Tier 3 profile (GUI starts fine, assertion fails) |
|
||||
| `test_context_sim_live` | `tests/test_extended_sims.py:27` | `assert len(entries) >= 2, f"Expected at least 2 entries, found {len(entries)}"` | GUI runs, AI responds, but session entries empty |
|
||||
| `test_ai_settings_sim_live` | `tests/test_extended_sims.py:35` | `assert client.wait_for_server(timeout=10)` | GUI process died after `test_context_sim_live` |
|
||||
| `test_tools_sim_live` | `tests/test_extended_sims.py:49` | Same | Same |
|
||||
| `test_execution_sim_live` | `tests/test_extended_sims.py:62` | Same | Same |
|
||||
| `test_full_live_workflow` | `tests/test_live_workflow.py:140` | `assert success, f"AI failed to respond. Entries: {client.get_session()}, Status: {client.get_mma_status()}"` | AI never responded (status always `None`) |
|
||||
| `test_mma_concurrent_tracks_execution` | `tests/test_mma_concurrent_tracks_sim.py:58` | `assert ok, f"Proposed tracks not found: {status.get('proposed_tracks')}"` | MMA epic plan never produced tracks |
|
||||
| `test_mma_concurrent_tracks_stress` | `tests/test_mma_concurrent_tracks_stress_sim.py:33` | `assert client.wait_for_server(timeout=15)` | Hook server didn't start |
|
||||
| `test_mma_step_mode_approval_flow` | `tests/test_mma_step_mode_sim.py:48` | `KeyError: 'tracks'` | Tracks never created after plan epic |
|
||||
| `test_phase4_final_verify` | `tests/test_rag_phase4_final_verify.py:78` | `if "error" in status.lower():` raises `AttributeError: 'NoneType' object has no attribute 'lower'` | Test doesn't handle `status=None` from `state.get('ai_status')` |
|
||||
| `test_rag_large_codebase_verification_sim` | `tests/test_rag_phase4_stress.py:17` | `assert client.wait_for_server(timeout=15)` | Hook server didn't start |
|
||||
| `test_rag_full_lifecycle_sim` | `tests/test_rag_visual_sim.py:17` | Same | Same |
|
||||
| `test_rag_settings_persistence_sim` | `tests/test_rag_visual_sim.py:81` | Same | Same |
|
||||
| `test_mma_complete_lifecycle` | `tests/test_visual_sim_mma_v2.py:92` | Timeout after 100s polling | Proposed tracks never appear |
|
||||
| `test_mock_malformed_json` | `tests/test_z_negative_flows.py:40` | `assert event is not None, "Did not receive terminal response event"` | Response event never received |
|
||||
| `test_mock_error_result` | `tests/test_z_negative_flows.py:51` | `assert client.wait_for_server(timeout=15)` | Hook server didn't start |
|
||||
| `test_mock_timeout` | `tests/test_z_negative_flows.py:93` | Same | Same |
|
||||
|
||||
**Pattern groups:**
|
||||
1. **GUI startup slowness (LogPruner busy loop):** Tests fail with "Hook server did not start" within 15s. The `LogPruner` is in a tight loop trying to delete locked log files (file still in use by the GUI process). This blocks the main thread from starting the FastAPI hook server promptly. **Affects:** `test_mma_concurrent_tracks_stress`, `test_rag_large_codebase_verification_sim`, `test_rag_full_lifecycle_sim`, `test_rag_settings_persistence_sim`, `test_mock_error_result`, `test_mock_timeout`, and the second/third/fourth tests in `test_extended_sims.py` (which die from cascading failure after first test).
|
||||
2. **Session entries not populated:** `test_context_sim_live` (and likely the extended_sims cascade). AI sends a response but no entries show up in `client.get_session()`. Could be a real bug in session/entry tracking.
|
||||
3. **MMA pipeline doesn't reach "tracks" state:** `test_mma_concurrent_tracks_execution`, `test_mma_step_mode_approval_flow`, `test_mma_complete_lifecycle`. All of these use the gemini_cli mock provider, call `btn_mma_plan_epic`, and then poll for `proposed_tracks` / `tracks`. None of them get them. Could be a real bug in MMA pipeline or the mock provider.
|
||||
4. **AI never responds:** `test_full_live_workflow`. The status stays `None` for 20 seconds, then the test times out.
|
||||
5. **Auto-switch layout not applying:** `test_auto_switch_sim`. The test triggers an MMA state update with `active_tier='Tier 3 (Worker): task-1'`, but the workspace profile doesn't auto-apply.
|
||||
6. **Test code bugs (not app bugs):** `test_rag_phase4_final_verify` doesn't handle `status=None`. `test_rag_phase4_stress` etc. depend on GUI startup being faster.
|
||||
|
||||
|
||||
## Execution Status (2026-06-05 - Updated)
|
||||
|
||||
| Task | Status | Commit |
|
||||
|---|---|---|
|
||||
| Task 1 (theme regression) | DONE | 38abf231 |
|
||||
| Task 2a (gui_phase4) | DONE | df43f158 |
|
||||
| Task 2b (prior_session) | PARTIAL (test still fails deeper) | f829d1df |
|
||||
| Task 2c (view_presets) | DONE | 970f198c |
|
||||
| Task 3a (LogPruner) | DONE | ac08ee87 |
|
||||
| Task 3b (session entries) | ROOT CAUSE FOUND (task 2b-related) | - |
|
||||
| Task 3c (MMA pipeline) | DEFERRED (live GUI + C-level crash) | - |
|
||||
| Task 3d (RAG NoneType) | DONE | c96bdb06 |
|
||||
| Task 3e (live workflow) | DEFERRED (live GUI + C-level crash) | - |
|
||||
| Task 3f (auto_switch) | DEFERRED (live GUI + C-level crash) | - |
|
||||
| Task 3g (z_negative_flows) | DEFERRED (live GUI + C-level crash) | - |
|
||||
|
||||
### BONUS FIX: GUI Production Bug (theme-caused)
|
||||
|
||||
**Commit 1469ecac** - Fixed `gui_2.py:3705-3707` where `DIR_COLORS.get(direction, C_VAL())`
|
||||
returned the callable function instead of calling it. This was causing
|
||||
`imgui.text_colored` to receive a function instead of `ImVec4`, raising
|
||||
TypeError on EVERY GUI frame in `render_comms_history_panel`. The error was
|
||||
caught by `_gui_func`'s except block so the GUI continued, but the Operations
|
||||
Hub comms panel was completely broken. This is the THEME-CAUSED production
|
||||
bug that was masking other test failures.
|
||||
|
||||
### ROOT CAUSE OF REMAINING LIVE_GUI FAILURES
|
||||
|
||||
The remaining 12 live_gui tests fail because the `sloppy.py` subprocess
|
||||
crashes with a C-level access violation (`0xc0000005`) in
|
||||
`_imgui_bundle.cp311-win_amd64.pyd`. This is a native crash, not a Python
|
||||
exception, so it cannot be caught or debugged from Python.
|
||||
|
||||
**Event Viewer log evidence:**
|
||||
```
|
||||
Faulting module name: _imgui_bundle.cp311-win_amd64.pyd
|
||||
Exception code: 0xc0000005
|
||||
Fault offset: 0x00000000011424ae
|
||||
```
|
||||
|
||||
**Why this blocks all live_gui tests:**
|
||||
- `test_gui_startup_smoke` PASSES (basic startup works)
|
||||
- All more complex live_gui tests fail (the GUI process dies after a few
|
||||
render frames when user input triggers deeper code paths)
|
||||
- The crash is non-deterministic (different fault offsets between runs),
|
||||
suggesting memory corruption from C-side state
|
||||
|
||||
**What's needed to unblock:**
|
||||
1. Capture a full crash dump from `_imgui_bundle.cp311-win_amd64.pyd`
|
||||
2. Identify the specific imgui function causing the crash
|
||||
3. Find the call site in `src/gui_2.py` that triggers it
|
||||
4. Fix the call (e.g., pass correct type, add null check, init context)
|
||||
|
||||
This requires:
|
||||
- A Windows debugger (WinDbg) or crash dump analysis
|
||||
- A reproducer script that crashes 100% of the time
|
||||
- Familiarity with imgui-bundle's C++ internals
|
||||
|
||||
### DEFERRED TASKS REQUIRING ABOVE
|
||||
|
||||
Tasks 3b-3g all depend on the live_gui fixture, which can't survive long
|
||||
enough to run the test bodies. After fixing the underlying crash, the
|
||||
deferred tasks should become tractable with normal test debugging.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Execution Constraints
|
||||
|
||||
- **No subagents.** Execute as a single agent (per user request).
|
||||
- **Per-file atomic commits.**
|
||||
- **Commit message format:** `<type>(<scope>): <imperative description>`.
|
||||
- **Git note format:** 3-8 line rationale per commit.
|
||||
- **Style baseline:** 1-space indent, no comments, type hints.
|
||||
- **Tests required:** every fix must include a passing test, not just patch existing ones.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `tests/test_gui_progress.py` | Modify | Adapt to new `C_LBL()` function API (Task 1) |
|
||||
| `tests/test_gui_phase4.py` | Modify | Mock `imgui.spacing()` in `flush_md` (Task 2) |
|
||||
| `tests/test_prior_session_no_pop_imbalance.py` | Modify | Use proper ImVec4 mock OR fix `shaders.py:10` to accept tuple (Task 2) |
|
||||
| `tests/test_view_presets.py` | Modify | Add `persona_manager` mock to fixture (Task 2) |
|
||||
| `src/markdown_helper.py` | Modify | Defensive guard around `imgui.spacing()` in `flush_md` (optional, if test-only fix is preferred) |
|
||||
| `src/shaders.py` | Modify | Defensive guard for tuple input in `draw_soft_shadow` (optional) |
|
||||
| `src/app_controller.py` | Modify | Defensive `hasattr(self, 'persona_manager')` check in `_refresh_from_project` (optional) |
|
||||
| `src/log_pruner.py` | Modify | Add backoff/retry to avoid blocking the main thread on locked log files (Task 3) |
|
||||
| `src/...` (various) | Investigate | Live_gui test fixes (Task 3) — need investigation per failure |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Fix theme-track regression in `test_gui_progress.py`
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_gui_progress.py`
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Read current test fixture**
|
||||
|
||||
Read `tests/test_gui_progress.py:1-30` to see the existing `with patch(...)` block.
|
||||
|
||||
- [ ] **Step 1.3: Add `src.theme_2.imgui` to the patch list**
|
||||
|
||||
In `tests/test_gui_progress.py`, locate the existing `with patch(...)` block (around line 25-28). Add `patch("src.theme_2.imgui", new=mock_imgui)` to the context manager chain so `theme.get_color()` returns the mocked `ImVec4` instead of the real one.
|
||||
|
||||
Current pattern (approximate):
|
||||
```python
|
||||
with patch('src.gui_2.imgui', mock_imgui), \
|
||||
patch('src.imgui_scopes.imgui', new=mock_imgui), \
|
||||
patch('src.gui_2.cost_tracker.estimate_cost', return_value=0.0):
|
||||
```
|
||||
|
||||
Change to:
|
||||
```python
|
||||
with patch('src.gui_2.imgui', mock_imgui), \
|
||||
patch('src.imgui_scopes.imgui', new=mock_imgui), \
|
||||
patch('src.theme_2.imgui', new=mock_imgui), \
|
||||
patch('src.gui_2.cost_tracker.estimate_cost', return_value=0.0):
|
||||
```
|
||||
|
||||
- [ ] **Step 1.4: Run test to verify it passes**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_gui_progress.py::test_render_mma_dashboard_progress -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 1.5: Run full test_gui_progress.py to check no regressions**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_gui_progress.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 1.6: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add tests/test_gui_progress.py
|
||||
git -C C:\projects\manual_slop commit -m "test(gui_progress): patch src.theme_2.imgui for C_LBL() function API"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "The 7ea52cbb commit changed C_LBL from an ImVec4 value to a C_LBL() function that calls theme.get_color. The test patches src.gui_2.imgui but theme.get_color uses the real imgui binding from src.theme_2. Adding patch('src.theme_2.imgui', new=mock_imgui) makes theme.get_color return the mock's ImVec4, so assert_any_call can compare it." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Fix pre-existing non-live_gui test failures
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_gui_phase4.py`
|
||||
- Modify: `tests/test_prior_session_no_pop_imbalance.py`
|
||||
- Modify: `tests/test_view_presets.py`
|
||||
|
||||
### Task 2a: Fix `test_track_discussion_toggle` (gui_phase4)
|
||||
|
||||
- [ ] **Step 2.1: Read test setup**
|
||||
|
||||
Read `tests/test_gui_phase4.py:80-130` to see the `mock_imgui` setup and find the `imgui_md.render` patch.
|
||||
|
||||
- [ ] **Step 2.2: Add `imgui_md.render` and `imgui.spacing` mocks if missing**
|
||||
|
||||
In the test's `with patch(...)` block, ensure the following mocks exist (most are already present per the captured traceback; verify):
|
||||
- `mock_imgui_md.render` is mocked to a no-op (or use a real one with the right return)
|
||||
- `mock_imgui.spacing` is mocked to a no-op (the traceback shows this is the failing call at `src/markdown_helper.py:147`)
|
||||
|
||||
If `imgui.spacing` is NOT already mocked, add it. The traceback shows the call is:
|
||||
```python
|
||||
imgui_md.render(chunk) # mocked, no-op
|
||||
imgui.spacing() # NOT mocked, fails IM_ASSERT
|
||||
```
|
||||
|
||||
Add `mock_imgui.spacing = MagicMock()` to the test fixture.
|
||||
|
||||
- [ ] **Step 2.3: Run test to verify it passes**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_gui_phase4.py::test_track_discussion_toggle -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2.4: Run full test_gui_phase4.py**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_gui_phase4.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 2.5: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add tests/test_gui_phase4.py
|
||||
git -C C:\projects\manual_slop commit -m "test(gui_phase4): mock imgui.spacing to avoid IM_ASSERT in markdown_helper"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "markdown_helper.flush_md calls imgui_md.render then imgui.spacing. The test mocks imgui_md.render but not imgui.spacing, so the second call hits the real imgui with no context and IM_ASSERT fails. Adding mock_imgui.spacing = MagicMock() prevents the assertion." $h
|
||||
```
|
||||
|
||||
### Task 2b: Fix `test_no_extraneous_pop_when_prior_session_renders` (prior_session)
|
||||
|
||||
- [ ] **Step 2.6: Investigate root cause**
|
||||
|
||||
Read `src/shaders.py:1-30` to see the `draw_soft_shadow` function. Confirm it does `r, g, b, a = color.x, color.y, color.z, color.w` which requires `color` to be a real `imgui.ImVec4` (not a tuple).
|
||||
|
||||
The test mock creates `color` as a tuple via `("ImVec4", a)` lambda. Two options:
|
||||
|
||||
**Option A (test fix):** Update the test mock to use `MagicMock(side_effect=lambda *a: type("ImVec4", (), {"x": a[0], "y": a[1], "z": a[2], "w": a[3]})(*a))` so the mock returns an object with `.x`/`.y`/`.z`/`.w` attributes.
|
||||
|
||||
**Option B (src fix):** Update `src/shaders.py:10` to accept tuple OR `ImVec4`:
|
||||
```python
|
||||
if hasattr(color, "x"):
|
||||
r, g, b, a = color.x, color.y, color.z, color.w
|
||||
elif isinstance(color, (tuple, list)) and len(color) == 4:
|
||||
r, g, b, a = color
|
||||
```
|
||||
|
||||
**Recommendation:** Option B — make the function defensive. Real `ImVec4` objects are passed at runtime; tests use tuples as a simplification. Both should work.
|
||||
|
||||
- [ ] **Step 2.7: Apply src fix to `src/shaders.py`**
|
||||
|
||||
Read current `src/shaders.py:1-15` and modify the unpacking in `draw_soft_shadow` to handle both `ImVec4` and tuple/list inputs:
|
||||
```python
|
||||
def draw_soft_shadow(draw_list, p_min, p_max, color, shadow_size=10.0, rounding=0.0) -> None:
|
||||
if hasattr(color, "x"):
|
||||
r, g, b, a = color.x, color.y, color.z, color.w
|
||||
else:
|
||||
r, g, b, a = color
|
||||
...
|
||||
```
|
||||
|
||||
Use 1-space indent. The rest of the function is unchanged.
|
||||
|
||||
- [ ] **Step 2.8: Run test to verify it passes**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_prior_session_no_pop_imbalance.py::test_no_extraneous_pop_when_prior_session_renders -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2.9: Run full test_prior_session_no_pop_imbalance.py**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_prior_session_no_pop_imbalance.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 2.10: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add src/shaders.py
|
||||
git -C C:\projects\manual_slop commit -m "fix(shaders): draw_soft_shadow accepts tuple or ImVec4 color"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Tests pass tuple mocks for color but the function expected ImVec4.x/.y/.z/.w attributes. Adding a hasattr fallback to unpack from a 4-tuple makes the function more permissive without changing real-app behavior (the real call path always passes a real ImVec4)." $h
|
||||
```
|
||||
|
||||
### Task 2c: Fix `test_view_presets.py` (missing `persona_manager`)
|
||||
|
||||
- [ ] **Step 2.11: Read test fixture**
|
||||
|
||||
Read `tests/test_view_presets.py:7-37` to see the `controller` fixture.
|
||||
|
||||
- [ ] **Step 2.12: Add `persona_manager` mock**
|
||||
|
||||
After the existing `tool_preset_manager` mock line, add:
|
||||
```python
|
||||
ctrl.persona_manager = type('Mock', (), {'load_all': lambda self: {}})()
|
||||
```
|
||||
|
||||
- [ ] **Step 2.13: Run tests to verify they pass**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_view_presets.py -v --timeout=15
|
||||
```
|
||||
|
||||
Expected: all tests pass (5 total).
|
||||
|
||||
- [ ] **Step 2.14: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add tests/test_view_presets.py
|
||||
git -C C:\projects\manual_slop commit -m "test(view_presets): mock persona_manager in fixture"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "AppController._refresh_from_project calls self.persona_manager.load_all() but the test fixture only mocks preset_manager and tool_preset_manager. Adding a minimal persona_manager mock (load_all returns empty dict) makes the test pass without requiring the full PersonaManager class." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Investigate and fix live_gui test failures
|
||||
|
||||
This is the largest task. The 16 failures fall into 4 pattern groups. Each needs investigation before a fix can be planned.
|
||||
|
||||
### Sub-Task 3a: Fix LogPruner busy loop blocking GUI startup
|
||||
|
||||
The "Hook server did not start" pattern occurs because `LogPruner` is in a tight retry loop on locked log files. This blocks the main GUI thread from initializing the FastAPI hook server.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/log_pruner.py`
|
||||
|
||||
- [ ] **Step 3.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add .
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Read current LogPruner code**
|
||||
|
||||
Read `src/log_pruner.py` to find the busy loop. The test output shows:
|
||||
```
|
||||
[LogPruner] Removing 20260605_094323 at C:\projects\manual_slop\logs\20260605_094323 (Size: 0 bytes)
|
||||
[LogPruner] Error removing C:\projects\manual_slop\logs\20260605_094323: [WinError 32] The process cannot access the file...
|
||||
[LogPruner] Removing 20260605_095304 at C:\projects\manual_slop\logs\20260605_095304 (Size: 0 bytes)
|
||||
[LogPruner] Error removing C:\projects\manual_slop\logs\20260605_095304: [WinError 32] ...
|
||||
```
|
||||
Tight loop on `WinError 32` (sharing violation).
|
||||
|
||||
- [ ] **Step 3.3: Add exponential backoff and skip-on-lock to LogPruner**
|
||||
|
||||
Modify the LogPruner's `prune` method to:
|
||||
1. Add a `time.sleep(0.1)` after a `WinError 32` to avoid tight-looping.
|
||||
2. Skip locked files on the first pass; try again on the next prune cycle.
|
||||
3. Cap the number of retry attempts per file per cycle.
|
||||
|
||||
Use 1-space indent.
|
||||
|
||||
- [ ] **Step 3.4: Run live_gui test to verify startup completes**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_auto_switch_sim.py -v --timeout=60
|
||||
```
|
||||
|
||||
Expected: PASS (or at least: hook server starts in <15s).
|
||||
|
||||
- [ ] **Step 3.5: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add src/log_pruner.py
|
||||
git -C C:\projects\manual_slop commit -m "fix(log_pruner): avoid tight retry loop on locked log files"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "The pruner was in a tight loop on WinError 32 (file in use) trying to delete logs the GUI process still holds. Added sleep + skip-on-lock to release the main thread so the FastAPI hook server can start. This unblocks 7+ live_gui tests that were timing out at wait_for_server(timeout=15)." $h
|
||||
```
|
||||
|
||||
### Sub-Task 3b: Investigate session entries not populated
|
||||
|
||||
`test_context_sim_live` runs an AI turn successfully (status: "md written: project_001.md") but no entries show in `client.get_session()`.
|
||||
|
||||
**Files:**
|
||||
- Investigate: `src/app_controller.py`, `src/session_logger.py`
|
||||
|
||||
- [ ] **Step 3.6: Add debug logging to test**
|
||||
|
||||
Read `tests/test_extended_sims.py:27-65` to see the test flow. Add a print statement before the assertion to dump `client.get_session()` and `client.get_mma_status()` to confirm the empty entries state.
|
||||
|
||||
- [ ] **Step 3.7: Run test with debug output**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_extended_sims.py::test_context_sim_live -v --timeout=60 -s
|
||||
```
|
||||
|
||||
Expected: see session structure with empty entries.
|
||||
|
||||
- [ ] **Step 3.8: Trace session update path**
|
||||
|
||||
Read `src/app_controller.py` to find where `disc_entries` gets updated after an AI turn. Verify that `self.disc_entries` is properly updated and the session endpoint returns the right structure.
|
||||
|
||||
- [ ] **Step 3.9: Identify and fix the bug**
|
||||
|
||||
(This will be determined by the investigation. Common causes: thread safety issue, missing lock, endpoint not refreshing from controller state, async task not awaited.)
|
||||
|
||||
- [ ] **Step 3.10: Run test to verify it passes**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_extended_sims.py::test_context_sim_live -v --timeout=60
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3.11: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add <modified files>
|
||||
git -C C:\projects\manual_slop commit -m "fix(session): <description from investigation>"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "..." $h
|
||||
```
|
||||
|
||||
### Sub-Task 3c: Investigate MMA pipeline not creating tracks
|
||||
|
||||
`test_mma_concurrent_tracks_execution`, `test_mma_step_mode_approval_flow`, `test_mma_complete_lifecycle` all call `btn_mma_plan_epic` with a mock gemini_cli provider, but `proposed_tracks` / `tracks` never appear.
|
||||
|
||||
**Files:**
|
||||
- Investigate: `src/multi_agent_conductor.py`, `src/dag_engine.py`, `src/api_hooks.py`, `tests/mock_gemini_cli.py`
|
||||
|
||||
- [ ] **Step 3.12: Run one test with -s to see the full poll output**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_mma_step_mode_sim.py::test_mma_step_mode_approval_flow -v --timeout=300 -s 2>&1 | Select-String "SIM|mma|tracks|proposed" | Select-Object -First 30
|
||||
```
|
||||
|
||||
Expected: see polling output and the failing poll condition.
|
||||
|
||||
- [ ] **Step 3.13: Inspect the mock gemini_cli response**
|
||||
|
||||
Read `tests/mock_gemini_cli.py` to verify it returns a valid track-proposal response for the epic input.
|
||||
|
||||
- [ ] **Step 3.14: Trace the proposal pipeline**
|
||||
|
||||
In `src/multi_agent_conductor.py`, find the `plan_epic` flow and verify it:
|
||||
1. Calls the mock provider
|
||||
2. Parses the response into `proposed_tracks`
|
||||
3. Sets `self.proposed_tracks` so `get_mma_status()` returns it
|
||||
|
||||
- [ ] **Step 3.15: Identify and fix the bug**
|
||||
|
||||
(Possible causes: mock provider path not being passed correctly, response parser failing silently, thread-safety issue with `proposed_tracks` field.)
|
||||
|
||||
- [ ] **Step 3.16: Run tests to verify they pass**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_mma_concurrent_tracks_sim.py tests/test_mma_concurrent_tracks_stress_sim.py tests/test_mma_step_mode_sim.py -v --timeout=300
|
||||
```
|
||||
|
||||
Expected: all PASS.
|
||||
|
||||
- [ ] **Step 3.17: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add <modified files>
|
||||
git -C C:\projects\manual_slop commit -m "fix(mma): <description from investigation>"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "..." $h
|
||||
```
|
||||
|
||||
### Sub-Task 3d: Fix test code bugs (not app bugs)
|
||||
|
||||
`test_rag_phase4_final_verify::test_phase4_final_verify` has:
|
||||
```python
|
||||
if "error" in status.lower():
|
||||
```
|
||||
But `status` is `None` when polling doesn't return one. This is a test bug — the test should handle None.
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_rag_phase4_final_verify.py`
|
||||
|
||||
- [ ] **Step 3.18: Read the test**
|
||||
|
||||
Read `tests/test_rag_phase4_final_verify.py:60-85` to see the poll loop.
|
||||
|
||||
- [ ] **Step 3.19: Add None check**
|
||||
|
||||
Change:
|
||||
```python
|
||||
if "error" in status.lower():
|
||||
```
|
||||
to:
|
||||
```python
|
||||
if status and "error" in status.lower():
|
||||
```
|
||||
|
||||
- [ ] **Step 3.20: Run test to verify it passes**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_rag_phase4_final_verify.py -v --timeout=60
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3.21: Commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop add tests/test_rag_phase4_final_verify.py
|
||||
git -C C:\projects\manual_slop commit -m "test(rag_phase4): handle None status in error check"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "The poll loop doesn't always return a status string. Added a None guard before calling .lower() to prevent AttributeError when status is missing. Real app status is always set, but test should be robust." $h
|
||||
```
|
||||
|
||||
### Sub-Task 3e: Investigate `test_full_live_workflow` AI never responding
|
||||
|
||||
`test_full_live_workflow` polls `ai_status` for 20s, never gets a non-None value.
|
||||
|
||||
**Files:**
|
||||
- Investigate: `src/app_controller.py`, `src/ai_client.py`
|
||||
|
||||
- [ ] **Step 3.22: Run with -s to see full poll output**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_live_workflow.py::test_full_live_workflow -v --timeout=120 -s 2>&1 | Select-String "Poll|status|set_value|click" | Select-Object -First 30
|
||||
```
|
||||
|
||||
- [ ] **Step 3.23: Trace the AI request path**
|
||||
|
||||
Investigate why `ai_status` is never set after `btn_gen_send`. The test sets `current_provider='gemini'`, `current_model='gemini-2.5-flash-lite'`, sends a message, then expects status to change to 'sending...' or 'streaming...'.
|
||||
|
||||
- [ ] **Step 3.24: Identify and fix the bug**
|
||||
|
||||
- [ ] **Step 3.25: Run test to verify it passes**
|
||||
|
||||
- [ ] **Step 3.26: Commit**
|
||||
|
||||
### Sub-Task 3f: Investigate `test_auto_switch_sim` workspace profile not applying
|
||||
|
||||
The test triggers `mma_state_update` with `active_tier='Tier 3 (Worker): task-1'` but the bound workspace profile doesn't auto-apply.
|
||||
|
||||
**Files:**
|
||||
- Investigate: `src/workspace_manager.py`, `src/gui_2.py` (auto-switch handler)
|
||||
|
||||
- [ ] **Step 3.27: Read test and find auto-switch handler**
|
||||
|
||||
Read `tests/test_auto_switch_sim.py:30-50` and find the auto-switch handler in `src/gui_2.py` (search for `ui_auto_switch_layout` or `auto_switch`).
|
||||
|
||||
- [ ] **Step 3.28: Identify the bug**
|
||||
|
||||
(Possible causes: tier name mismatch, profile name not loading correctly, switch never fires.)
|
||||
|
||||
- [ ] **Step 3.29: Run test to verify it passes**
|
||||
|
||||
- [ ] **Step 3.30: Commit**
|
||||
|
||||
### Sub-Task 3g: Investigate `test_z_negative_flows` (3 tests)
|
||||
|
||||
`test_mock_malformed_json`, `test_mock_error_result`, `test_mock_timeout` all fail. The first fails because the response event never arrives; the others fail on hook server startup.
|
||||
|
||||
- [ ] **Step 3.31: Wait for Sub-Task 3a to complete (LogPruner fix)**
|
||||
|
||||
These tests depend on the GUI starting successfully. The "Hook server did not start" failures will likely be fixed by the LogPruner fix in 3a.
|
||||
|
||||
- [ ] **Step 3.32: Run the three tests to see which still fail**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_z_negative_flows.py -v --timeout=60
|
||||
```
|
||||
|
||||
- [ ] **Step 3.33: Investigate `test_mock_malformed_json` separately**
|
||||
|
||||
If it still fails after 3a, investigate the response event delivery for the malformed JSON case.
|
||||
|
||||
- [ ] **Step 3.34: Identify and fix any remaining bugs**
|
||||
|
||||
- [ ] **Step 3.35: Commit**
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Phase Completion Verification
|
||||
|
||||
- [ ] **Step 4.1: Run full test suite to verify all fixes**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run python scripts/run_tests_batched.py
|
||||
```
|
||||
|
||||
Expected: 0 failed batches. (Skips allowed.)
|
||||
|
||||
- [ ] **Step 4.2: Address any new failures**
|
||||
|
||||
If new failures emerge, add them to the regression list and create follow-up tasks.
|
||||
|
||||
- [ ] **Step 4.3: Create checkpoint commit**
|
||||
|
||||
```powershell
|
||||
git -C C:\projects\manual_slop commit --allow-empty -m "conductor(checkpoint): Regression fixes complete"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "All 21 test failures from 2026-06-05 full suite run resolved. 1 theme-track regression, 4 pre-existing non-live_gui failures, and 16 live_gui failures (mix of environment, app bugs, and test bugs) fixed. See plan.md for individual task rationales." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** All 21 failures from the 11 failed batches are covered: 1 in Task 1, 4 in Task 2, 16 in Task 3.
|
||||
- **Placeholder scan:** Sub-tasks 3b, 3c, 3e, 3f, 3g have investigation steps before fix steps because the root cause needs to be determined at runtime. The plan explicitly says "Identify and fix the bug" with a "commit" step that will document what was found. No TBDs.
|
||||
- **Type consistency:** All tests modified keep their existing signatures. Source changes are defensive guards (no API changes).
|
||||
- **Constraint compliance:** No subagents (per user request). Per-file atomic commits. Style baseline 1-space indent.
|
||||
|
||||
## Execution Notes for User
|
||||
|
||||
The user said "Don't spawn workers, you'll need todo the fixes after planning" — meaning **you will execute these tasks yourself** (not me or subagents). The plan above is structured so each task can be done by hand:
|
||||
|
||||
- Task 1, Task 2a, 2b, 2c: Source-level changes are small (~5 lines each), can be done with `manual-slop_edit_file` or `manual-slop_py_update_definition`.
|
||||
- Task 3: Investigation-heavy. Sub-tasks 3a, 3d are deterministic (LogPruner busy loop, None check). 3b, 3c, 3e, 3f, 3g need actual debugging with the live GUI.
|
||||
|
||||
Run the verification batched test script at the end of each sub-task to confirm no new failures.
|
||||
@@ -1,161 +0,0 @@
|
||||
# undo_redo_lifecycle_fix_20260605 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:** Resolve the `test_undo_redo_lifecycle` failure. Phase 1: verify state-sync fix is sufficient. Phase 2: investigate snapshot mechanism if needed. Phase 3: flake-fix with polling if needed.
|
||||
|
||||
**Architecture:** Sequential investigation. Cheapest fix first.
|
||||
|
||||
**Tech Stack:** Python 3.11+, pytest 9.0.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Change | Purpose |
|
||||
|---|---|---|
|
||||
| (Phase 1) None | | |
|
||||
| (Phase 2) `src/history.py`, `src/gui_2.py`, `tests/test_undo_redo_ai_input_snapshot.py` | Possibly modify | Fix snapshot if it doesn't include ai_input |
|
||||
| (Phase 3) `tests/test_undo_redo_sim.py` | Possibly modify | Replace time.sleep with polling |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Phase 1 — Run the test, see if it passes after the state-sync fix
|
||||
|
||||
**Files:** (no changes; verification)
|
||||
|
||||
- [ ] **Step 1.1: Run the test in isolation**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_undo_redo_sim.py::test_undo_redo_lifecycle -v --timeout=60
|
||||
```
|
||||
|
||||
Expected outcomes:
|
||||
- **A) PASSES** → Done. The state-sync fix is sufficient. Skip to Task 4 (documentation).
|
||||
- **B) FAILS** → Proceed to Task 2 (Phase 2: investigate snapshot).
|
||||
|
||||
- [ ] **Step 1.2: Document the outcome**
|
||||
|
||||
If passes: commit a doc-only note confirming state-sync fixed it.
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git -c core.autocrlf=false commit --allow-empty -m "verify: undo_redo_lifecycle passes after state-sync fix"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Confirmed: the ui_ai_input property delegation in live_gui_state_sync_20260605 fixes test_undo_redo_lifecycle. The snapshot reads app.ui_ai_input (now delegated to controller.ui_ai_input where the value lives) and captures the right value. Undo restores correctly." $h
|
||||
```
|
||||
|
||||
If fails: proceed to Task 2.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Phase 2 — Check the snapshot mechanism for `ai_input`
|
||||
|
||||
**Files:** (read-only; possibly modify later)
|
||||
|
||||
- [ ] **Step 2.1: Read `UISnapshot` definition**
|
||||
|
||||
Read `src/history.py` to find the `UISnapshot` dataclass. List its fields.
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run python -c "
|
||||
import re
|
||||
with open('src/history.py', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
m = re.search(r'class UISnapshot', content)
|
||||
if m:
|
||||
print(content[m.start():m.start()+500])
|
||||
"
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Check if `ai_input` is a field**
|
||||
|
||||
- **A) `ai_input` is a field** → Task 3: check `_apply_snapshot` for restore line.
|
||||
- **B) `ai_input` is NOT a field** → Add it. See Step 2.3.
|
||||
|
||||
- [ ] **Step 2.3: If `ai_input` is missing from UISnapshot, add it**
|
||||
|
||||
Add `ai_input: str = ""` to the UISnapshot dataclass.
|
||||
|
||||
In `src/gui_2.py:_take_snapshot` (line 551), add `ai_input=self.ui_ai_input,`.
|
||||
|
||||
In `src/gui_2.py:_apply_snapshot` (line 569), add `self.ui_ai_input = snapshot.ai_input`.
|
||||
|
||||
Commit:
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add src/history.py src/gui_2.py
|
||||
git -C C:\projects\manual_slop commit -m "fix(gui_2): add ai_input to UISnapshot for undo/redo round-trip"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Add ai_input field to UISnapshot (src/history.py), capture in _take_snapshot, restore in _apply_snapshot. The undo/redo system was silently dropping ai_input changes; this fixes test_undo_redo_lifecycle." $h
|
||||
```
|
||||
|
||||
- [ ] **Step 2.4: Run the test**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_undo_redo_sim.py::test_undo_redo_lifecycle -v --timeout=60
|
||||
```
|
||||
|
||||
Expected: 1 passed.
|
||||
|
||||
If still fails → proceed to Task 3 (Phase 3: flake-fix with polling).
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Phase 3 — Test-ordering / flake investigation
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_undo_redo_sim.py` (replace time.sleep with polling)
|
||||
|
||||
- [ ] **Step 3.1: Add the polling helpers (or import from wait_for_ready track)**
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def wait_for_value(client, item, expected, timeout=5.0):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if client.get_value(item) == expected:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"Item '{item}' did not become {expected!r} within {timeout}s")
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Replace the time.sleep calls**
|
||||
|
||||
- [ ] **Step 3.3: Run the test**
|
||||
|
||||
- [ ] **Step 3.4: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add tests/test_undo_redo_sim.py
|
||||
git -C C:\projects\manual_slop commit -m "test(undo_redo): replace time.sleep with wait_for_value polling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Update tracks.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/tracks.md`
|
||||
|
||||
- [ ] **Step 4.1: Add a note about the outcome**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add conductor/tracks.md
|
||||
git -C C:\projects\manual_slop commit -m "conductor: undo_redo_lifecycle sub-track complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** 3-phase sequential investigation. State-sync fix may resolve it (Phase 1). If not, snapshot investigation (Phase 2). If not, flake-fix (Phase 3).
|
||||
- **Placeholders:** None.
|
||||
- **Type consistency:** `ai_input: str` matches the existing type.
|
||||
- **Risk:** Low — only investigation + minimal source change.
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Inline execution. Up to 4 tasks; some may be skipped depending on the outcome of Phase 1.
|
||||
@@ -1,191 +0,0 @@
|
||||
# wait_for_ready_test_pattern_20260605 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:** Replace `time.sleep(N)` in `test_workspace_profiles_sim.py` and `test_auto_switch_sim.py` with polling helpers that wait for the operation to complete. Tests should pass consistently across machines.
|
||||
|
||||
**Architecture:** Inline polling helpers (or extracted to `tests/helpers.py` if 3+ tests need them). 100ms poll interval, 5s default timeout.
|
||||
|
||||
**Tech Stack:** Python 3.11+, pytest 9.0, time-based polling.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Change | Purpose |
|
||||
|---|---|---|
|
||||
| `tests/test_workspace_profiles_sim.py` | Modify | Replace time.sleep with polling |
|
||||
| `tests/test_auto_switch_sim.py` | Modify | Replace time.sleep with polling |
|
||||
|
||||
No production code changes. No new shared module (helpers are inlined for now).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Migrate `test_workspace_profiles_sim.py`
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_workspace_profiles_sim.py`
|
||||
|
||||
- [ ] **Step 1.1: Pre-edit checkpoint**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git status --short
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Read the test**
|
||||
|
||||
Read `tests/test_workspace_profiles_sim.py` to see the current `time.sleep` calls.
|
||||
|
||||
- [ ] **Step 1.3: Add the polling helpers at the top of the file**
|
||||
|
||||
After the existing imports, add:
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def wait_for_save_completion(client, profile_name, timeout=5.0):
|
||||
"""Poll until the saved profile appears in the workspace profiles."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
profiles = client.get_value('workspace_profiles') or {}
|
||||
if profile_name in profiles:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"Profile '{profile_name}' did not appear in workspace_profiles within {timeout}s")
|
||||
|
||||
def wait_for_load_completion(client, item, expected, timeout=5.0):
|
||||
"""Poll until the item's value matches expected."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if client.get_value(item) == expected:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"Item '{item}' did not become {expected!r} within {timeout}s")
|
||||
```
|
||||
|
||||
Use exactly 1-space indentation. No comments.
|
||||
|
||||
- [ ] **Step 1.4: Replace the `time.sleep` calls**
|
||||
|
||||
In the test body, replace:
|
||||
- `time.sleep(2.0)` after `save_workspace_profile` → `wait_for_save_completion(client, "test_restore")`
|
||||
- `time.sleep(2.0)` after `load_workspace_profile` → `wait_for_load_completion(client, 'ui_separate_tier1', True)`
|
||||
- The other `time.sleep(1.0)` calls after `set_value` can stay (set_value is synchronous in the controller) OR be replaced with `wait_for_load_completion` for consistency.
|
||||
|
||||
**Recommended:** keep the `set_value` sleeps for now (set_value writes to controller synchronously; the sleep is for the GUI to process the change), but replace the save/load ones.
|
||||
|
||||
- [ ] **Step 1.5: Run the test**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_workspace_profiles_sim.py -v --timeout=30
|
||||
```
|
||||
|
||||
Expected: 1 passed.
|
||||
|
||||
- [ ] **Step 1.6: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add tests/test_workspace_profiles_sim.py
|
||||
git -C C:\projects\manual_slop commit -m "test(workspace_profiles): replace time.sleep with wait_for_X polling helpers"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Replaced time.sleep(2.0) with wait_for_save_completion and wait_for_load_completion polling helpers. 100ms poll interval, 5s default timeout. Per the Authoring Robust live_gui Tests rules in docs/guide_testing.md: use wait-for-ready pattern, not fixed sleeps." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Migrate `test_auto_switch_sim.py`
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_auto_switch_sim.py`
|
||||
|
||||
- [ ] **Step 2.1: Read the test**
|
||||
|
||||
Read `tests/test_auto_switch_sim.py` to see the current `time.sleep` calls.
|
||||
|
||||
- [ ] **Step 2.2: Add the polling helpers at the top of the file**
|
||||
|
||||
Same as Task 1 Step 1.3 (or import from a shared location if extracted in the future).
|
||||
|
||||
- [ ] **Step 2.3: Replace the `time.sleep(1)` calls after each `trigger_tier(...)` call**
|
||||
|
||||
The test triggers a tier-2 then tier-3 transition. After each trigger, wait for `show_windows['Diagnostics']` to reach the expected value:
|
||||
|
||||
```python
|
||||
trigger_tier('Tier 2 (Tech Lead)')
|
||||
wait_for_load_completion(client, 'show_windows', {'Diagnostics': False})
|
||||
assert client.get_value('show_windows').get('Diagnostics', False) == False
|
||||
|
||||
trigger_tier('Tier 3 (Worker): task-1')
|
||||
wait_for_load_completion(client, 'show_windows', {'Diagnostics': True})
|
||||
assert client.get_value('show_windows').get('Diagnostics', False) == True
|
||||
```
|
||||
|
||||
- [ ] **Step 2.4: Run the test**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_auto_switch_sim.py -v --timeout=60
|
||||
```
|
||||
|
||||
Expected: 1 passed.
|
||||
|
||||
- [ ] **Step 2.5: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add tests/test_auto_switch_sim.py
|
||||
git -C C:\projects\manual_slop commit -m "test(auto_switch): replace time.sleep with wait_for_load_completion polling"
|
||||
$h = git -C C:\projects\manual_slop log -1 --format='%H'
|
||||
git -C C:\projects\manual_slop notes add -m "Replaced time.sleep(1) after each trigger_tier with wait_for_load_completion. The auto-switch applies a workspace profile; the test now polls until the expected show_windows state is observed." $h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Verify both tests pass in the full batched suite
|
||||
|
||||
**Files:** (no file changes; verification only)
|
||||
|
||||
- [ ] **Step 3.1: Run both tests**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; uv run pytest tests/test_workspace_profiles_sim.py tests/test_auto_switch_sim.py -v --timeout=60
|
||||
```
|
||||
|
||||
Expected: 2 passed.
|
||||
|
||||
- [ ] **Step 3.2: Commit (no-op)**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git -c core.autocrlf=false commit --allow-empty -m "verify: wait_for_ready migration unblocks 2 tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Update tracks.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `conductor/tracks.md`
|
||||
|
||||
- [ ] **Step 4.1: Add a brief note**
|
||||
|
||||
Find the live_gui_test_hardening_v2 entry and add: "Sub-track `wait_for_ready_test_pattern_20260605` complete: time.sleep replaced with polling helpers in test_workspace_profiles_sim and test_auto_switch_sim."
|
||||
|
||||
- [ ] **Step 4.2: Commit**
|
||||
|
||||
```powershell
|
||||
cd C:\projects\manual_slop; git add conductor/tracks.md
|
||||
git -C C:\projects\manual_slop commit -m "conductor: wait_for_ready_test_pattern sub-track complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** 2 tests migrated; polling helpers defined; fixed sleeps replaced.
|
||||
- **Placeholders:** None.
|
||||
- **Type consistency:** Polling helpers return None on success, raise TimeoutError on failure. Test assertions unchanged.
|
||||
- **Risk:** Low — only test files change.
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Inline execution. 4 tasks, atomic commits. User runs the full batched suite to confirm.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,253 +0,0 @@
|
||||
# Context Composition Redesign Specification
|
||||
|
||||
## Overview
|
||||
|
||||
Redesign Context Composition in Manual Slop to support sophisticated, layered context management for MMA epics and 1:1 agent discussions. The goal is explicit user control over what context an agent receives at varying granularities.
|
||||
|
||||
## Current State
|
||||
|
||||
- Files & Media and Context Composition are coupled (flags sync visually)
|
||||
- Context Composition inherits from Files & Media automatically
|
||||
- No view presets, limited slice visualization
|
||||
- No context preview before sending
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **Decouple** Files & Media from Context Composition
|
||||
2. **Context Composition** = curated selection from project whitelist with view modes
|
||||
3. **File View Presets** = named combinations of sig/def/full/custom for per-file selection
|
||||
4. **Directory grouping** for compact, scrollable file lists
|
||||
5. **Slice visualization** with annotation support
|
||||
6. **Context Presets** = save/load file+view+slices compositions
|
||||
7. **Context Preview** = show exactly what will be sent to agent
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Decoupling + File Stats + View Selection
|
||||
|
||||
### 1.1 Files & Media = Project Whitelist
|
||||
|
||||
**Purpose:** Define what files exist and belong to the project codebase.
|
||||
|
||||
**Behavior:**
|
||||
- Raw file list with wildcards (existing)
|
||||
- Does NOT affect Discussion Context directly
|
||||
- RAG indexing pulls from this list
|
||||
- Tool access pulls from this list (via tooling permissions)
|
||||
|
||||
**UI:**
|
||||
- File listing with path display
|
||||
- Add/remove files
|
||||
- Wildcard patterns for bulk add
|
||||
- Rescan project for new files
|
||||
|
||||
### 1.2 Context Composition = Curated Selection
|
||||
|
||||
**Purpose:** Select which files to include in THIS discussion/epic and HOW.
|
||||
|
||||
**Behavior:**
|
||||
- Independent from Files & Media (decoupled)
|
||||
- User manually adds files FROM the project whitelist
|
||||
- Or user adds ALL from whitelist then removes unwanted
|
||||
- Each file has view mode and optional custom slices
|
||||
|
||||
**View Modes per file:**
|
||||
- `full` - raw text (no AST pruning)
|
||||
- `sig` - signature-only outline (basic)
|
||||
- `def` - definition outline (includes forwards, full AST)
|
||||
- `custom` - sig/def + user-defined slices with annotations
|
||||
|
||||
**UI Structure:**
|
||||
```
|
||||
[Preset Selector: ▼ Default ▼] [+ New] [💾 Save] [🗑 Delete]
|
||||
|
||||
[Search/Filter files...]
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📁 base/auxiliary/
|
||||
☑ builder.cpp [View: def ▼] [Inspect] [Slices]
|
||||
☑ allocator.cpp [View: sig ▼] [Inspect] [Slices]
|
||||
📁 core/
|
||||
☑ engine.h [View: full ▼] [Inspect] [Slices]
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
[Stats Panel]
|
||||
Files: 3 | Lines: 1,247 | AST Elements: 89
|
||||
```
|
||||
|
||||
### 1.3 Directory Grouping
|
||||
|
||||
**Compact display:**
|
||||
- Files grouped by common relative directory prefix
|
||||
- Directory header collapsible
|
||||
- Reduces scrolling, eliminates horizontal scroll
|
||||
- Format: `📁 relative/path/`
|
||||
|
||||
### 1.4 File Stats
|
||||
|
||||
**Per-file stats display:**
|
||||
- Line count
|
||||
- AST element count (functions, classes, structs, etc.)
|
||||
- View mode indicator
|
||||
|
||||
**Aggregate stats for selection:**
|
||||
- Total files, total lines, total AST elements
|
||||
- Helps user decide: "should I just inject full text or curate?"
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Slice Visualization + Annotations
|
||||
|
||||
### 2.1 Slice Inspector (replaces Inspect button)
|
||||
|
||||
**Purpose:** Visualize and edit slices for a file.
|
||||
|
||||
**Opens as popup/modal showing:**
|
||||
- Full file content with line numbers
|
||||
- AST-derived slices highlighted (sig elements, def elements)
|
||||
- User custom slices with annotations
|
||||
- Line range indicators for each slice
|
||||
|
||||
**Slice types:**
|
||||
- `sig` - AST signature elements
|
||||
- `def` - AST definition elements
|
||||
- `custom` - user-defined line ranges with optional tag/comment
|
||||
|
||||
**Annotation support:**
|
||||
- Each custom slice can have:
|
||||
- `tag` - category label (e.g., "performance", "bug", "api")
|
||||
- `comment` - free-text explanation for agent
|
||||
|
||||
### 2.2 View Presets
|
||||
|
||||
**Named combinations of view settings:**
|
||||
- Preset defines default view mode + default slices for a file type
|
||||
- Example: "Debug View" = full text + custom slice at error-prone lines
|
||||
- Example: "API Surface" = sig + custom slices for public API functions
|
||||
|
||||
**Behavior:**
|
||||
- Select preset for a file → auto-fills view mode + common slices
|
||||
- User can override after selecting preset
|
||||
- Presets are project-scoped (saved in project config)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Context Presets
|
||||
|
||||
### 3.1 Save/Load Context Presets
|
||||
|
||||
**Context Preset contains:**
|
||||
- List of files with:
|
||||
- Relative path
|
||||
- View mode (full/sig/def/custom)
|
||||
- Custom slices with line ranges, tags, comments
|
||||
- Preset name
|
||||
- Preset description (optional)
|
||||
|
||||
**Save behavior:**
|
||||
- Validate all files exist in project
|
||||
- If file missing: warn user, offer to:
|
||||
- Save without missing files
|
||||
- Cancel and resolve manually
|
||||
- Save to project config (project_context_presets.toml)
|
||||
|
||||
**Load behavior:**
|
||||
- On preset select, Context Composition populates with saved state
|
||||
- If file from preset is missing: highlight in red, warn
|
||||
- User can remove missing or attempt to re-path
|
||||
|
||||
### 3.2 Context Preview
|
||||
|
||||
**Before sending to agent:**
|
||||
- Show exactly what will be composed
|
||||
- For each file, show:
|
||||
- Which view mode applied
|
||||
- Line ranges included
|
||||
- Tags/comments visible to agent
|
||||
- Total token estimate
|
||||
|
||||
**Preview modes:**
|
||||
- Collapsed (just file list + view modes)
|
||||
- Expanded (show actual text/slices that will be sent)
|
||||
|
||||
---
|
||||
|
||||
## Data Models
|
||||
|
||||
### FileViewPreset
|
||||
```python
|
||||
@dataclass
|
||||
class FileViewPreset:
|
||||
name: str
|
||||
description: str
|
||||
view_mode: str # "full" | "sig" | "def" | "custom"
|
||||
default_slices: list[dict] # [{start_line, end_line, tag, comment}]
|
||||
```
|
||||
|
||||
### ContextPreset
|
||||
```python
|
||||
@dataclass
|
||||
class ContextPreset:
|
||||
name: str
|
||||
description: str
|
||||
files: list[ContextFileEntry]
|
||||
|
||||
@dataclass
|
||||
class ContextFileEntry:
|
||||
relative_path: str # relative to project root
|
||||
view_mode: str
|
||||
custom_slices: list[dict] # [{start_line, end_line, tag, comment}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UX Flows
|
||||
|
||||
### Adding Files to Context Composition
|
||||
|
||||
1. User clicks "Add Files" in Context Composition
|
||||
2. File picker shows project whitelist files, grouped by directory
|
||||
3. User selects files (multi-select supported)
|
||||
4. Files added with default view mode (configurable)
|
||||
|
||||
### Creating Custom Slice
|
||||
|
||||
1. User clicks [Slices] on a file
|
||||
2. Slice editor opens showing file content + AST slices
|
||||
3. User selects line range via click-drag or input
|
||||
4. User adds tag and/or comment
|
||||
5. Slice saved to file entry in Context Composition
|
||||
|
||||
### Saving Context Preset
|
||||
|
||||
1. User curates Context Composition
|
||||
2. Clicks [💾 Save]
|
||||
3. Dialog: enter preset name + optional description
|
||||
4. Validation runs - warns if files missing
|
||||
5. Preset saved to project
|
||||
|
||||
### Loading Context Preset
|
||||
|
||||
1. User selects preset from dropdown
|
||||
2. Context Composition cleared
|
||||
3. Files populated from preset
|
||||
4. Missing files highlighted with warning
|
||||
5. User resolves or proceeds
|
||||
|
||||
---
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- All file paths in presets stored as relative to project root
|
||||
- FileItem model extended with view_mode, custom_slices
|
||||
- Context Composition panel completely rewired to be independent of Files & Media
|
||||
- Aggregate functions updated to respect view modes and slices
|
||||
- Tool access still uses Files & Media whitelist
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- RAG configuration changes (handled separately)
|
||||
- Tool preset changes (already exists)
|
||||
- MMA track creation (handled by track system)
|
||||
- Agent behavior when given incomplete views (assumes tooling access)
|
||||
@@ -1,96 +0,0 @@
|
||||
# ImGui Context Manager Suite Design
|
||||
|
||||
## Status
|
||||
|
||||
Proposed — 2026-05-11
|
||||
|
||||
## Context
|
||||
|
||||
The `src/gui_2.py` file contains ~50 `imgui.begin()` / `imgui.end()` pairs scattered across 5900+ lines. Dear PyGui (and ImGui) uses a procedural API where every `begin_X()` requires a matching `end_X()`. This creates two problems:
|
||||
|
||||
1. **Error-prone for AI agents** — forgetting an `end()` call causes UI nesting bugs that are hard to trace
|
||||
2. **Visual sprawl** — the begin/end pairing is separated by potentially hundreds of lines, making scope boundaries unclear
|
||||
|
||||
## Decision
|
||||
|
||||
Implement a context manager suite that pairs `begin` and `end` calls on the same line using Python's `with` statement.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Core Base Class
|
||||
|
||||
```python
|
||||
class ImGuiScope:
|
||||
def __init__(self, begin_fn, end_fn, *args, **kwargs):
|
||||
self._begin_fn = begin_fn
|
||||
self._end_fn = end_fn
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
self._opened = None
|
||||
self._entered = False
|
||||
|
||||
def __enter__(self):
|
||||
result = self._begin_fn(*self._args, **self._kwargs)
|
||||
if isinstance(result, tuple):
|
||||
self._opened = result[0]
|
||||
else:
|
||||
self._opened = result
|
||||
self._entered = bool(self._opened)
|
||||
return self._opened
|
||||
|
||||
def __exit__(self, *args):
|
||||
if self._entered:
|
||||
self._end_fn()
|
||||
return False
|
||||
```
|
||||
|
||||
### Scope Helpers
|
||||
|
||||
| Function | Wraps | Signature |
|
||||
|----------|-------|-----------|
|
||||
| `imgui_window(name, visible=True, flags=0)` | `imgui.begin` | `__enter__` returns `opened` bool |
|
||||
| `imgui_table(name, columns, flags=0)` | `imgui.begin_table` | `__enter__` returns `opened` bool |
|
||||
| `imgui_menu_bar()` | `imgui.begin_menu_bar` | No args |
|
||||
| `imgui_menu(label)` | `imgui.begin_menu` | `__enter__` returns `opened` bool |
|
||||
| `imgui_child(id, width=0, height=0, flags=0)` | `imgui.begin_child` | `__enter__` returns `opened` bool |
|
||||
| `imgui_group()` | `imgui.begin_group` | No args |
|
||||
| `imgui_popup(id)` | `imgui.begin_popup` | `__enter__` returns `opened` bool |
|
||||
| `imgui_tooltip()` | `imgui.begin_tooltip` | No args |
|
||||
| `imgui_clipper(count)` | `clipper.begin` | `__enter__` returns `opened` count |
|
||||
| `node_editor_scope(name)` | `ed.begin` | `__enter__` returns `opened` bool |
|
||||
|
||||
### Usage Pattern
|
||||
|
||||
```python
|
||||
# Before
|
||||
exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"])
|
||||
if exp:
|
||||
imgui.text("Settings")
|
||||
imgui.separator()
|
||||
# ... 50 more lines ...
|
||||
imgui.end()
|
||||
|
||||
# After
|
||||
if imgui_window("AI Settings", self.show_windows["AI Settings"], flags):
|
||||
imgui.text("Settings")
|
||||
imgui.separator()
|
||||
# ... 50 more lines ...
|
||||
# imgui.end() called automatically
|
||||
```
|
||||
|
||||
## File Location
|
||||
|
||||
`src/imgui_scopes.py`
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. New code MUST use context managers
|
||||
2. Existing code migrates incrementally during bug fixes and feature work
|
||||
3. No强制性 — old `begin()`/`end()` calls remain valid
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Reduced** scope pairing errors
|
||||
- **Improved** code legibility for AI agents
|
||||
- **Slight** line count reduction (one `imgui.end()` line saved per scope)
|
||||
- **No** runtime performance impact (imperceptible nanoseconds vs C++ binding calls)
|
||||
@@ -1,101 +0,0 @@
|
||||
# ai_client.py Style Convention Curation
|
||||
|
||||
## Overview
|
||||
|
||||
Refactor `src/ai_client.py` (2522 lines) to align with `conductor/code_styleguides/python.md` conventions, following the pattern established in the recent `gui_2.py` refactor.
|
||||
|
||||
## Current State Audit (as of UNCOMMITTED)
|
||||
|
||||
### Already Implemented
|
||||
- 1-space indentation throughout
|
||||
- Type annotations on all functions
|
||||
- Thread-local helpers (`get_current_tier`, `set_current_tier`) as module-level functions
|
||||
- SDM tags on docstrings
|
||||
|
||||
### Gaps to Fill
|
||||
- `ProviderError` defined inside module (should be at module level per convention)
|
||||
- No `#region` blocks despite 2522-line size (violates python.md Section 14)
|
||||
- No logical section organization for 80+ functions
|
||||
- Error classifier functions (`_classify_*_error`) could use vertical alignment
|
||||
|
||||
## Goals
|
||||
|
||||
1. Move `ProviderError` to module level (proper exception class, justified)
|
||||
2. Add `#region` blocks to organize functions into logical sections
|
||||
3. Apply vertical compaction alignment to dense conditionals
|
||||
4. Preserve all call sites and SDM tags
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Module-Level `ProviderError`
|
||||
|
||||
Move from inline class definition to module level, before top-level functions:
|
||||
|
||||
```python
|
||||
class ProviderError(Exception):
|
||||
def __init__(self, provider: str, message: str, code: str | None = None):
|
||||
self.provider = provider
|
||||
self.message = message
|
||||
self.code = code
|
||||
|
||||
def ui_message(self) -> str:
|
||||
return f"[{self.provider.upper()}] {self.message}"
|
||||
```
|
||||
|
||||
Location: After imports, before first function (~line 300).
|
||||
|
||||
### 2. Region Block Organization
|
||||
|
||||
Add `#region: Section Name` / `#endregion: Section Name` blocks:
|
||||
|
||||
| Region | Functions |
|
||||
|--------|-----------|
|
||||
| `#region: Provider Configuration` | `set_provider`, `get_provider`, `set_model_params`, `list_models`, `_list_gemini_cli_models`, `_list_gemini_models`, `_list_anthropic_models`, `_list_deepseek_models`, `_list_minimax_models` |
|
||||
| `#region: Credentials & Setup` | `_get_proxy`, `get_credentials_path`, `_load_credentials`, `cleanup` |
|
||||
| `#region: System Prompt Management` | `set_custom_system_prompt`, `set_base_system_prompt`, `set_use_default_base_prompt`, `set_project_context_marker`, `_get_context_marker`, `_get_combined_system_prompt`, `get_combined_system_prompt` |
|
||||
| `#region: Comms Log` | `get_comms_log_callback`, `set_comms_log_callback`, `_append_comms`, `get_comms_log`, `clear_comms_log` |
|
||||
| `#region: Error Classification` | `_classify_anthropic_error`, `_classify_gemini_error`, `_classify_deepseek_error`, `_classify_minimax_error` |
|
||||
| `#region: Tool Configuration` | `set_agent_tools`, `set_tool_preset`, `set_bias_profile`, `get_bias_profile`, `_build_anthropic_tools`, `_get_anthropic_tools`, `_gemini_tool_declaration`, `_build_deepseek_tools`, `_get_deepseek_tools`, `_content_block_to_dict` |
|
||||
| `#region: Tool Execution` | `_execute_tool_calls_concurrently`, `_execute_single_tool_call_async`, `_run_script`, `_truncate_tool_output` |
|
||||
| `#region: File Context Building` | `_reread_file_items`, `_build_file_context_text`, `_build_file_diff_text` |
|
||||
| `#region: Token Estimation` | `_estimate_message_tokens`, `_invalidate_token_estimate`, `_estimate_prompt_tokens`, `_strip_stale_file_refreshes` |
|
||||
| `#region: History Management` | `_trim_anthropic_history`, `_repair_anthropic_history`, `_repair_deepseek_history`, `_add_history_cache_breakpoint`, `_strip_cache_controls` |
|
||||
| `#region: Gemini Provider` | `_ensure_gemini_client`, `_get_gemini_history_list`, `_send_gemini`, `_send_gemini_cli`, `get_gemini_cache_stats` |
|
||||
| `#region: Anthropic Provider` | `_ensure_anthropic_client`, `_chunk_text`, `_build_chunked_context_blocks`, `_send_anthropic` |
|
||||
| `#region: DeepSeek Provider` | `_ensure_deepseek_client`, `_send_deepseek` |
|
||||
| `#region: MiniMax Provider` | `_ensure_minimax_client`, `_send_minimax` |
|
||||
| `#region: Tier 4 Analysis` | `run_tier4_analysis`, `run_tier4_patch_callback`, `run_tier4_patch_generation` |
|
||||
| `#region: Session & Public API` | `reset_session`, `get_token_stats`, `send`, `_add_bleed_derived`, `run_subagent_summarization` |
|
||||
|
||||
### 3. Vertical Compaction
|
||||
|
||||
Apply alignment to error classifier functions:
|
||||
|
||||
```python
|
||||
# Before
|
||||
if status == 'running': col = (0.0, 1.0, 0.0, 1.0)
|
||||
elif status == 'starting': col = (1.0, 1.0, 0.0, 1.0)
|
||||
elif status == 'error': col = (1.0, 0.0, 0.0, 1.0)
|
||||
|
||||
# Apply to _classify_*_error functions with similar patterns
|
||||
```
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- No logic changes — purely organizational
|
||||
- All existing `[C: ...]` SDM tags preserved
|
||||
- All call sites continue to work (50+ references)
|
||||
- 1-space indentation maintained
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Extraction of class methods to module-level functions
|
||||
- Indentation changes
|
||||
- Logic modifications
|
||||
- Test changes (unless regression detected)
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
- `conductor/code_styleguides/python.md` — Style conventions
|
||||
- `src/gui_2.py` — Reference for region block pattern (5000+ line refactor)
|
||||
- `docs/guide_architecture.md` — AI client threading model context
|
||||
@@ -1,110 +0,0 @@
|
||||
# AI Server IPC Design
|
||||
|
||||
## Overview
|
||||
|
||||
Decouple heavy AI SDK imports (google.genai, anthropic) from the GUI process via a subprocess command queue. GUI starts instantly (~0.5s) while AI server loads in background (~1.2s one-time cost).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
GUI Process AI Server Process
|
||||
+-----------+ +------------------+
|
||||
| Command |----pipe/json--->| AI processing |
|
||||
| Queue | | (google.genai, |
|
||||
+-----------+ | anthropic) |
|
||||
| Response |<---pipe/json-----|------------------+
|
||||
| Queue | | |
|
||||
+-----------+ +------------------+
|
||||
```
|
||||
|
||||
## Command Queue (Input to AI Server)
|
||||
|
||||
Format: JSON lines on stdin
|
||||
```json
|
||||
{"id": "uuid", "method": "send", "params": {...}}
|
||||
{"id": "uuid", "method": "list_models", "params": {}}
|
||||
```
|
||||
|
||||
## Response Queue (Output from AI Server)
|
||||
|
||||
Format: JSON lines on stdout
|
||||
```json
|
||||
{"id": "uuid", "result": {...}}
|
||||
{"id": "uuid", "error": "message"}
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Method | Params | Description |
|
||||
|--------|--------|-------------|
|
||||
| `send` | `{history, model, provider, tools}` | Send AI request |
|
||||
| `list_models` | `{provider}` | List available models |
|
||||
| `cleanup` | `{}` | Cleanup sessions |
|
||||
| `reset_session` | `{}` | Reset conversation history |
|
||||
| `set_provider` | `{provider, model}` | Switch provider |
|
||||
| `set_credentials` | `{creds}` | Set API credentials |
|
||||
|
||||
## AI Server Lifecycle
|
||||
|
||||
1. **Spawn**: `subprocess.Popen(["python", "-m", "src.ai_server"])`
|
||||
2. **Startup**: Load google.genai, anthropic SDKs (~1.2s)
|
||||
3. **Ready**: Send `{"type": "ready"}` to GUI
|
||||
4. **Process**: Read commands, write responses
|
||||
5. **Shutdown**: On GUI exit or disconnect
|
||||
|
||||
## GUI Response
|
||||
|
||||
- **Immediate return** from command queue operations
|
||||
- **Background thread reads** response queue
|
||||
- **No polling** - blocking read with timeout
|
||||
- **Lock-free** queue operations
|
||||
|
||||
## Status Indicator
|
||||
|
||||
GUI tracks AI server state:
|
||||
- `init` - Server starting
|
||||
- `ready` - Server loaded, accepting requests
|
||||
- `busy` - Processing request
|
||||
- `error` - Server error state
|
||||
|
||||
Panels that need AI show "Initializing..." tint when `status != ready`.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Files
|
||||
|
||||
- `src/ai_server.py` - Subprocess AI server (new)
|
||||
- `src/ai_client_proxy.py` - Queue client for GUI (new)
|
||||
- Modify `src/ai_client.py` - Route via proxy when AI server enabled
|
||||
|
||||
### ai_server.py
|
||||
|
||||
```
|
||||
- stdin reader loop
|
||||
- Command dispatcher
|
||||
- Provider wrappers (google.genai, anthropic)
|
||||
- stdout writer
|
||||
```
|
||||
|
||||
### ai_client_proxy.py
|
||||
|
||||
```
|
||||
- Command queue (subprocess.stdin)
|
||||
- Response queue (subprocess.stdout reader thread)
|
||||
- Request/response matching by ID
|
||||
- Timeout handling
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Server crash**: GUI detects via broken pipe, auto-restart server
|
||||
- **Timeout**: Requests timeout after 60s, return error
|
||||
- **Queue full**: Backpressure, return busy status
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
1. GUI starts, shows immediate (~0.5s)
|
||||
2. Spawn ai_server subprocess
|
||||
3. Server loads SDKs (~1.2s)
|
||||
4. Server sends `{"type": "ready"}`
|
||||
5. GUI enables AI panels
|
||||
@@ -1,192 +0,0 @@
|
||||
# Hot Reloader Design Spec
|
||||
|
||||
**Date:** 2026-05-14
|
||||
**Author:** Tier 2 Tech Lead
|
||||
**Status:** Draft
|
||||
|
||||
## Overview
|
||||
|
||||
Implement a selective, state-preserving hot-reload system for the Manual Slop `./src` Python codebase. This follows the "data in stable memory, code in reloadable modules" pattern pioneered by Casey's Handmade Hero hot-reload in C.
|
||||
|
||||
## Goals
|
||||
|
||||
- Enable hot-reloading of `src/gui_2.py` and `src/app_controller.py` initially
|
||||
- Preserve App instance state across reloads (scroll position, form inputs, open windows)
|
||||
- Extensible architecture — future hot modules (e.g., `ai_client.py`) can be added via registry
|
||||
- Manual trigger only (Ctrl+Alt+R keyboard shortcut + GUI button)
|
||||
- Silent fallback on failure with visual error tint
|
||||
|
||||
## Architecture
|
||||
|
||||
### Delegation Pattern
|
||||
|
||||
Code is separated into **delegators** (thin wrappers in stable modules) and **delegation targets** (actual logic in reloadable modules).
|
||||
|
||||
```python
|
||||
# src/gui_2.py (reloadable)
|
||||
def render_main_interface(app: App) -> None:
|
||||
if app.perf_profiling_enabled:
|
||||
app.perf_monitor.start_component("_render_main_interface")
|
||||
app._render_window_if_open("Project Settings", app._render_project_settings_hub)
|
||||
# ... all render logic here
|
||||
|
||||
# src/app_controller.py (stable) — App class stays stable
|
||||
class App:
|
||||
def _render_main_interface(self) -> None:
|
||||
import src.gui_2 as gui2
|
||||
gui2.render_main_interface(self)
|
||||
```
|
||||
|
||||
### HotModule Registry Entry
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class HotModule:
|
||||
name: str # "src.gui_2"
|
||||
file_path: str # Absolute path to .py
|
||||
state_keys: list[str] # App attrs to preserve during reload
|
||||
delegation_targets: list[str] # Method names on App that delegate here
|
||||
```
|
||||
|
||||
### HotReloader Core API
|
||||
|
||||
```python
|
||||
class HotReloader:
|
||||
HOT_MODULES: dict[str, HotModule] # Module registry
|
||||
|
||||
@classmethod
|
||||
def register(cls, module: HotModule) -> None: ...
|
||||
|
||||
@classmethod
|
||||
def reload(cls, module_name: str, app: App) -> bool:
|
||||
"""Reload a single module. Returns True on success, False on failure."""
|
||||
|
||||
@classmethod
|
||||
def reload_all(cls, app: App) -> bool:
|
||||
"""Reload all registered modules in dependency order."""
|
||||
|
||||
@classmethod
|
||||
def capture_state(cls, app: App, state_keys: list[str]) -> dict: ...
|
||||
|
||||
@classmethod
|
||||
def restore_state(cls, app: App, state: dict) -> None: ...
|
||||
```
|
||||
|
||||
### State Capture/Restore
|
||||
|
||||
Before reload, `HotReloader.capture_state()` serializes App attributes listed in `state_keys`. After reload (success or failure), `restore_state()` writes them back.
|
||||
|
||||
State is captured as a dict of `{attr_name: copy.deepcopy(value)}`. Restoration uses `setattr` with re-imported module reference.
|
||||
|
||||
### Error Handling Flow
|
||||
|
||||
```
|
||||
Reload triggered
|
||||
↓
|
||||
capture_state(app)
|
||||
↓
|
||||
attempt importlib.reload(module)
|
||||
↓
|
||||
on Exception:
|
||||
restore_state(app) # revert to pre-reload state
|
||||
cls.last_error = traceback
|
||||
cls.is_error_state = True
|
||||
tint GUI red
|
||||
return False
|
||||
↓
|
||||
on success:
|
||||
clear last_error
|
||||
clear error tint
|
||||
return True
|
||||
```
|
||||
|
||||
### Trigger Mechanism
|
||||
|
||||
- `Ctrl+Alt+R` keyboard shortcut captured in main input loop
|
||||
- GUI button in MMA Dashboard: "Hot Reload" with icon
|
||||
- Both call `HotReloader.reload_all(self)` on the App instance
|
||||
|
||||
### Visual Error Tint
|
||||
|
||||
When `HotReloader.is_error_state` is True:
|
||||
- If NERV theme active: overlay with NERV red (rgba 255, 72, 64, alpha)
|
||||
- Else: overlay with red tint
|
||||
- Tint cleared on next successful reload
|
||||
|
||||
## Module Registry (Initial)
|
||||
|
||||
```python
|
||||
HOT_MODULES = {
|
||||
"src.gui_2": HotModule(
|
||||
name="src.gui_2",
|
||||
file_path=str(Path(__file__).parent / "gui_2.py"),
|
||||
state_keys=[
|
||||
"_active_discussion", "_disc_entries", "_disc_roles",
|
||||
"show_windows", "ui_discussion_split_h", "active_tickets",
|
||||
# ... more keys TBD during implementation
|
||||
],
|
||||
delegation_targets=[
|
||||
"_render_main_interface", "_render_discussion_hub",
|
||||
"_render_discussion_panel", "_render_discussion_selector",
|
||||
# ... more targets TBD during implementation
|
||||
],
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
## Delegation Refactoring Phases
|
||||
|
||||
### Phase 1: GUI Methods
|
||||
Extract render methods from `App` in `app_controller.py` into delegation targets in `gui_2.py`.
|
||||
|
||||
### Phase 2: State Keys Inventory
|
||||
Catalog all `App` instance attributes that need preservation.
|
||||
|
||||
### Phase 3: HotReloader Implementation
|
||||
Implement `src/hot_reloader.py` with capture/restore, registry, and error handling.
|
||||
|
||||
### Phase 4: Trigger Integration
|
||||
Add Ctrl+Alt+R handler and GUI button.
|
||||
|
||||
### Phase 5: Visual Tint
|
||||
Implement error tint overlay.
|
||||
|
||||
## Extensibility: Adding Future Hot Modules
|
||||
|
||||
```python
|
||||
# Add ai_client as hot module:
|
||||
HotReloader.register(HotModule(
|
||||
name="src.ai_client",
|
||||
file_path=str(Path(__file__).parent / "ai_client.py"),
|
||||
state_keys=["_pending_requests", "_api_key"],
|
||||
delegation_targets=["_send_request", "_stream_response"],
|
||||
))
|
||||
|
||||
# Or via decorator on the delegation target:
|
||||
@hot_module(state_keys=["_pending_requests"])
|
||||
def send_request(app: App) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
## Files Affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/hot_reloader.py` | New — HotReloader class |
|
||||
| `src/gui_2.py` | Refactor render methods to module-level functions |
|
||||
| `src/app_controller.py` | Refactor App methods to delegation wrappers |
|
||||
| `src/imgui_scopes.py` | Unchanged (still used by refactored code) |
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Pressing Ctrl+Alt+R reloads `src.gui_2` without losing App state
|
||||
2. GUI button triggers same reload
|
||||
3. Failed reload shows error tint, preserves last-good state
|
||||
4. New hot modules can be added via `HotReloader.register()` without modifying core
|
||||
5. No performance impact when not reloading (< 1ms overhead per frame)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Automatic file watching (manual trigger only per design)
|
||||
- Hot-reloading of C extensions or native code
|
||||
- Cross-platform reload support (Windows focus for now)
|
||||
@@ -1,274 +0,0 @@
|
||||
# Performance Profiling System Design Spec
|
||||
|
||||
**Date:** 2026-05-15
|
||||
**Author:** Tier 2 Tech Lead
|
||||
**Status:** Draft
|
||||
|
||||
## Overview
|
||||
|
||||
Implement a layered performance profiling system for Manual Slop's `./src` codebase:
|
||||
|
||||
- **Phase 1:** Enhanced Diagnostics Panel — sorted component timings, expandable detail rows
|
||||
- **Phase 2:** Tracy integration via `pytracy` — real-time flamegraph streaming to Tracy GUI
|
||||
- **Phase 3 (future):** Custom `sys.settrace` sampler + imgui flamegraph as fallback
|
||||
|
||||
## Goals
|
||||
|
||||
- Surface hot code paths with zero friction (always-on real-time highlighting)
|
||||
- Enable deep dive profiling on demand via industry-standard tools
|
||||
- Preserve existing PerformanceMonitor infrastructure
|
||||
- Self-contained — minimal external dependencies beyond Tracy
|
||||
|
||||
## Phase 1: Enhanced Diagnostics Panel
|
||||
|
||||
### What's Already There
|
||||
|
||||
The Diagnostics Panel (`_render_diagnostics_panel` in `gui_2.py`) already displays:
|
||||
- FPS, Frame Time, CPU %, Input Lag with live values
|
||||
- Optional per-metric graphs (toggle checkbox)
|
||||
- Detailed Component Timings table: Avg, Count, Max, Min per component
|
||||
- RED highlighting for components with avg > 10ms
|
||||
- Performance Graphs section with rolling history plots
|
||||
- Diagnostic Log table
|
||||
|
||||
### What's Missing (for D)
|
||||
|
||||
1. **No sorting** — components iterate in dict hash order, not worst-first
|
||||
2. **No expandability** — cannot click a row to see full stats breakdown
|
||||
|
||||
### Implementation
|
||||
|
||||
**Sort by worst avg:**
|
||||
```python
|
||||
sorted_components = sorted(
|
||||
[(k, v) for k, v in metrics.items() if k.startswith("time_") and k.endswith("_ms")],
|
||||
key=lambda x: metrics.get(f"{x[0]}_avg", x[1]),
|
||||
reverse=True
|
||||
)
|
||||
```
|
||||
|
||||
**Expandable rows:**
|
||||
```python
|
||||
for key, val in sorted_components:
|
||||
# Render collapsed row with summary
|
||||
expanded = imgui.tree_node(comp_name)
|
||||
if expanded:
|
||||
# Show: last_value, avg, count, max, min, peak frames, stddev
|
||||
imgui.text(f"Last: {val:.2f}ms")
|
||||
imgui.text(f"StdDev: {stddev:.2f}ms")
|
||||
imgui.text(f"Peak frame: {peak_val:.2f}ms at frame {peak_frame}")
|
||||
imgui.tree_pop()
|
||||
```
|
||||
|
||||
### Files Affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/gui_2.py` | `_render_diagnostics_panel` — sort + expandable rows |
|
||||
|
||||
### Success Criteria
|
||||
|
||||
1. Component timings table sorted by worst avg descending
|
||||
2. Click a row → expands to show last, stddev, peak info
|
||||
3. Existing red highlighting (>10ms) preserved
|
||||
4. No regression to other diagnostics panel features
|
||||
|
||||
## Phase 2: Tracy Integration
|
||||
|
||||
### Tracy Overview
|
||||
|
||||
Tracy is a real-time, nanosecond-resolution, frame-based profiler for game devs and high-performance applications. It streams profiling data to a dedicated GUI client over a TCP connection. Features:
|
||||
|
||||
- Live CPU profiling with call stacks
|
||||
- Memory profiling (allocations, leaks)
|
||||
- Lock contention visualization
|
||||
- Frame capture (good for your Dear PyGui render loop)
|
||||
- Very low overhead (~1-2%)
|
||||
|
||||
### pytracy Binding
|
||||
|
||||
`pytracy` is a Python binding on PyPI:
|
||||
```bash
|
||||
uv add pytracy
|
||||
```
|
||||
|
||||
Basic usage:
|
||||
```python
|
||||
import pytracy
|
||||
pytracy.setproctitle("manual_slop")
|
||||
|
||||
# Zone annotations (instrumentation)
|
||||
def long_running_function():
|
||||
pytracy.begin("my_zone")
|
||||
# ... work ...
|
||||
pytracy.end("my_zone")
|
||||
```
|
||||
|
||||
### Integration Points in Manual Slop
|
||||
|
||||
**1. Process naming:**
|
||||
```python
|
||||
# In App.__init__ or gui_2.py:
|
||||
pytracy.setproctitle("manual_slop")
|
||||
```
|
||||
|
||||
**2. Zone instrumentation for render components:**
|
||||
```python
|
||||
# In each _render_* method:
|
||||
with pytracy.ctx_zone(name="_render_discussion_panel"):
|
||||
# ... render logic ...
|
||||
```
|
||||
|
||||
**3. Memory tracking (optional):**
|
||||
```python
|
||||
pytracy.allocator_hook_enable()
|
||||
```
|
||||
|
||||
**4. Connection handling:**
|
||||
Tracy GUI must be running and listening before manual_slop starts. The app connects to `localhost:8086` by default (configurable).
|
||||
|
||||
### Tracy GUI
|
||||
|
||||
- Tracy has its own cross-platform UI (Windows/macOS/Linux)
|
||||
- Download from https://github.com/wolfpld/tracy/releases or build from source
|
||||
- Once connected, you get live flamegraphs, frame time charts, memory graphs
|
||||
- Can save trace files for later analysis
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
If Tracy is not running or `pytracy` import fails:
|
||||
- App continues normally — profiling is opt-in
|
||||
- Existing PerformanceMonitor keeps working
|
||||
- Log a warning on startup: "Tracy not connected — profiling unavailable"
|
||||
|
||||
### Implementation
|
||||
|
||||
```python
|
||||
# src/profiling/tracy_integration.py (new file)
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
_tracy_available = False
|
||||
_tracy = None
|
||||
|
||||
def init_tracy() -> bool:
|
||||
"""Try to initialize pytracy connection. Returns True on success."""
|
||||
global _tracy_available, _tracy
|
||||
try:
|
||||
import pytracy
|
||||
_tracy = pytracy
|
||||
pytracy.setproctitle("manual_slop")
|
||||
_tracy_available = True
|
||||
return True
|
||||
except Exception:
|
||||
_tracy_available = False
|
||||
return False
|
||||
|
||||
def is_tracy_available() -> bool:
|
||||
return _tracy_available
|
||||
|
||||
class TracyZone:
|
||||
"""Context manager for Tracy zones."""
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.active = False
|
||||
|
||||
def __enter__(self):
|
||||
if _tracy_available and _tracy:
|
||||
_tracy.enter(self.name)
|
||||
self.active = True
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
if self.active:
|
||||
_tracy.leave(self.name)
|
||||
return False
|
||||
|
||||
# Convenience decorator
|
||||
def tracy_zone(name: str):
|
||||
"""Decorator to wrap a function in a Tracy zone."""
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
with TracyZone(name):
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
```
|
||||
|
||||
### GUI Button for Tracy Status
|
||||
|
||||
Add to Diagnostics Panel:
|
||||
```python
|
||||
if imgui.button("Open Tracy GUI"):
|
||||
import subprocess
|
||||
subprocess.Popen(["tracy"]) # Or path to Tracy executable
|
||||
imgui.same_line()
|
||||
imgui.text(f"Tracy: {'Connected' if tracy_available else 'Not connected'}")
|
||||
```
|
||||
|
||||
### Files Affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/profiling/tracy_integration.py` | New — Tracy integration module |
|
||||
| `src/gui_2.py` | Add Tracy zone wrappers around render methods, button in Diagnostics |
|
||||
| `src/app_controller.py` | Optionally add Tracy init in startup |
|
||||
|
||||
### Success Criteria
|
||||
|
||||
1. `pytracy` is a declared dependency in pyproject.toml
|
||||
2. Tracy zones wrap every `_render_*` component in `gui_2.py`
|
||||
3. App starts without error if Tracy GUI is not running (graceful degradation)
|
||||
4. When Tracy GUI is running, live flamegraph appears for running app
|
||||
5. "Open Tracy GUI" button launches Tracy if installed
|
||||
|
||||
## Phase 3 (Future): Custom Sampler Fallback
|
||||
|
||||
Out of scope for initial spec. Would implement `sys.settrace` based sampler if:
|
||||
- Tracy is unavailable/unwanted
|
||||
- User wants self-contained flamegraph rendered in imgui directly
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Diagnostics Panel (gui_2.py)
|
||||
|
|
||||
├── PerformanceMonitor (component timings, always-on)
|
||||
| └── O(1) rolling averages, per-component ms tracking
|
||||
|
|
||||
└── Tracy Integration (profiling, on-demand)
|
||||
└── pytracy → Tracy GUI (live flamegraph + memory + locks)
|
||||
```
|
||||
|
||||
Both run independently. PerformanceMonitor gives per-frame glanceable data. Tracy gives deep dive on demand.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dependency | Purpose | Notes |
|
||||
|------------|---------|-------|
|
||||
| `pytracy` | Tracy Python binding | Phase 2 only, graceful degradation if unavailable |
|
||||
|
||||
## Files
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `src/profiling/tracy_integration.py` | Create — Tracy wrapper with graceful degradation |
|
||||
| `src/profiling/__init__.py` | Create — Package init |
|
||||
| `src/gui_2.py` | Modify — Sort + expand Diagnostics, wrap render zones |
|
||||
| `src/app_controller.py` | Optional — Tracy init in startup |
|
||||
| `pyproject.toml` | Modify — Add `pytracy` dependency |
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Tracy connection params** — default `localhost:8086`, should be configurable via `config.toml`?
|
||||
2. **Which render methods to zone** — All `_render_*` or only top-level ones?
|
||||
3. **Memory profiling** — Enable allocator hook by default or opt-in only?
|
||||
|
||||
## Success Criteria Summary
|
||||
|
||||
- [ ] Phase 1: Diagnostics panel sorts by worst avg, rows expandable
|
||||
- [ ] Phase 2: pytracy integrated with graceful degradation
|
||||
- [ ] Phase 2: Every `_render_*` method in gui_2.py has Tracy zone
|
||||
- [ ] Phase 2: Tracy GUI shows live flamegraph when connected
|
||||
- [ ] Phase 3: Future extension point clear for custom sampler
|
||||
@@ -1,150 +0,0 @@
|
||||
# OpenCode Agent Definition Fix - Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
Fix OpenCode agent definitions so subagents spawned via `@mention` (when using superpowers) properly enforce the conductor workflow. Currently subagents fail to follow 1-space indentation, TDD protocols, and delegation rules even when explicitly instructed.
|
||||
|
||||
## Current State Audit
|
||||
|
||||
### Files Involved
|
||||
- `.opencode/agents/tier3-worker.md` - Tier 3 Worker agent definition
|
||||
- `.opencode/agents/tier4-qa.md` - Tier 4 QA agent definition
|
||||
- `.opencode/agents/tier2-tech-lead.md` - Tier 2 Tech Lead agent definition
|
||||
- `.opencode/agents/tier1-orchestrator.md` - Tier 1 Orchestrator agent definition
|
||||
|
||||
### Problems Identified
|
||||
|
||||
1. **Wrong MCP Tool Naming**
|
||||
- Current: `discovered_tool_py_get_code_outline`, `discovered_tool_run_powershell`
|
||||
- Correct: `manual-slop_py_get_code_outline`, `manual-slop_run_powershell`
|
||||
- Result: Subagents cannot find/locate MCP tools
|
||||
|
||||
2. **Insufficient Code Style Enforcement**
|
||||
- "1-space indentation" mentioned but not CRITICAL
|
||||
- No explicit warning about native edit tool destroying indentation
|
||||
- Subagents default to 4-space on unknown files
|
||||
|
||||
3. **Missing Conductor Workflow Structure**
|
||||
- No pre-delegation checkpoint protocol (`git add .`)
|
||||
- No atomic commit per-task rules
|
||||
- No TDD Red→Green→Refactor phase enforcement
|
||||
- No mandatory task state tracking in plan.md
|
||||
|
||||
4. **Context Amnesia Not Addressed**
|
||||
- Subagents lose all workflow context between tasks
|
||||
- No reminder about stateless operation
|
||||
- No checkpoint/commit reminder system
|
||||
|
||||
5. **Tier 3 Worker Mode Incorrect**
|
||||
- `mode: subagent` may cause OpenCode to strip context
|
||||
- May need `mode: primary` for full workflow access
|
||||
|
||||
## Goals
|
||||
|
||||
1. Subagents spawned via `@mention` follow conductor workflow without deviation
|
||||
2. 1-space indentation enforced as CRITICAL for all Python code
|
||||
3. TDD phases (Red→Green→Refactor) enforced mandatorily
|
||||
4. MCP tool names correct and functional
|
||||
5. Pre-delegation checkpoints prevent work loss
|
||||
6. Atomic commits per task maintained
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### 1. Agent Definition Rewrite
|
||||
|
||||
All tier agent files (`.opencode/agents/*.md`) must include:
|
||||
|
||||
- **CRITICAL Indentation Block** at top:
|
||||
```
|
||||
## CRITICAL: 1-Space Indentation for Python
|
||||
|
||||
ALL Python code MUST use exactly 1 (ONE) space for indentation.
|
||||
VIOLATION: Using 4 spaces or tabs will corrupt the codebase.
|
||||
```
|
||||
|
||||
- **MCP Tool Mapping Table** (correct names):
|
||||
```
|
||||
| Native Tool | MCP Tool |
|
||||
|-------------|----------|
|
||||
| read | manual-slop_read_file |
|
||||
| edit | manual-slop_edit_file |
|
||||
| bash | manual-slop_run_powershell |
|
||||
| glob | manual-slop_search_files |
|
||||
```
|
||||
|
||||
- **Pre-Delegation Checkpoint Protocol**:
|
||||
```
|
||||
## Pre-Delegation Checkpoint (MANDATORY)
|
||||
|
||||
Before delegating ANY change:
|
||||
1. Run: git add .
|
||||
2. Reason: Prevents work loss if subagent fails
|
||||
```
|
||||
|
||||
- **TDD Phase Enforcement**:
|
||||
```
|
||||
## TDD Protocol
|
||||
|
||||
1. RED: Write failing test, confirm failure
|
||||
2. GREEN: Implement to pass, confirm pass
|
||||
3. REFACTOR: Optional, with passing tests
|
||||
NEVER skip phases.
|
||||
```
|
||||
|
||||
- **Atomic Commit Rules**:
|
||||
```
|
||||
## Commit Protocol
|
||||
|
||||
After each task: git add . && git commit
|
||||
Do NOT batch commits.
|
||||
```
|
||||
|
||||
### 2. Tier 3 Worker Specific Fixes
|
||||
|
||||
- Verify `mode: subagent` is correct for OpenCode's @mention system
|
||||
- Add WHERE/WHAT/HOW/SAFETY task structure reminder
|
||||
- Add BLOCKED protocol for unresolvable tasks
|
||||
- Include Context Amnesia reminder (fresh context each task)
|
||||
|
||||
### 3. Tier 4 QA Specific Fixes
|
||||
|
||||
- Add DO NOT FIX warning - analysis only
|
||||
- Include root cause tracing instructions
|
||||
- Add data flow tracing from docs/guide_architecture.md
|
||||
|
||||
### 4. Tier 2 Tech Lead Specific Fixes
|
||||
|
||||
- Add persistent memory reminder
|
||||
- Add surgical prompt structure (WHERE/WHAT/HOW/SAFETY)
|
||||
- Add delegation via Task tool instructions
|
||||
- Add phase completion verification protocol
|
||||
|
||||
### 5. Tier 1 Orchestrator Specific Fixes
|
||||
|
||||
- Verify mode: primary for main agent sessions
|
||||
- Add track initialization workflow
|
||||
- Add product alignment check
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
Based on:
|
||||
- `conductor/workflow.md` - TDD protocol, commit rules
|
||||
- `conductor/product-guidelines.md` - Code style (1-space indentation)
|
||||
- `opencode.json` - MCP tool definitions (manual-slop_*)
|
||||
- superpowers docs - @mention subagent spawning
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Changes to superpowers plugin itself
|
||||
- Changes to OpenCode core behavior
|
||||
- Changes to mma_exec.py (not used in OpenCode flow)
|
||||
- Changes to Gemini CLI workflow (separate system)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. All Python files in project use 1-space indentation
|
||||
2. Subagents follow TDD protocol without skipping phases
|
||||
3. Pre-delegation checkpoints executed before risky operations
|
||||
4. MCP tools findable by subagents
|
||||
5. Atomic commits maintained per task
|
||||
6. Conductor workflow followed without deviation even after corrections
|
||||
@@ -1,195 +0,0 @@
|
||||
# Clean Install Test
|
||||
|
||||
**Date:** 2026-06-02
|
||||
**Status:** Draft (pending review)
|
||||
|
||||
---
|
||||
|
||||
## Context & Motivation
|
||||
|
||||
The user wants a "clean install" test that verifies Manual Slop works correctly when installed from scratch in an isolated environment. The test should:
|
||||
|
||||
1. Clone the repo to a temp directory (no shared state with the source)
|
||||
2. Install dependencies via `uv sync`
|
||||
3. Launch `sloppy.py --enable-test-hooks`
|
||||
4. Verify the Hook API responds (smoke test that the app is functional)
|
||||
|
||||
This is a defense against:
|
||||
- Repository changes that only work on the developer's machine
|
||||
- Dependency drift (works on dev, fails on fresh install)
|
||||
- Build/launch issues that only appear in clean environments
|
||||
|
||||
The target is the user's private Gitea server: `https://git.cozyair.dev/ed/manual_slop`. This is intentionally NOT a public GitHub URL — the test must work in the user's private infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- `tests/test_clean_install.py` — Opt-in pytest test
|
||||
- `pyproject.toml` update: add `clean_install` marker
|
||||
- Gating via `RUN_CLEAN_INSTALL_TEST=1` env var
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Auto-clone in CI (the user can opt in via a future CI workflow)
|
||||
- Continuous monitoring
|
||||
- Cloning from a specific branch/tag (uses HEAD of main by default)
|
||||
|
||||
---
|
||||
|
||||
## Design
|
||||
|
||||
### Test File Structure
|
||||
|
||||
```python
|
||||
# tests/test_clean_install.py
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
REPO_URL = "https://git.cozyair.dev/ed/manual_slop"
|
||||
STARTUP_TIMEOUT_SECONDS = 30
|
||||
READINESS_POLL_INTERVAL = 0.5
|
||||
|
||||
|
||||
@pytest.mark.clean_install
|
||||
def test_clean_install_runs_with_hooks(tmp_path):
|
||||
"""Clone the repo, install deps, launch sloppy.py, verify Hook API."""
|
||||
if os.environ.get("RUN_CLEAN_INSTALL_TEST") != "1":
|
||||
pytest.skip("Set RUN_CLEAN_INSTALL_TEST=1 to enable")
|
||||
|
||||
clone_dir = tmp_path / "manual_slop"
|
||||
|
||||
# 1. Clone
|
||||
result = subprocess.run(
|
||||
["git", "clone", REPO_URL, str(clone_dir)],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
assert result.returncode == 0, f"Clone failed: {result.stderr}"
|
||||
|
||||
# 2. Install deps
|
||||
result = subprocess.run(
|
||||
["uv", "sync"],
|
||||
cwd=str(clone_dir),
|
||||
capture_output=True, text=True, timeout=180,
|
||||
)
|
||||
assert result.returncode == 0, f"uv sync failed: {result.stderr}"
|
||||
|
||||
# 3. Launch sloppy.py with hooks
|
||||
process = subprocess.Popen(
|
||||
["uv", "run", "sloppy.py", "--enable-test-hooks"],
|
||||
cwd=str(clone_dir),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == 'nt' else 0,
|
||||
)
|
||||
|
||||
try:
|
||||
# 4. Poll /status endpoint
|
||||
start = time.time()
|
||||
ready = False
|
||||
while time.time() - start < STARTUP_TIMEOUT_SECONDS:
|
||||
if process.poll() is not None:
|
||||
pytest.fail(f"Process exited early. stderr: {process.stderr.read()[:2000]}")
|
||||
try:
|
||||
response = requests.get(
|
||||
"http://127.0.0.1:8999/status",
|
||||
timeout=1.0,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
payload = response.json()
|
||||
if payload.get("status") == "running":
|
||||
ready = True
|
||||
break
|
||||
except (requests.ConnectionError, requests.Timeout):
|
||||
pass
|
||||
time.sleep(READINESS_POLL_INTERVAL)
|
||||
|
||||
assert ready, f"Hook server did not respond within {STARTUP_TIMEOUT_SECONDS}s"
|
||||
|
||||
# 5. Test a write hook (any POST endpoint that should respond)
|
||||
response = requests.get(
|
||||
"http://127.0.0.1:8999/api/gui/mma_status",
|
||||
timeout=5.0,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
# The mma_status endpoint returns a dict; verify it has expected keys
|
||||
data = response.json()
|
||||
assert "status" in data or "mma_state" in data
|
||||
|
||||
finally:
|
||||
# 6. Cleanup
|
||||
if os.name == 'nt':
|
||||
subprocess.run(
|
||||
["taskkill", "/F", "/T", "/PID", str(process.pid)],
|
||||
capture_output=True,
|
||||
)
|
||||
else:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
```
|
||||
|
||||
### `pyproject.toml` Update
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"integration: integration tests requiring live GUI",
|
||||
"strict: tests that require strict mode",
|
||||
"clean_install: clean install verification (opt-in via RUN_CLEAN_INSTALL_TEST=1)",
|
||||
]
|
||||
```
|
||||
|
||||
### Running the Test
|
||||
|
||||
**Default (skip):**
|
||||
```bash
|
||||
uv run pytest tests/test_clean_install.py -v
|
||||
# SKIPPED: Set RUN_CLEAN_INSTALL_TEST=1 to enable
|
||||
```
|
||||
|
||||
**Opt-in:**
|
||||
```bash
|
||||
RUN_CLEAN_INSTALL_TEST=1 uv run pytest tests/test_clean_install.py -v
|
||||
```
|
||||
|
||||
**Just the clean_install marker:**
|
||||
```bash
|
||||
RUN_CLEAN_INSTALL_TEST=1 uv run pytest -m clean_install -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- `tests/test_clean_install.py` — NEW
|
||||
- `pyproject.toml` — MODIFY: add `clean_install` marker
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `RUN_CLEAN_INSTALL_TEST=1 uv run pytest tests/test_clean_install.py -v` passes when run in an environment with network access to `git.cozyair.dev`
|
||||
- Without the env var, the test skips (no network access required)
|
||||
- The test takes 30-90 seconds to run (clone + install + launch)
|
||||
- A failure in any step (clone, sync, launch, hook response) results in a clear error message
|
||||
- Process cleanup is robust (no orphaned processes on Windows or Unix)
|
||||
|
||||
---
|
||||
|
||||
## Risks
|
||||
|
||||
1. **Network dependency:** The test requires network access to `git.cozyair.dev`. In CI environments without that access, the test will fail. Mitigation: the env var gating makes this opt-in.
|
||||
2. **Clone target is private:** Unlike GitHub, the URL is on a private Gitea server. Test failure on a public CI would leak the existence of the private repo. Mitigation: only run on private infrastructure; the test is opt-in.
|
||||
3. **Port conflicts:** The test uses port 8999. If another process is using it, the test will fail. Mitigation: the polling loop detects early exit and reports the port-in-use error.
|
||||
4. **`uv sync` is slow:** On a fresh machine, `uv sync` can take 30-60 seconds. The test budget is 180 seconds which should be sufficient.
|
||||
@@ -1,222 +0,0 @@
|
||||
# Command Palette Implementation & Tests
|
||||
|
||||
**Date:** 2026-06-02
|
||||
**Status:** Draft (pending review)
|
||||
**Parent Track:** `command_palette_and_performance_20260602` (continuing Phase 2 + adding Phase 3)
|
||||
**Spec:** `conductor/tracks/command_palette_and_performance_20260602/spec.md` (existing)
|
||||
|
||||
---
|
||||
|
||||
## Context & Motivation
|
||||
|
||||
A `command_palette_and_performance_20260602` track was started in early June 2026. **Phase 1 (Async Context Preview)** is complete. **Phase 2 (Command Palette)** is unstarted — no `src/command_palette.py`, no `src/commands.py`, no test files. The user reports the palette doesn't pop up on `Ctrl+Shift+P`.
|
||||
|
||||
The existing spec says `Ctrl+P`; this design uses `Ctrl+Shift+P` (per the user's expectation and VSCode convention documented in `docs/guide_command_palette.md`).
|
||||
|
||||
This design finishes Phase 2 of the existing track and adds Phase 3 (tests).
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- `src/command_palette.py` — Module-level: `Command` dataclass, `CommandRegistry`, `fuzzy_match()`, `render_palette_modal(app)`, `render_everything_modal(app)`
|
||||
- `src/commands.py` — Static command definitions (~30-50 commands across categories)
|
||||
- `src/gui_2.py` — `self.show_command_palette: bool` in `App.__init__`, `_render_command_palette(self)` thin wrapper, `Ctrl+Shift+P` keyboard handler
|
||||
- `tests/test_command_palette.py` — Unit tests for fuzzy matcher, command registry, mode detection
|
||||
- `tests/test_command_palette_sim.py` — Integration tests via `live_gui`
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Async context preview (Phase 1; already complete)
|
||||
- The "Everything" mode async search worker (mentioned in the existing spec; defer to a follow-up track)
|
||||
- Visual theming of the palette (use existing ImGui style)
|
||||
|
||||
---
|
||||
|
||||
## Design
|
||||
|
||||
### Data Model: `Command`
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Command:
|
||||
id: str # Unique identifier
|
||||
title: str # Display name
|
||||
category: str # Category for grouping
|
||||
shortcut: Optional[str] # Optional default shortcut (e.g., "Ctrl+S")
|
||||
description: str = "" # Optional help text
|
||||
enabled_when: Optional[str] = None # Optional condition expression
|
||||
action: Callable = None # Function to execute when selected
|
||||
```
|
||||
|
||||
### Command Registry
|
||||
|
||||
```python
|
||||
# src/commands.py
|
||||
from src.command_palette import Command, CommandRegistry
|
||||
|
||||
registry = CommandRegistry()
|
||||
|
||||
@registry.register
|
||||
def save_file(app: App) -> None:
|
||||
"""Save File — File category, Ctrl+S"""
|
||||
# ... call app's save logic
|
||||
```
|
||||
|
||||
**Registration patterns:**
|
||||
- Decorator: `@registry.register` for top-level functions
|
||||
- Explicit: `registry.register(Command(id=..., title=..., action=...))` for closures or classes
|
||||
|
||||
### Fuzzy Matcher
|
||||
|
||||
Implemented in `src/command_palette.py` as a pure function:
|
||||
|
||||
```python
|
||||
def fuzzy_match(query: str, candidates: List[Command], top_n: int = 20) -> List[ScoredCommand]:
|
||||
"""
|
||||
Returns the top_n candidates matching query, ranked by score.
|
||||
|
||||
Algorithm:
|
||||
1. Subsequence check: query chars must appear in title, in order
|
||||
2. Score calculation:
|
||||
- Exact prefix match: +1.0
|
||||
- Word boundary match: +0.5
|
||||
- Contiguous match: +0.3
|
||||
- Character distance penalty: -0.1 per gap
|
||||
3. Sort by score descending
|
||||
4. Return top_n
|
||||
"""
|
||||
```
|
||||
|
||||
### Modal Rendering
|
||||
|
||||
The palette is a centered ImGui modal. Module-level function (per delegation pattern):
|
||||
|
||||
```python
|
||||
# src/command_palette.py
|
||||
def render_palette_modal(app: App) -> None:
|
||||
"""Render the Command Palette modal. Called from gui_2.py when app.show_command_palette is True."""
|
||||
if not app.show_command_palette:
|
||||
return
|
||||
imgui.set_next_window_position(...) # Centered
|
||||
imgui.set_next_window_size(...)
|
||||
if imgui.begin("Command Palette##palette", closable=True):
|
||||
# Search input
|
||||
# Fuzzy-matched results list
|
||||
# Keyboard navigation
|
||||
imgui.end()
|
||||
```
|
||||
|
||||
### Keyboard Handler
|
||||
|
||||
In `gui_2.py`'s main event loop:
|
||||
|
||||
```python
|
||||
io = imgui.get_io()
|
||||
if io.key_ctrl and io.key_shift and imgui.is_key_pressed(imgui.Key.p):
|
||||
app.show_command_palette = not app.show_command_palette
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- `src/command_palette.py` — NEW: Command, CommandRegistry, fuzzy_match, render_palette_modal
|
||||
- `src/commands.py` — NEW: Static command definitions and registry
|
||||
- `src/gui_2.py` — MODIFY: add `self.show_command_palette`, add `_render_command_palette` wrapper, add Ctrl+Shift+P handler, register the palette module
|
||||
- `tests/test_command_palette.py` — NEW: Unit tests
|
||||
- `tests/test_command_palette_sim.py` — NEW: Integration tests via live_gui
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
### Unit Tests (`tests/test_command_palette.py`)
|
||||
|
||||
```python
|
||||
def test_fuzzy_match_prefix_ranks_first():
|
||||
from src.command_palette import fuzzy_match
|
||||
candidates = [
|
||||
Command(id="find", title="Find in Selection"),
|
||||
Command(id="fold", title="Fold All"),
|
||||
Command(id="config", title="Configure Settings"),
|
||||
]
|
||||
results = fuzzy_match("fin", candidates)
|
||||
assert results[0].command.id == "find"
|
||||
assert results[0].score > 0.5
|
||||
|
||||
def test_fuzzy_match_rejects_no_match():
|
||||
from src.command_palette import fuzzy_match
|
||||
candidates = [Command(id="x", title="foo bar")]
|
||||
results = fuzzy_match("xyz", candidates)
|
||||
assert len(results) == 0
|
||||
|
||||
def test_command_registry_register_and_list():
|
||||
from src.command_palette import CommandRegistry
|
||||
from src.commands import registry
|
||||
assert "save_file" in registry.all()
|
||||
# All commands have id, title, category
|
||||
for cmd in registry.all():
|
||||
assert cmd.id and cmd.title and cmd.category
|
||||
|
||||
def test_command_registry_duplicate_raises():
|
||||
from src.command_palette import CommandRegistry, Command
|
||||
reg = CommandRegistry()
|
||||
reg.register(Command(id="x", title="X", category="test"))
|
||||
with pytest.raises(ValueError):
|
||||
reg.register(Command(id="x", title="X", category="test"))
|
||||
```
|
||||
|
||||
### Integration Tests (`tests/test_command_palette_sim.py`)
|
||||
|
||||
```python
|
||||
def test_ctrl_shift_p_opens_palette(live_gui):
|
||||
client = live_gui[1]
|
||||
# Press Ctrl+Shift+P
|
||||
client.press_key_combo("Ctrl+Shift+P")
|
||||
# Verify the palette is visible
|
||||
state = client.get_window_state("command_palette")
|
||||
assert state["visible"] == True
|
||||
|
||||
def test_palette_filters_as_user_types(live_gui):
|
||||
client = live_gui[1]
|
||||
client.press_key_combo("Ctrl+Shift+P")
|
||||
client.type_in_palette("save")
|
||||
results = client.get_palette_results()
|
||||
assert any("Save" in r.title for r in results)
|
||||
# Other commands not shown
|
||||
assert not any("Compress" in r.title for r in results)
|
||||
|
||||
def test_palette_executes_command_on_enter(live_gui):
|
||||
client = live_gui[1]
|
||||
client.press_key_combo("Ctrl+Shift+P")
|
||||
client.type_in_palette("Reset")
|
||||
client.press_key("Down")
|
||||
client.press_key("Enter")
|
||||
# Verify the reset command was executed (check via Hook API)
|
||||
state = client.get_session_state()
|
||||
assert state.get("discussion_history", []) == []
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `Ctrl+Shift+P` opens the palette (verified via `live_gui` test)
|
||||
- Typing in the palette filters results via fuzzy match
|
||||
- Selecting a command (Enter key) executes it and closes the palette
|
||||
- Escape closes the palette without executing
|
||||
- All unit tests pass
|
||||
- All integration tests pass
|
||||
- The palette respects the existing theme (dark/light/nerv)
|
||||
- No new lint errors
|
||||
|
||||
---
|
||||
|
||||
## Risks
|
||||
|
||||
1. **Keyboard handler conflicts:** The Ctrl+Shift+P combo might be intercepted by other subsystems. Mitigation: check for other handlers in the codebase first; if conflicts, document them.
|
||||
2. **Pyodide build dependencies:** The image_bundle web backend (for Track 4) has a different architecture than this track. The two are independent but should be aware of each other.
|
||||
3. **Test flakiness:** `live_gui` tests can be flaky if the GUI doesn't initialize in time. Mitigation: the standard 15-second readiness polling is sufficient.
|
||||
@@ -1,385 +0,0 @@
|
||||
# Comprehensive Documentation Refresh
|
||||
|
||||
**Date:** 2026-06-02
|
||||
**Status:** Draft (pending review)
|
||||
**Parent Track:** `documentation_refresh_comprehensive_20260602`
|
||||
**Sub-Tracks:** 3 (sequential with one parallel pair)
|
||||
|
||||
---
|
||||
|
||||
## 1. Context & Motivation
|
||||
|
||||
Manual Slop is a local GUI orchestrator for LLM-driven coding sessions. Since the last documentation refresh track (`documentation_refresh_20260224`, completed February 2026), the codebase has grown substantially:
|
||||
|
||||
- **New subsystems** without dedicated guides: Beads mode (Dolt-backed issue tracking), RAG (ChromaDB + multi-source retrieval), Hot Reload (state-preserving module reloading), Discussion Metrics & Compression, Command Palette, Structural File Editor (unified AST inspector + slice editor)
|
||||
- **New language support** via tree-sitter: C, C++ (already in docs), plus planned Lua, GDScript, C# (in backlog)
|
||||
- **Two new guide files** that exist but are not linked from the docs index: `docs/guide_context_curation.md`, `docs/guide_shaders_and_window.md`
|
||||
- **Drift in agent config files** (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`): the three files overlap on Session Startup, Conductor System, MMA tiers, and anti-patterns; no single source of truth
|
||||
- **CLAUDE.md is largely vestigial** — Claude Code is no longer the primary toolchain, but the file still receives content updates
|
||||
- **AGENTS.md contains rich guidance content** that should live in `mma-orchestrator/SKILL.md` or `conductor/workflow.md`, not as a surface-level orientation file
|
||||
|
||||
The user has explicitly stated two goals for this refresh:
|
||||
|
||||
1. **Human-facing docs (`./docs/`, READMEs)** must reach **textbook / Microsoft "purple-tomb" SDK documentation** fidelity. Every detail: state machines, public APIs, threading constraints, algorithms. No hand-waving, no "left to the reader." A human should be able to use the tool AND maintain/extend the codebase from the docs alone.
|
||||
2. **Agent-facing docs** (`AGENTS.md`, `GEMINI.md`, skill files in `.agents/`, `.gemini/`) must follow an **explicit divergence model** with no content duplication. `AGENTS.md` becomes a thin pointer; `CLAUDE.md` is deprecated; `GEMINI.md` stays Gemini-CLI-specific.
|
||||
|
||||
This refresh is **documentation-only** (no new code, no new tooling, no new scripts). Drift control after the refresh is manual via the track's verification step.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope Boundaries
|
||||
|
||||
### In Scope
|
||||
|
||||
- Top-level `Readme.md`
|
||||
- `docs/Readme.md` and all `docs/guide_*.md` files
|
||||
- `conductor/product.md`, `conductor/product-guidelines.md`, `conductor/tech-stack.md`, `conductor/workflow.md`, `conductor/index.md`
|
||||
- `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`
|
||||
- Light verification pass (read each, confirm role accuracy, fix obvious drift; do NOT rewrite wholesale) on `.agents/skills/*/SKILL.md`, `mma-orchestrator/SKILL.md`, `.gemini/...` mirrors
|
||||
- Per-file atomic git commits with git notes attached as rationale trails
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Source code changes (a separate agent is making code modifications)
|
||||
- New lint/check scripts (drift control is manual)
|
||||
- New `docs/` files beyond what's required to document new subsystems
|
||||
- Re-architecting the conductor system itself
|
||||
- Removing CLAUDE.md (kept as a stub for compatibility)
|
||||
- Visual diagrams (ASCII is fine; no image generation)
|
||||
- Translating the docs to other languages
|
||||
|
||||
---
|
||||
|
||||
## 3. Track Architecture
|
||||
|
||||
The parent track `documentation_refresh_comprehensive_20260602` is a coordinator: it registers the three sub-tracks in `conductor/tracks.md`, sequences them, and produces a final verification report. Each sub-track is an independent conductor track with its own `spec.md`, `plan.md`, and `metadata.json`.
|
||||
|
||||
### Dependency Graph
|
||||
|
||||
```
|
||||
Sub-Track 1: docs_layer_refresh
|
||||
│
|
||||
├──> Sub-Track 2: conductor_docs_refresh
|
||||
│
|
||||
└──> Sub-Track 3: agent_config_refresh
|
||||
│
|
||||
└──> Parent verification
|
||||
```
|
||||
|
||||
Sub-Tracks 2 and 3 can run in parallel after Sub-Track 1 establishes the human-facing doc layer. The parent track does the final verification and creates the checkpoint commit.
|
||||
|
||||
### Execution Constraints (apply to all 3 sub-tracks)
|
||||
|
||||
- **No subagents.** Each sub-track is executed by a single agent (the user or the Tier 2 Tech Lead session) sequentially. No Tier 3 worker delegation, no Tier 4 QA delegation, no parallel worker execution.
|
||||
- **Per-file atomic commits.** One file = one commit. The git note attached to each commit captures the rationale and the source files cross-referenced during the rewrite.
|
||||
- **Pre-edit `git add .` checkpoint** before each file edit (protects against `git restore` mishaps).
|
||||
- **Style baseline:** `conductor/product-guidelines.md` — VEFontCache-Odin pattern (Philosophy → Architectural Boundaries → Implementation Logic → Verification), high information density, structural parity between doc symbols and source code symbols.
|
||||
- **Symbol parity:** Every class, function, method, and event named in the docs must match the source exactly. If the source uses `AsyncEventQueue`, the doc must use `AsyncEventQueue`, not "the event queue."
|
||||
- **Link validation:** Every `[text](./relative/path)` link is manually verified via `manual-slop_search_files` to confirm the target exists. Cross-doc links must point to files that exist in this or a future sub-track.
|
||||
- **No comments added to source code.** Documentation lives in `./docs/`, not inline.
|
||||
|
||||
---
|
||||
|
||||
## 4. Sub-Track 1: `docs_layer_refresh_20260602`
|
||||
|
||||
**Priority:** HIGHEST. This is the dominant work.
|
||||
|
||||
### File List
|
||||
|
||||
| File | Current Size | Notes |
|
||||
|---|---|---|
|
||||
| `Readme.md` | 15.5K | Main project README |
|
||||
| `docs/Readme.md` | ~3K | Documentation index |
|
||||
| `docs/guide_architecture.md` | 34.5K | Threading, events, AI client, HITL |
|
||||
| `docs/guide_mma.md` | unknown | 4-tier hierarchy, DAG, worker lifecycle |
|
||||
| `docs/guide_tools.md` | 22.9K | MCP tools, Hook API, shell runner |
|
||||
| `docs/guide_simulations.md` | unknown | live_gui fixture, Puppeteer, mock provider |
|
||||
| `docs/guide_context_curation.md` | 8K | Skeleton/curated/targeted views, fuzzy anchors |
|
||||
| `docs/guide_shaders_and_window.md` | 3.4K | Shader pipeline, custom window frame |
|
||||
| `docs/guide_meta_boundary.md` | 4.6K | Application vs Meta-Tooling domain |
|
||||
| `docs/MMA_Support/*` | varies | Legacy MMA reference docs — review for archival or merge |
|
||||
|
||||
### Tasks (Phases)
|
||||
|
||||
**Phase 1.1: Inventory and Gap Analysis**
|
||||
- Read each existing guide, summarize what subsystems it covers
|
||||
- Identify subsystems in `conductor/product.md` that have NO corresponding guide
|
||||
- Identify subsystems that DO have a guide but the guide is stale (e.g., does not cover features added since Feb 2026)
|
||||
- Produce a gap table: subsystem → current guide → needed changes (rewrite / add / link from index / remove)
|
||||
|
||||
**Phase 1.2: Update the Documentation Index**
|
||||
- Update `docs/Readme.md` so every `docs/guide_*.md` appears in the Guides table
|
||||
- Currently unlinked: `docs/guide_context_curation.md`, `docs/guide_shaders_and_window.md`
|
||||
- Decide fate of `docs/MMA_Support/*` (legacy archive or merge into `docs/guide_mma.md`)
|
||||
- Commit: `docs(index): link guide_context_curation and guide_shaders_and_window`
|
||||
|
||||
**Phase 1.3: Rewrite Each Guide (one file at a time, per-file atomic commits)**
|
||||
- For each guide, follow the per-file workflow:
|
||||
1. Read current source code (use `manual-slop_get_file_summary` and `manual-slop_py_get_skeleton`; do NOT read full files >50 lines)
|
||||
2. Read current `docs/MMA_Support/*` and `conductor/tracks/*/plan.md` for context on subsystems that have evolved
|
||||
3. Cross-reference `conductor/product.md` and `conductor/tech-stack.md` for the canonical feature/tech statement
|
||||
4. Identify what changed since the guide was last meaningfully updated
|
||||
5. Rewrite affected sections, preserving the VEFontCache-Odin style (Philosophy → Architectural Boundaries → Implementation Logic → Verification)
|
||||
6. Validate every internal `[link](./relative/path)` resolves
|
||||
7. Commit per-file with `docs(<scope>): <description>` message
|
||||
8. Attach git note with: source files cross-referenced, subsystems updated, any decisions made
|
||||
|
||||
**Phase 1.4: Update `Readme.md`**
|
||||
- Add a "Module by Domain" reference table that links to each guide for deep dives
|
||||
- Add a "Subsystem Index" section cross-referencing each major feature to its dedicated guide
|
||||
- Update the screenshot/gallery references if needed
|
||||
- Update setup instructions to reflect any new prerequisites (e.g., Beads CLI, Dolt)
|
||||
- Commit per-section: `docs(readme): add module-by-domain reference table`, `docs(readme): add subsystem index`, `docs(readme): update setup prerequisites`, etc.
|
||||
|
||||
**Phase 1.5: Subsystems Without Guides (write new guides as needed)**
|
||||
For each subsystem identified in 1.1 that lacks a guide, write a new `docs/guide_<name>.md` using the same per-file workflow. Likely candidates based on the gap analysis:
|
||||
- RAG (`docs/guide_rag.md`) — vector store, chunking, multi-provider search
|
||||
- Beads (`docs/guide_beads.md`) — Dolt integration, toolset, context compaction
|
||||
- Hot Reload (`docs/guide_hot_reload.md`) — `HotReloader` lifecycle, state preservation, UI delegation pattern
|
||||
- Discussion Metrics & Compression (`docs/guide_discussion_metrics.md`) — per-response token tracking, history compression strategy
|
||||
- Command Palette (`docs/guide_command_palette.md`) — async context preview, fuzzy command resolution
|
||||
- Structural File Editor (`docs/guide_structural_editor.md`) — unified AST inspector + slice editor
|
||||
- NERV Theme (`docs/guide_nerv_theme.md`) — black void palette, CRT-style effects, status flickering
|
||||
|
||||
Each new guide must follow the same VEFontCache-Odin style and link from `docs/Readme.md` (which means the index update in 1.2 must accommodate them).
|
||||
|
||||
**Phase 1.6: Verification**
|
||||
- `grep` for every internal link in every rewritten guide to confirm targets exist
|
||||
- `grep` for stale feature references (e.g., "MMA 4-tier" without the recent persona, RAG, Beads integrations mentioned in `conductor/product.md`)
|
||||
- Spot-check 3 random sections per guide against the corresponding source code
|
||||
- Manual user review (per workflow.md Phase Completion Verification protocol)
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Every `docs/guide_*.md` is referenced from `docs/Readme.md`
|
||||
- Every internal cross-doc link resolves
|
||||
- Every subsystem in `conductor/product.md` has a corresponding guide (either existing or newly written)
|
||||
- Every public class, function, and event named in any guide matches the source code symbol exactly
|
||||
- Every guide follows the VEFontCache-Odin pattern (Philosophy → Architectural Boundaries → Implementation Logic → Verification) with all four sections present where applicable
|
||||
- "Textbook / purple-tomb fidelity" interpreted concretely: for every public class/function/event mentioned, the doc states its signature, threading constraints, side effects, and at least one usage example. Internal algorithms are explained step-by-step, not summarized. State machines include the full transition table
|
||||
- Per-file commits exist for every change, with git notes attached
|
||||
|
||||
---
|
||||
|
||||
## 5. Sub-Track 2: `conductor_docs_refresh_20260602`
|
||||
|
||||
**Priority:** Medium. Sync the source-of-truth conductor docs to current state.
|
||||
|
||||
### File List
|
||||
|
||||
| File | Current Size | Notes |
|
||||
|---|---|---|
|
||||
| `conductor/product.md` | 23.1K | Product vision, feature list, use cases |
|
||||
| `conductor/product-guidelines.md` | 6.6K | Style and process guidelines |
|
||||
| `conductor/tech-stack.md` | unknown | Technology stack constraints |
|
||||
| `conductor/workflow.md` | 25.2K | Task lifecycle, TDD protocol |
|
||||
| `conductor/index.md` | 334 bytes | Conductor directory index |
|
||||
|
||||
### Tasks (Phases)
|
||||
|
||||
**Phase 2.1: Sync `conductor/product.md`**
|
||||
- Cross-reference every feature claim in `product.md` against the actual codebase
|
||||
- Add features that exist in code but are missing from `product.md` (Beads, RAG, Hot Reload, Discussion Metrics/Compression, Command Palette, Structural File Editor, NERV theme, persona editor, workspace profiles, etc.)
|
||||
- Remove features that have been culled or replaced
|
||||
- Update the "Primary Use Cases" section to reflect current usage patterns
|
||||
- Commit per-section: `docs(product): sync MMA dashboard section`, `docs(product): add RAG feature`, etc.
|
||||
|
||||
**Phase 2.2: Sync `conductor/tech-stack.md`**
|
||||
- Verify every entry corresponds to an actual Python dep (check `pyproject.toml` / `requirements.txt`) or an actual src/ module
|
||||
- Add new dependencies: chromadb (RAG), dolt (Beads), any new ones
|
||||
- Add new src/ modules: rag_engine.py, beads_client.py, hot_reloader.py, history.py, workspace_manager.py, etc.
|
||||
- Commit per-section.
|
||||
|
||||
**Phase 2.3: Sync `conductor/workflow.md`**
|
||||
- Verify the TDD protocol is followed by recent tracks (spot-check 3 recent tracks)
|
||||
- Verify the Phase Completion Verification protocol is followed
|
||||
- Verify the checkpointing protocol is followed
|
||||
- Update any sections that have drifted from actual practice
|
||||
- Commit per-section.
|
||||
|
||||
**Phase 2.4: Sync `conductor/product-guidelines.md`**
|
||||
- Verify the AI-Optimized Python Style (1-space indent, no comments, type hints) is actually followed by recent code
|
||||
- Verify the "Modular Controller Pattern" and "UI Delegation Pattern" are followed in `src/app_controller.py` and `src/gui_2.py`
|
||||
- Verify the "Mandatory ImGui Verification" linter (`scripts/check_imgui_scopes.py`) is still the source of truth
|
||||
- Update any sections that have drifted
|
||||
- Commit per-section.
|
||||
|
||||
**Phase 2.5: Update `conductor/index.md`**
|
||||
- If any new top-level conductor docs were added (e.g., `conductor/code_styleguides/` is referenced but not verified), link them
|
||||
- Commit.
|
||||
|
||||
**Phase 2.6: Verification**
|
||||
- `grep` for references to features that no longer exist
|
||||
- `grep` for missing dependencies (every dep in `pyproject.toml` should appear in `tech-stack.md`)
|
||||
- Spot-check 3 random claims per file
|
||||
- Manual user review
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Every feature in `conductor/product.md` corresponds to a real subsystem in `src/` or `tests/`
|
||||
- Every entry in `conductor/tech-stack.md` is an actual dep or module
|
||||
- `conductor/workflow.md` matches the actual workflow used by recent completed tracks
|
||||
- `conductor/product-guidelines.md` matches the actual coding style used in `src/`
|
||||
- `conductor/index.md` links to all relevant conductor files
|
||||
- Per-file commits with git notes
|
||||
|
||||
---
|
||||
|
||||
## 6. Sub-Track 3: `agent_config_refresh_20260602`
|
||||
|
||||
**Priority:** Lower. Apply the explicit divergence model.
|
||||
|
||||
### File List
|
||||
|
||||
| File | Current Size | Notes |
|
||||
|---|---|---|
|
||||
| `AGENTS.md` | 5.4K | Universal agent orientation — target: thin pointer |
|
||||
| `CLAUDE.md` | 6.7K | DEPRECATE to stub |
|
||||
| `GEMINI.md` | 3.8K | Keep Gemini-CLI-specific content |
|
||||
| `.agents/skills/mma-tier1-orchestrator/SKILL.md` | 2.4K | Light verification only |
|
||||
| `.agents/skills/mma-tier2-tech-lead/SKILL.md` | 3.8K | Light verification only |
|
||||
| `.agents/skills/mma-tier3-worker/SKILL.md` | unknown | Light verification only |
|
||||
| `.agents/skills/mma-tier4-qa/SKILL.md` | 813 bytes | Light verification only |
|
||||
| `.agents/skills/mma-orchestrator/SKILL.md` | 9.7K | Light verification only |
|
||||
| `mma-orchestrator/SKILL.md` (root copy) | 9.3K | Light verification only |
|
||||
| `.gemini/skills/*/SKILL.md` | varies | Light verification only (mirrors) |
|
||||
| `.gemini/agents/*.md` | varies | Light verification only (mirrors) |
|
||||
| `.claude/commands/*.md` | 9 files | Light verification only (legacy) |
|
||||
|
||||
### Tasks (Phases)
|
||||
|
||||
**Phase 3.1: Rewrite `AGENTS.md` to a thin pointer**
|
||||
|
||||
Target structure (estimated ~1K):
|
||||
|
||||
```markdown
|
||||
# AGENTS.md
|
||||
|
||||
## What This Is
|
||||
Manual Slop is a local GUI orchestrator for LLM-driven coding sessions. It bridges high-latency AI reasoning with a low-latency ImGui render loop via a thread-safe async pipeline; every AI-generated payload passes through a human-auditable gate before execution.
|
||||
|
||||
## Guidance for AI Agents
|
||||
Detailed agent guidance lives in the following locations — read these directly, do not duplicate content here:
|
||||
|
||||
- **Operational workflow:** `conductor/workflow.md`
|
||||
- **Code style and process:** `conductor/product-guidelines.md`
|
||||
- **Tech stack and constraints:** `conductor/tech-stack.md`
|
||||
- **Product context:** `conductor/product.md`
|
||||
- **MMA orchestrator role:** `mma-orchestrator/SKILL.md`
|
||||
- **Tier 1 (Orchestrator):** `.agents/skills/mma-tier1-orchestrator/SKILL.md`
|
||||
- **Tier 2 (Tech Lead):** `.agents/skills/mma-tier2-tech-lead/SKILL.md`
|
||||
- **Tier 3 (Worker):** `.agents/skills/mma-tier3-worker/SKILL.md`
|
||||
- **Tier 4 (QA):** `.agents/skills/mma-tier4-qa/SKILL.md`
|
||||
|
||||
## Human-Facing Documentation
|
||||
For understanding, using, and maintaining the tool, see `docs/Readme.md` and the guides it indexes.
|
||||
|
||||
## Critical Anti-Patterns
|
||||
- Do not read full files >50 lines without first using `py_get_skeleton` or `get_file_summary`
|
||||
- Do not modify the tech stack without updating `conductor/tech-stack.md` first
|
||||
- Do not implement code directly as Tier 2 — delegate to Tier 3 workers
|
||||
- Do not skip TDD — write failing tests before implementation
|
||||
- Do not batch commits — commit per-task for atomic rollback
|
||||
```
|
||||
|
||||
**Phase 3.2: Replace `CLAUDE.md` with a 1-line stub**
|
||||
|
||||
Target content:
|
||||
|
||||
```markdown
|
||||
# CLAUDE.md
|
||||
|
||||
This project is no longer actively used with Claude Code. For project context, see `AGENTS.md`. The conductor system in `./conductor/` is the cross-tool abstraction and works with any agent toolchain.
|
||||
```
|
||||
|
||||
(Keep the file as a stub to avoid breaking any external tooling that may still reference it.)
|
||||
|
||||
**Phase 3.3: Lightly refresh `GEMINI.md`**
|
||||
|
||||
- Verify it covers only Gemini-CLI-specific content (no shared content that should live in `AGENTS.md`)
|
||||
- Update any stale references to features, models, or paths
|
||||
- If any content was previously shared with `AGENTS.md` and has been relocated, update `GEMINI.md` to point to `AGENTS.md` instead
|
||||
- Commit: `docs(gemini): refresh Gemini CLI orientation`
|
||||
|
||||
**Phase 3.4: Verify `.agents/skills/*/SKILL.md` and `.gemini/...` mirrors for coherence**
|
||||
|
||||
For each skill file:
|
||||
- Read it; confirm it covers the role accurately
|
||||
- Compare with the canonical role description in `mma-orchestrator/SKILL.md`
|
||||
- If drift is found, fix it (small surgical edits)
|
||||
- If the file is outdated, flag for a separate track — do not rewrite wholesale in this track
|
||||
- Commit per file: `docs(skills): fix drift in tier1 orchestrator skill`
|
||||
|
||||
**Phase 3.5: Verify `.claude/commands/*.md` (legacy)**
|
||||
|
||||
- These exist for backward compatibility
|
||||
- Lightly verify they still work and don't reference removed features
|
||||
- Do not rewrite — they are legacy
|
||||
- If a file is broken, flag for separate track
|
||||
|
||||
**Phase 3.6: Verification**
|
||||
- `grep` for the section headers that should have been relocated (Session Startup, Conductor System, MMA 4-Tier, Anti-Patterns) across `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`
|
||||
- Confirm each appears in at most one file (or in multiple files only as short pointers)
|
||||
- Spot-check 2 random skills files against their canonical description
|
||||
- Manual user review
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- `AGENTS.md` is ≤ 1.5K and contains only project orientation + pointers
|
||||
- `CLAUDE.md` is ≤ 200 bytes and contains only the deprecation stub
|
||||
- `GEMINI.md` contains only Gemini-CLI-specific content
|
||||
- No section header appears in 2+ of these files unless it's a short pointer (≤3 lines)
|
||||
- `.agents/skills/*/SKILL.md` and `.gemini/...` mirrors are verified for coherence
|
||||
- `.claude/commands/*.md` legacy files are verified to not reference removed features
|
||||
- Per-file commits with git notes
|
||||
|
||||
---
|
||||
|
||||
## 7. Parent Track: Final Verification
|
||||
|
||||
After all 3 sub-tracks complete, the parent track produces a final verification report:
|
||||
|
||||
1. **Cross-track link audit:** For every markdown file modified in any sub-track, run `grep` for `[text](./relative/path)` patterns and verify each target exists
|
||||
2. **Symbol parity audit:** Spot-check 5 random symbols named in any doc against the source — confirm exact match
|
||||
3. **Drift audit:** `grep` for the relocated section headers (`Session Startup`, `Conductor System`, `MMA 4-Tier`, `Anti-Patterns`, etc.) across `AGENTS.md`, `CLAUDE.md`, `GEMINI.md` — confirm explicit divergence holds
|
||||
4. **Subsystem coverage audit:** For every feature in `conductor/product.md`, confirm a corresponding guide exists in `docs/`
|
||||
5. **User review gate:** Present the final verification report and request user sign-off per workflow.md Phase Completion Verification protocol
|
||||
|
||||
The parent track then creates a single checkpoint commit: `conductor(checkpoint): Comprehensive documentation refresh complete` and attaches the verification report as a git note.
|
||||
|
||||
---
|
||||
|
||||
## 8. Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| A source code change by the other agent makes a doc claim stale mid-refresh | Per-file atomic commits + pre-edit `git add .` checkpoint; if drift is found during a later file's rewrite, fix the earlier file in a follow-up commit |
|
||||
| Scope creep — adding new guides balloons the track | The gap analysis in 1.1 is the gating step; new guides are written only for subsystems explicitly listed in `conductor/product.md` |
|
||||
| The 3 sub-tracks drift in style consistency | Style baseline is `conductor/product-guidelines.md` (single source); reviewed by user per sub-track |
|
||||
| Manual link validation misses broken links | Per-file commit immediately after each rewrite; a broken link is caught in the next file's cross-reference work |
|
||||
| User reviews take long enough that the codebase evolves underneath | Sub-tracks are independent; each can resume from its last commit if interrupted |
|
||||
| The legacy `CLAUDE.md` deprecation breaks external tooling | The file is kept as a 1-line stub, not removed; any external tooling that reads it will see a graceful pointer to `AGENTS.md` |
|
||||
|
||||
---
|
||||
|
||||
## 9. Out of Scope (Reminder)
|
||||
|
||||
- Source code changes
|
||||
- New lint scripts (drift control is manual)
|
||||
- Image/diagram generation
|
||||
- Translations
|
||||
- Removing CLAUDE.md
|
||||
- Visual layout of the docs site (Manual Slop is a GUI app, not a docs site)
|
||||
|
||||
---
|
||||
|
||||
## 10. Success Criteria
|
||||
|
||||
The track is complete when:
|
||||
|
||||
1. All 3 sub-tracks have completed all their phases with per-file atomic commits and git notes
|
||||
2. The parent track's verification report shows zero broken cross-doc links, zero symbol mismatches in spot-checks, and confirmed explicit divergence in agent config files
|
||||
3. Every subsystem in `conductor/product.md` has a corresponding guide
|
||||
4. `Readme.md`, `docs/Readme.md`, and every `docs/guide_*.md` is at textbook / purple-tomb fidelity (verified by user review)
|
||||
5. `AGENTS.md` is a thin pointer document; `CLAUDE.md` is a deprecation stub
|
||||
6. The user has signed off on the final verification report per workflow.md
|
||||
@@ -1,288 +0,0 @@
|
||||
# Docker Container & Web-Hosted ImGui Frontend
|
||||
|
||||
**Date:** 2026-06-02
|
||||
**Status:** Draft (pending review)
|
||||
**Reference:** https://imgui-bundle.pages.dev/explorer/ — imgui-bundle web backend via Hello ImGui
|
||||
|
||||
---
|
||||
|
||||
## Context & Motivation
|
||||
|
||||
The user wants to deploy Manual Slop on Unraid (a home server OS) and access the GUI via a web browser. The goal is for agents to operate on projects hosted on the home server, with the user monitoring/controlling via web browser.
|
||||
|
||||
Current state:
|
||||
- `sloppy.py` is a desktop GUI (ImGui via imgui-bundle + Python)
|
||||
- `src/api_hooks.py` provides a FastAPI/Uvicorn headless service on `:8999` for external automation
|
||||
- The app is Windows-oriented (PowerShell subprocesses, `pywin32` for window frame)
|
||||
|
||||
Target state:
|
||||
- Docker container with the full app
|
||||
- Web browser shows the ImGui GUI in real-time
|
||||
- Agents can interact via the existing Hook API on `:8999`
|
||||
- The user's Unraid server can host multiple project directories
|
||||
|
||||
imgui-bundle's web backend ([reference](https://imgui-bundle.pages.dev/explorer/)) uses a server-side render with a client-side WebGL display. The Hello ImGui runner pairs a Python render loop with a JavaScript WebGL canvas via WebSocket.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- `Dockerfile` — Container build for the Manual Slop app
|
||||
- `docker-compose.yml` — Multi-container deployment for Unraid
|
||||
- `scripts/docker_build.sh` — Build helper
|
||||
- `scripts/docker_run.sh` — Run helper with env var wiring
|
||||
- `docs/guide_docker_deployment.md` — Unraid setup guide
|
||||
- `tests/test_docker_build.py` — Opt-in Docker build test
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Migrating `sloppy.py` to use the imgui-bundle web backend (the web backend is an alternative to the desktop backend; switching is a significant refactor and may be deferred)
|
||||
- Multi-user authentication (single-user deployment)
|
||||
- Cloud-specific deployment (AWS, GCP) — Unraid is the target
|
||||
- TLS termination (assumed handled by a reverse proxy like Traefik or Caddy)
|
||||
|
||||
---
|
||||
|
||||
## Design
|
||||
|
||||
### Architecture: V2 — Server-side Python + WebGL client (via WebSocket)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Docker Container (unraid:manual_slop:latest) │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ Python app │ │
|
||||
│ │ - ImGui renders to framebuffer │ │
|
||||
│ │ - Hello ImGui web backend: │ │
|
||||
│ │ - Python: render loop │ │
|
||||
│ │ - WebSocket: frame deltas │ │
|
||||
│ │ - HTTP: serves JS client │ │
|
||||
│ │ - HookServer on :8999 │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Exposed ports: │
|
||||
│ - 8080: Web client (HTTP + WS) │
|
||||
│ - 8999: Hook API │
|
||||
│ │
|
||||
│ Volumes: │
|
||||
│ - /projects: project workspaces │
|
||||
│ - /config: app state, presets, personas │
|
||||
└─────────────────────────────────────────────┘
|
||||
↑ ↑
|
||||
│ Browser (Chrome, Firefox) │ Agent (curl, scripts)
|
||||
│ WebSocket for live frames │ HTTP for state
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
# System deps
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git curl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv
|
||||
RUN pip install uv
|
||||
|
||||
# App setup
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --frozen
|
||||
|
||||
COPY . .
|
||||
|
||||
# Volumes
|
||||
RUN mkdir -p /projects /config
|
||||
VOLUME ["/projects", "/config"]
|
||||
|
||||
# Expose
|
||||
EXPOSE 8080 8999
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||||
CMD curl -f http://127.0.0.1:8999/status || exit 1
|
||||
|
||||
# Entrypoint
|
||||
ENTRYPOINT ["uv", "run", "sloppy.py", "--enable-test-hooks", "--web-host=0.0.0.0", "--web-port=8080"]
|
||||
```
|
||||
|
||||
### `docker-compose.yml`
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
manual_slop:
|
||||
build: .
|
||||
image: manual_slop:latest
|
||||
container_name: manual_slop
|
||||
ports:
|
||||
- "8999:8999" # Hook API (host)
|
||||
- "8080:8080" # Web client (host)
|
||||
volumes:
|
||||
- /mnt/user/projects:/projects:rw # Unraid project share
|
||||
- /mnt/user/appdata/manual_slop:/config:rw # App state
|
||||
environment:
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}
|
||||
- MINIMAX_API_KEY=${MINIMAX_API_KEY}
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:8999/status"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
### Entry Point Changes (`sloppy.py`)
|
||||
|
||||
The current `sloppy.py` launches the desktop GUI. For web mode, we need:
|
||||
|
||||
```python
|
||||
# In sloppy.py
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
# ... existing args ...
|
||||
parser.add_argument("--web-host", default=None, help="Enable web mode and bind to this host")
|
||||
parser.add_argument("--web-port", type=int, default=8080, help="Web mode port")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.web_host is not None:
|
||||
from imgui_bundle import hello_imgui
|
||||
runner_params = hello_imgui.RunnerParams()
|
||||
runner_params.app_window_params.borderless = False
|
||||
runner_params.imgui_window_params.default_imgui_window_type = ... # web backend
|
||||
hello_imgui.run(runner_params)
|
||||
else:
|
||||
# Existing desktop launch
|
||||
...
|
||||
```
|
||||
|
||||
The imgui-bundle web backend is selected by the Hello ImGui runner. The exact config is per the [imgui-bundle explorer docs](https://imgui-bundle.pages.dev/explorer/).
|
||||
|
||||
### `docs/guide_docker_deployment.md`
|
||||
|
||||
A complete Unraid setup guide:
|
||||
|
||||
- Prerequisites (Unraid version, Docker template)
|
||||
- Building the image
|
||||
- Configuring volumes and env vars
|
||||
- Accessing the web client (URL, browser requirements)
|
||||
- Agent interaction examples (curl, Python script)
|
||||
- Backup and restore of /config
|
||||
- Updating the image
|
||||
|
||||
### `tests/test_docker_build.py`
|
||||
|
||||
```python
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
IMAGE_NAME = "manual_slop:test"
|
||||
CONTAINER_NAME = "manual_slop_test"
|
||||
WEB_PORT = 8080
|
||||
HOOK_PORT = 8999
|
||||
|
||||
|
||||
@pytest.mark.docker
|
||||
def test_docker_container_starts_and_serves(tmp_path):
|
||||
"""Build the Docker image, run the container, verify web client + hook API."""
|
||||
if os.environ.get("RUN_DOCKER_TEST") != "1":
|
||||
pytest.skip("Set RUN_DOCKER_TEST=1 to enable")
|
||||
|
||||
if not _docker_available():
|
||||
pytest.skip("Docker not available")
|
||||
|
||||
# Build
|
||||
result = subprocess.run(
|
||||
["docker", "build", "-t", IMAGE_NAME, "."],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
)
|
||||
assert result.returncode == 0, f"Docker build failed: {result.stderr}"
|
||||
|
||||
# Run
|
||||
subprocess.run(["docker", "rm", "-f", CONTAINER_NAME], capture_output=True)
|
||||
result = subprocess.run([
|
||||
"docker", "run", "-d",
|
||||
"--name", CONTAINER_NAME,
|
||||
"-p", f"{WEB_PORT}:8080",
|
||||
"-p", f"{HOOK_PORT}:8999",
|
||||
IMAGE_NAME,
|
||||
], capture_output=True, text=True, timeout=30)
|
||||
assert result.returncode == 0, f"Docker run failed: {result.stderr}"
|
||||
|
||||
try:
|
||||
# Wait for hook API
|
||||
start = time.time()
|
||||
ready = False
|
||||
while time.time() - start < 60:
|
||||
try:
|
||||
r = requests.get(f"http://127.0.0.1:{HOOK_PORT}/status", timeout=1)
|
||||
if r.status_code == 200:
|
||||
ready = True
|
||||
break
|
||||
except (requests.ConnectionError, requests.Timeout):
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
assert ready, "Container did not start hook API within 60s"
|
||||
|
||||
# Verify web client is served
|
||||
r = requests.get(f"http://127.0.0.1:{WEB_PORT}/", timeout=5)
|
||||
assert r.status_code == 200
|
||||
assert b"<html" in r.content.lower() or b"<!doctype" in r.content.lower()
|
||||
|
||||
finally:
|
||||
subprocess.run(["docker", "rm", "-f", CONTAINER_NAME], capture_output=True)
|
||||
|
||||
|
||||
def _docker_available() -> bool:
|
||||
result = subprocess.run(["docker", "version"], capture_output=True)
|
||||
return result.returncode == 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- `Dockerfile` — NEW
|
||||
- `docker-compose.yml` — NEW
|
||||
- `scripts/docker_build.sh` — NEW
|
||||
- `scripts/docker_run.sh` — NEW
|
||||
- `docs/guide_docker_deployment.md` — NEW
|
||||
- `tests/test_docker_build.py` — NEW
|
||||
- `sloppy.py` — MODIFY: add `--web-host` and `--web-port` args
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `docker build -t manual_slop:latest .` succeeds on a clean machine
|
||||
- `docker compose up` starts the container, and `:8999/status` returns 200 within 60s
|
||||
- `curl http://localhost:8080/` returns the web client HTML
|
||||
- An agent can `curl http://localhost:8999/api/gui/mma_status` and get a valid response
|
||||
- The user can navigate to the web UI in a browser and see the ImGui panels
|
||||
- File operations on `/projects` persist across container restarts
|
||||
- Env vars for API keys are not committed to the image (use runtime env)
|
||||
|
||||
---
|
||||
|
||||
## Risks
|
||||
|
||||
1. **imgui-bundle web backend maturity:** The web backend is less battle-tested than the desktop backend. There may be rendering quirks, input latency, or unsupported features. Mitigation: this is experimental; expect to iterate.
|
||||
2. **Headless rendering in container:** Some ImGui features (e.g., font hinting) may need extra config for headless rendering. Mitigation: test early in development; fall back to Xvfb + noVNC if web backend is too immature.
|
||||
3. **WebSocket bandwidth:** Streaming frame deltas requires consistent network. On flaky networks, the user experience degrades. Mitigation: implement client-side prediction or reduce frame rate.
|
||||
4. **Container size:** Python + uv + all deps can produce a 1-2GB image. Mitigation: use multi-stage builds; pin Python deps for reproducibility.
|
||||
5. **Unraid-specific quirks:** Unraid uses a specific Docker storage driver and may have path mapping edge cases. Mitigation: test on the actual Unraid deployment; document the path mapping clearly.
|
||||
@@ -1,220 +0,0 @@
|
||||
# Test Consolidation & TOML Sandboxing Enforcement
|
||||
|
||||
**Date:** 2026-06-02
|
||||
**Status:** Draft (pending review)
|
||||
|
||||
---
|
||||
|
||||
## Context & Motivation
|
||||
|
||||
The Manual Slop test suite has grown to ~258 test files. Many tests read or write project TOML files (manual_slop.toml, config.toml, credentials.toml, presets.toml, etc.) for fixtures. The pattern is inconsistent:
|
||||
|
||||
- Some tests use `tmp_path` + `monkeypatch` (good — isolated)
|
||||
- Some tests use real `./` paths (bad — pollutes user config)
|
||||
- Some tests use mock paths at module level (good — fast)
|
||||
|
||||
The user wants to:
|
||||
1. Audit tests for real-TOML usage
|
||||
2. Migrate offenders to sandboxed variants
|
||||
3. Consolidate similar tests where it improves clarity
|
||||
4. Enforce the rule going forward
|
||||
|
||||
The `isolate_workspace` autouse fixture in `tests/conftest.py` (added in the May 2026 docs refresh work) is the foundation for the migration pattern.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Audit all `tests/*.py` for direct path references to `./` TOML files
|
||||
- Migrate offenders to use `tmp_path` + `monkeypatch` (or `isolate_workspace`)
|
||||
- Consolidate similar tests where it improves clarity (judgment call)
|
||||
- Add a `tests/conftest.py` autouse fixture that prevents regression
|
||||
- Add a `scripts/check_test_toml_paths.py` script for CI/pre-commit
|
||||
- Add tests for the enforcement mechanism itself
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Rewriting tests for clarity (only consolidation where it improves maintainability)
|
||||
- Adding new tests
|
||||
- Changing the test runner (pytest stays)
|
||||
- Coverage tooling changes
|
||||
|
||||
---
|
||||
|
||||
## Design
|
||||
|
||||
### Phase 1: Audit
|
||||
|
||||
A script that greps `tests/*.py` for problematic patterns:
|
||||
|
||||
```python
|
||||
# scripts/check_test_toml_paths.py
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
PROBLEMATIC_PATTERNS = [
|
||||
r'Path\("(?:manual_slop|config|credentials|presets|personas|tool_presets|workspace_profiles)\.toml"\)',
|
||||
r'open\(["\'](?:manual_slop|config|credentials|presets|personas|tool_presets|workspace_profiles)\.toml["\']',
|
||||
r'["\']\.{1,2}/(?:manual_slop|config|credentials|presets|personas|tool_presets|workspace_profiles)\.toml["\']',
|
||||
]
|
||||
|
||||
def find_violations(tests_dir: Path) -> List[Tuple[Path, int, str]]:
|
||||
"""Returns list of (file, line, pattern) for each violation."""
|
||||
...
|
||||
```
|
||||
|
||||
Run this script as the first step. Output a report grouped by file.
|
||||
|
||||
### Phase 2: Migrate Offenders
|
||||
|
||||
For each violation, refactor the test to use the sandboxed pattern:
|
||||
|
||||
**Before (real TOML):**
|
||||
```python
|
||||
def test_load_presets():
|
||||
path = Path("presets.toml") # Real file!
|
||||
if path.exists():
|
||||
data = tomllib.loads(path.read_text())
|
||||
assert data is not None
|
||||
```
|
||||
|
||||
**After (sandboxed):**
|
||||
```python
|
||||
def test_load_presets(tmp_path):
|
||||
path = tmp_path / "presets.toml"
|
||||
path.write_text("[presets.test]\nkey = 'value'\n")
|
||||
# Patch the path module to point to tmp_path
|
||||
monkeypatch = pytest.MonkeyPatch()
|
||||
monkeypatch.setattr("src.paths.get_global_presets_path", lambda: path)
|
||||
data = tomllib.loads(path.read_text())
|
||||
assert data["presets"]["test"]["key"] == "value"
|
||||
monkeypatch.undo()
|
||||
```
|
||||
|
||||
Or use the `isolate_workspace` autouse fixture (already in conftest.py) which redirects all path resolution to `tmp_path`.
|
||||
|
||||
### Phase 3: Consolidate (Judgment Call)
|
||||
|
||||
Examples of consolidation opportunities (NOT a forced refactor):
|
||||
|
||||
| Current | Proposed | Rationale |
|
||||
|---|---|---|
|
||||
| `test_ai_settings_layout.py` + `test_sim_ai_settings.py` | `test_ai_settings.py` with parametrize | Tests cover same surface |
|
||||
| `test_*_provider.py` (5+ files) | `test_providers.py` parametrized | Each provider test has same shape |
|
||||
| `test_*_preset*.py` (3 files) | `test_presets.py` with class organization | Settings/presets/tools all CRUD TOML |
|
||||
| `test_*_screenshot*.py` | `test_screenshots.py` | Currently fragmented |
|
||||
|
||||
Each consolidation is reviewed case-by-case. **Test count is not a goal; test clarity is.** Don't merge tests that test different things just to reduce file count.
|
||||
|
||||
### Phase 4: Enforce
|
||||
|
||||
**4a. Autouse fixture** in `tests/conftest.py`:
|
||||
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def enforce_no_real_toml(monkeypatch, tmp_path):
|
||||
"""Prevents any test from reading ./<name>.toml by detecting file existence
|
||||
and asserting the path is inside tmp_path or explicitly monkeypatched."""
|
||||
|
||||
real_toml_paths = [
|
||||
Path("manual_slop.toml"),
|
||||
Path("config.toml"),
|
||||
Path("credentials.toml"),
|
||||
Path("presets.toml"),
|
||||
Path("personas.toml"),
|
||||
Path("tool_presets.toml"),
|
||||
Path("workspace_profiles.toml"),
|
||||
]
|
||||
|
||||
# If any real TOML exists in the cwd, save it for restoration
|
||||
snapshots = {}
|
||||
for p in real_toml_paths:
|
||||
if p.exists():
|
||||
snapshots[p] = p.read_bytes()
|
||||
p.unlink() # Remove to prevent test from reading
|
||||
yield # Run the test
|
||||
# Restore after test
|
||||
for p, content in snapshots.items():
|
||||
p.write_bytes(content)
|
||||
```
|
||||
|
||||
This is **strict** — any test that tries to read a real TOML will get FileNotFoundError. Tests must use `tmp_path` or `monkeypatch`.
|
||||
|
||||
If this is too aggressive, a softer alternative:
|
||||
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def warn_on_real_toml():
|
||||
"""Warns if a test reads a real TOML. Does not fail by default;
|
||||
set ENFORCE_NO_REAL_TOML=1 to convert warnings to failures."""
|
||||
...
|
||||
```
|
||||
|
||||
**4b. CI script** `scripts/check_test_toml_paths.py` — runs on every commit:
|
||||
|
||||
```python
|
||||
# Greps for direct ./<name>.toml references
|
||||
# Exits non-zero if any found
|
||||
# Output: "test_foo.py:42: Path('presets.toml') — direct reference to real TOML"
|
||||
```
|
||||
|
||||
Add to `conductor/...` workflow or as a pre-commit hook (out of scope for this track — just provide the script).
|
||||
|
||||
### Phase 5: Test the Enforcer
|
||||
|
||||
`tests/test_enforce_no_real_toml.py` — meta-test:
|
||||
|
||||
```python
|
||||
def test_enforcer_catches_violation(tmp_path, monkeypatch):
|
||||
"""Verify the fixture prevents reading a real TOML."""
|
||||
# Create a real-looking TOML in cwd
|
||||
real_path = Path("test_enforcer_temp.toml")
|
||||
real_path.write_text("[test]\nkey='value'")
|
||||
try:
|
||||
# The fixture removes it; try to read it
|
||||
with pytest.raises(FileNotFoundError):
|
||||
real_path.read_text()
|
||||
finally:
|
||||
if real_path.exists():
|
||||
real_path.unlink()
|
||||
|
||||
def test_enforcer_restores_real_tomls(tmp_path):
|
||||
"""Verify the fixture restores real TOMLs after the test."""
|
||||
real_path = Path("test_enforcer_temp2.toml")
|
||||
original = b"[test]\nkey='original'"
|
||||
real_path.write_bytes(original)
|
||||
# The test runs (fixture activates)
|
||||
assert real_path.exists() # The fixture restored it
|
||||
assert real_path.read_bytes() == original
|
||||
real_path.unlink()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- `scripts/check_test_toml_paths.py` — NEW: greps for violations, exits non-zero
|
||||
- `tests/conftest.py` — MODIFY: add `enforce_no_real_toml` autouse fixture (strict or warn-only)
|
||||
- `tests/test_enforce_no_real_toml.py` — NEW: tests for the enforcer
|
||||
- Various `tests/test_*.py` — MODIFY: migrate offenders to sandboxed pattern
|
||||
- Various `tests/test_*.py` — MODIFY: consolidate where it improves clarity
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- All existing tests pass after migration
|
||||
- `scripts/check_test_toml_paths.py` exits 0 on the test suite after migration
|
||||
- The autouse fixture catches new violations in CI
|
||||
- Test count is approximately the same after consolidation (slight decrease acceptable)
|
||||
- No real TOML files in the user's project are touched by the test suite
|
||||
|
||||
---
|
||||
|
||||
## Risks
|
||||
|
||||
1. **Test breakage:** Migration may break tests that depend on real-file behavior. Mitigation: run full test suite after each migration batch.
|
||||
2. **Performance:** The autouse fixture adds overhead to every test. Mitigation: keep it cheap (just snapshot/restore file existence).
|
||||
3. **Coverage regression:** Removing real-file behavior may hide bugs. Mitigation: add explicit tests for the sandboxed path resolution.
|
||||
@@ -1,277 +0,0 @@
|
||||
# UI Polish Track — Design Spec
|
||||
|
||||
**Date:** 2026-06-03
|
||||
**Status:** Approved for implementation
|
||||
**Author:** Tier 2 Tech Lead
|
||||
**Scope:** Five discrete UI quality-of-life fixes identified during user review of the GUI.
|
||||
**Related tracks:** `gui_2_cleanup_20260513`, `selectable_ui_text_20260308`, `context_comp_decouple_20260510`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
User review surfaced five independent UI defects / quality issues that have been outstanding for some time and that previous agent attempts have not been able to fully resolve. This spec decomposes them into five phases of a single track. Each phase is independently shippable and produces visible improvement.
|
||||
|
||||
| # | Severity | Issue | Prior attempts |
|
||||
|---|----------|-------|----------------|
|
||||
| 1 | High | Markdown tables in `imgui_md` are rendered as run-on text — column boundaries lost, headers indistinguishable from body rows. | Multiple agents reported attempted fixes; the underlying limitation is that `imgui-bundle`'s `imgui_md` does not implement GFM tables. |
|
||||
| 2 | High | The `Keep Pairs` numeric input next to `Truncate` in the discussion panel is clipped to 80 px — single-digit values are visibly truncated. | One-line width fix. |
|
||||
| 3 | High | The `Refresh Registry` button in Log Management instantiates a new `LogRegistry` but does not call `.load_registry()`, so the displayed table never reflects on-disk state. | One-line fix. |
|
||||
| 4 | Medium | Operations Hub has no per-vendor session state view (quota, context-window usage, last-error class). Existing "Usage Analytics" tab shows historical aggregates only. | New panel. |
|
||||
| 5 | Medium | Files & Media > Files shows a flat sorted table; the user wants directory-grouped collapsible tree nodes matching the Context Composition visual style. | Refactor mirroring `render_context_files_table` pattern. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture Overview
|
||||
|
||||
All five phases target the rendering layer only. No new infrastructure, no threading changes, no provider API changes. The track stays inside the ImGui immediate-mode model and the existing `markdown_helper.py` / `aggregate.py` / `log_registry.py` modules.
|
||||
|
||||
### 2.1 Phase Boundaries
|
||||
|
||||
```
|
||||
Phase 1 ─ Markdown table pre-processor
|
||||
Phase 2 ─ Discussion Truncate / Keep Pairs layout fix
|
||||
Phase 3 ─ Log Management refresh bug fix
|
||||
Phase 4 ─ Operations Hub > Vendor State panel
|
||||
Phase 5 ─ Files & Media > Files directory tree
|
||||
```
|
||||
|
||||
Each phase is one track-level task with its own Red/Green/Commit cycle. Phase 1 is the only one with non-trivial complexity (it introduces a new sub-module). Phases 2, 3, 5 are surgical. Phase 4 adds a new sub-component.
|
||||
|
||||
### 2.2 Shared Conventions
|
||||
|
||||
- All Python code uses 1-space indentation (per `conductor/code_styleguides/python.md`).
|
||||
- No new comments in source files (per `AGENTS.md`).
|
||||
- All new render functions live at module level in `src/gui_2.py` and follow the `(app: App) -> None` signature, with a thin `_render_vendor_state(self)` wrapper on `App` if they need to be hot-reload-able.
|
||||
- SDM dependency tags required on all new public functions.
|
||||
- Tests live in `tests/test_<module>.py` and follow the existing `live_gui` fixture pattern when they need real GUI state.
|
||||
- Branch coverage target: ≥ 80 % for new code; full regression for the touched panel.
|
||||
|
||||
---
|
||||
|
||||
## 3. Per-Phase Design
|
||||
|
||||
### Phase 1 — Markdown Table Pre-Processor
|
||||
|
||||
**Problem:** `imgui-bundle`'s `imgui_md` (vendored as `src/imgui_md`) does not implement GFM table syntax. Lines like:
|
||||
|
||||
```
|
||||
| Name | Type |
|
||||
|-------|------|
|
||||
| foo | int |
|
||||
```
|
||||
|
||||
render as a single run of `|` characters with no column alignment, no header/body distinction, and no border. The user has reported this as a recurring frustration for months.
|
||||
|
||||
**Why previous fixes failed:** Attempts to monkey-patch `imgui_md` or to convert tables to ASCII pre-formatted blocks lose interactivity (links inside cells, syntax highlighting of code cells) and look bad in high-density themes like NERV.
|
||||
|
||||
**Approach:** Insert a GFM-table interceptor into `MarkdownRenderer.render()` that runs *before* `imgui_md.render()`. The interceptor:
|
||||
|
||||
1. Detects table blocks via the GFM signature: a line of `|`-delimited cells followed by a separator line containing only `|`, `-`, `:`, and spaces.
|
||||
2. Computes the natural width of each column by measuring rendered text (using `imgui.calc_text_size`) — this is what makes it "data-oriented" rather than string-length-based.
|
||||
3. Renders the table via `imgui.begin_table()` with one column per logical column, headers via `imgui.table_headers_row()`, and body rows via `imgui.table_next_row()` / `imgui.table_set_column_index()`.
|
||||
4. Recursively delegates any markdown *inside* cell content (links, emphasis, inline code) to `imgui_md.render()` per cell.
|
||||
5. Falls back to the existing `imgui_md.render()` pass if a block is detected as table-shaped but fails sanity checks (e.g. zero columns, separator with no body).
|
||||
|
||||
**Module boundary:** New module `src/markdown_table.py` exports a single function `render_markdown_tables(text: str) -> str` that returns the text with tables replaced by an inert placeholder (`\x00TABLE_<idx>\x00`), plus a list of table specs. The actual rendering stays inside `MarkdownRenderer` because it needs an ImGui context.
|
||||
|
||||
**Files touched:**
|
||||
- New: `src/markdown_table.py` — pure parser, no ImGui imports.
|
||||
- Modify: `src/markdown_helper.py` — `MarkdownRenderer.render` intercepts and dispatches.
|
||||
- New tests: `tests/test_markdown_table.py` (parser unit tests), `tests/test_markdown_table_render.py` (live_gui render tests).
|
||||
|
||||
**Safety:** The interceptor only activates when a block matches the GFM table shape; everything else passes through unchanged. The placeholder scheme is robust to nested `imgui_md` quirks because we substitute placeholder *after* `imgui_md` has finished its pass.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- A representative 4-column, 3-row table renders with aligned borders, bold header, and proper column widths.
|
||||
- Tables inside code fences (```` ``` ````) are NOT touched.
|
||||
- Inline code / links inside cells still render correctly.
|
||||
- Performance: rendering a 5-table, 50-row document is < 16 ms (one frame budget).
|
||||
- Existing markdown tests still pass.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — Truncate / Keep Pairs Input Width
|
||||
|
||||
**Problem:** `src/gui_2.py:3829`:
|
||||
|
||||
```python
|
||||
imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80)
|
||||
ch, app.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", app.ui_disc_truncate_pairs, 1)
|
||||
```
|
||||
|
||||
The width of 80 px is too narrow for a 2–3 digit number. Image evidence: the digit `2` is partially cut off on the right border.
|
||||
|
||||
**Fix:** Increase `set_next_item_width` from 80 to 140 (matches the width of the adjacent `Truncate` button + spacing). Additionally, switch from `imgui.input_int` to `imgui.drag_int` for the same field — this gives the user the `+/-` stepper buttons inline without needing the `same_line` button pair, and prevents the digit-clipping behavior of `input_int` when the value approaches the field width.
|
||||
|
||||
**Files touched:**
|
||||
- Modify: `src/gui_2.py` — single line at 3829 plus the surrounding `same_line()` chain.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- 3-digit values (`999`) render fully inside the input.
|
||||
- The `Truncate` button remains clickable and aligned on the same row.
|
||||
- `ui_disc_truncate_pairs` still floors at 1.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — Log Management `Refresh Registry` Bug
|
||||
|
||||
**Problem:** `src/gui_2.py:1675`:
|
||||
|
||||
```python
|
||||
if imgui.button("Refresh Registry"):
|
||||
app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
|
||||
```
|
||||
|
||||
The new `LogRegistry` instance is constructed (which opens the file) but `.load_registry()` is never called, so `app._log_registry.data` stays empty. The user sees the same stale table.
|
||||
|
||||
**Fix:** Two acceptable shapes:
|
||||
|
||||
**A — In-place reload (preferred):**
|
||||
```python
|
||||
if imgui.button("Refresh Registry"):
|
||||
if app._log_registry is not None:
|
||||
app._log_registry.load_registry()
|
||||
```
|
||||
|
||||
**B — Re-instantiate + load (defensive):**
|
||||
```python
|
||||
if imgui.button("Refresh Registry"):
|
||||
app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
|
||||
app._log_registry.load_registry()
|
||||
```
|
||||
|
||||
We pick **A** because it preserves any in-memory state (e.g. a pending `update_session_metadata` call) and is what the user likely meant. We add `load_registry` to `LogRegistry`'s public API (it already exists privately at lines 75-103 — we just stop reading the wrong attribute).
|
||||
|
||||
**Files touched:**
|
||||
- Modify: `src/gui_2.py` — single line at 1675.
|
||||
- New test: `tests/test_log_management_refresh.py` — drives a temp registry, calls the button via live_gui or via direct function call, asserts table count increases.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Pressing the button updates the visible table when the on-disk TOML has changed.
|
||||
- No regression to existing log_management tests.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — Operations Hub > Vendor State Panel
|
||||
|
||||
**Problem:** The current Operations Hub has tabs for Comms / Tool Calls / Usage Analytics / External Tools / Workspace Layouts. There is no at-a-glance view of "what is the current vendor's session state?" — things like:
|
||||
- Current provider + model.
|
||||
- Context-window utilization (used / limit, percentage bar).
|
||||
- Cache hit rate for the active session.
|
||||
- Last error class (if any).
|
||||
- Quota state (when the vendor exposes it; Anthropic and Gemini CLI expose different signals).
|
||||
|
||||
**Approach:** Add a new tab `Vendor State` between `Usage Analytics` and `External Tools`. The tab shows a single high-density `imgui.begin_table()` with one row per tracked metric. The metrics are pulled from a new module-level helper `get_vendor_state(app: App) -> list[VendorMetric]` that aggregates from:
|
||||
- `app.current_provider`, `app.current_model` (from `models.PROVIDERS`).
|
||||
- `app.controller.token_tracker` for used / limit / cache stats.
|
||||
- `app.controller.last_error` for error class.
|
||||
- A new `app.controller.vendor_quota` (lazy-loaded dict) populated by `ai_client` when the provider returns a quota-bearing response.
|
||||
|
||||
**Component shape:**
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class VendorMetric:
|
||||
key: str # e.g. "context_window"
|
||||
label: str # e.g. "Context Window"
|
||||
value: str # e.g. "78,234 / 200,000 (39%)"
|
||||
state: str # "ok" | "warn" | "error" | "info"
|
||||
tooltip: str # long-form explanation, shown on hover
|
||||
```
|
||||
|
||||
The new tab is a thin renderer:
|
||||
```python
|
||||
def render_vendor_state(app: App) -> None:
|
||||
metrics = get_vendor_state(app)
|
||||
if imgui.begin_table("vendor_state", 3, imgui.TableFlags_.row_bg | imgui.TableFlags_.borders):
|
||||
imgui.table_setup_column("Metric", imgui.TableColumnFlags_.width_fixed, 180)
|
||||
imgui.table_setup_column("Value", imgui.TableColumnFlags_.width_stretch)
|
||||
imgui.table_setup_column("State", imgui.TableColumnFlags_.width_fixed, 60)
|
||||
imgui.table_headers_row()
|
||||
for m in metrics:
|
||||
...
|
||||
if imgui.is_item_hovered(): imgui.set_tooltip(m.tooltip)
|
||||
```
|
||||
|
||||
**Files touched:**
|
||||
- New: `src/vendor_state.py` — pure aggregator, no ImGui imports.
|
||||
- Modify: `src/gui_2.py` — add `render_vendor_state`, new tab in `render_operations_hub`.
|
||||
- Modify: `src/app_controller.py` — add `vendor_quota: dict[str, Any] = field(default_factory=dict)` and a `set_vendor_quota(provider, payload)` callback.
|
||||
- Modify: `src/ai_client.py` — call `set_vendor_quota` on quota-bearing responses (Anthropic `usage` blocks, Gemini `metadata.tokenInfo`, DeepSeek rate-limit headers).
|
||||
- New tests: `tests/test_vendor_state.py` (aggregator logic), `tests/test_vendor_state_render.py` (live_gui rendering).
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Tab appears and is the default-open tab when Operations Hub is opened.
|
||||
- All four metric categories (provider/model, context window, cache, last error, quota) render with stable keys.
|
||||
- Missing data renders as `—` (em dash), not as a crash.
|
||||
- No regression in `live_gui` test suite.
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 — Files & Media > Files Directory Tree
|
||||
|
||||
**Problem:** `src/gui_2.py:2689` `render_files_and_media` renders `app.files` as a flat 3-column table (Act / Path / Status), sorted alphabetically. This is hard to scan for a user with 50+ files. The user wants the directory-grouped, collapsible tree node style used in `render_context_files_table` (lines 3111-3260).
|
||||
|
||||
**Approach:** Reuse the existing `aggregate.group_files_by_dir()` helper. For each directory key returned, render a `tree_node_ex(..., default_open)` with the directory name as the label, then iterate the files under it as the leaf rows. The 3-column layout (Act / Path / Status) is preserved at the leaf level.
|
||||
|
||||
**Component shape:**
|
||||
```python
|
||||
def render_files_and_media(app: App) -> None:
|
||||
if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open):
|
||||
with imscope.group():
|
||||
grouped = aggregate.group_files_by_dir(app.files)
|
||||
for dir_name, g_files in sorted(grouped.items()):
|
||||
with imscope.tree_node_ex(f"{dir_name}##files_dir", imgui.TreeNodeFlags_.default_open) as is_open:
|
||||
if is_open:
|
||||
# ... existing per-file row logic, with all `i` indices scoped to the directory
|
||||
# ... existing "Add Files to Inventory" button
|
||||
```
|
||||
|
||||
**Files touched:**
|
||||
- Modify: `src/gui_2.py:2689-2750` — wrap the inner per-file loop in a directory group loop.
|
||||
- New test: `tests/test_files_and_media_tree.py` — asserts that two files in different directories render with the directory labels visible, and that `tree_node` open/closed state is preserved across frames.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Two files in the same directory are grouped under one collapsible node.
|
||||
- One-file "directories" still render (no special-case).
|
||||
- The Act / Path / Status columns still function (Add, Remove, Active / Cached / disabled).
|
||||
- No regression in `tests/test_gui_fast_render.py::test_render_files_and_media_fast`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Cross-Cutting Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Phase 1 markdown change regresses existing markdown rendering (communications, log previews, discussion responses). | All `live_gui` tests for those panels must pass before merging Phase 1. Add a snapshot test that hashes a fixed multi-table markdown input. |
|
||||
| Phase 4 `vendor_quota` thread-safety — providers fire callbacks on background threads. | The new `set_vendor_quota` is a pure field write under the controller's existing lock; `get_vendor_state` reads under the same lock. Document in SDM tag. |
|
||||
| Phase 5 changes the row identity for the `+` and `x` buttons — indices must be globally unique across all directories, not per-directory. | The current code uses `i = enumerate(app.files)`; we preserve the original global index for button IDs by computing it via `app.files.index(f_item)` once per iteration. |
|
||||
| ImGui scope mismatches introduced by the new `tree_node_ex` blocks. | Run `scripts/check_imgui_scopes.py` after each phase. |
|
||||
| Plan exceeds one track's worth of work; some phases might be deferred. | Each phase is independently shippable and self-contained; we will checkpoint after each phase rather than at the end. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing Strategy
|
||||
|
||||
- **Unit tests** for every new module (`markdown_table`, `vendor_state`) — pure parser/aggregator logic, no ImGui context needed.
|
||||
- **live_gui tests** for any change that touches `gui_2.py` render functions. Use the session-scoped `live_gui` fixture from `tests/conftest.py`.
|
||||
- **Regression** — full targeted batch of `tests/test_gui_fast_render.py`, `tests/test_log_management_ui.py`, `tests/test_markdown_*.py`, `tests/test_discussion_hub_*.py` after each phase.
|
||||
- **No new `unittest.mock.patch`** on core infrastructure (per the Structural Testing Contract).
|
||||
|
||||
---
|
||||
|
||||
## 6. Rollout
|
||||
|
||||
Phases are checkpointed independently:
|
||||
|
||||
```
|
||||
Phase 1 ─ checkpoint ─ Phase 2 ─ checkpoint ─ Phase 3 ─ checkpoint ─ Phase 4 ─ checkpoint ─ Phase 5 ─ final
|
||||
```
|
||||
|
||||
Each checkpoint:
|
||||
1. Run targeted test batch.
|
||||
2. Spawn live_gui in headless mode and confirm a smoke screenshot of the touched panel.
|
||||
3. Attach a git note with the verification report.
|
||||
4. Update `conductor/tracks.md` with the phase SHA.
|
||||
|
||||
If a phase is approved out-of-order, the others remain independently executable. There are no inter-phase dependencies (Phase 1, 2, 3, 5 do not depend on Phase 4; Phase 4 does not depend on any of them).
|
||||
@@ -1,104 +0,0 @@
|
||||
# Theme & Syntax Highlighting Modularization
|
||||
|
||||
## Problem
|
||||
|
||||
The current theming system in `src/theme_2.py` has three limitations:
|
||||
|
||||
1. **Themes are hardcoded as a Python dict.** Users cannot author new themes without editing Python source and recompiling. This is inconsistent with the rest of the project (presets, personas, tool_presets, context_presets, bias profiles, workspace profiles all use TOML).
|
||||
|
||||
2. **Syntax highlighting is hardcoded.** The `MarkdownRenderer._lang_map` in `src/markdown_helper.py` uses `imgui-bundle`'s `imgui_color_text_edit` language definitions whose token colors are baked into the C++ library. There is no way to align syntax token colors with the active UI theme.
|
||||
|
||||
3. **No way to bundle new themes with a release or share them between projects.**
|
||||
|
||||
## Goals
|
||||
|
||||
- **TOML-based theme authoring.** Themes live in `themes/<name>.toml` (global) and `<project>/project_themes.toml` (project override). Schema mirrors the existing `_PALETTES` dict shape.
|
||||
|
||||
- **Authoring without recompiling.** Drop a new `.toml` file in `themes/` and it appears in the palette selector after the next load (or hot-reload, future).
|
||||
|
||||
- **Syntax palette mapping.** Each theme TOML declares a `syntax_palette` field that maps to one of the four built-in `imgui_color_text_edit` palettes (`dark`, `light`, `mariana`, `retro_blue`). The renderer calls `editor.set_default_palette(...)` whenever the active theme changes.
|
||||
|
||||
- **Scope-based merging** matches the existing pattern: project themes override global themes with the same name.
|
||||
|
||||
## Constraints
|
||||
|
||||
- `imgui-bundle` only ships 4 built-in syntax palettes and exposes no API to define new ones or override individual token colors. This is a hard upstream limit. The plan accepts the limit and works around it via palette mapping.
|
||||
|
||||
- We do NOT attempt to wrap or shadow `imgui_color_text_edit`. The C++ library owns the per-language token regexes and default token colors. We pick the closest of the 4 palettes for each theme and let users override the mapping per theme.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Defining new `imgui_color_text_edit` palettes or overriding token colors per language (blocked by upstream API).
|
||||
- Hot-reload of theme changes (the user can re-apply from the selector).
|
||||
- Per-language color customization (e.g., Python `keyword` color distinct from C `keyword`).
|
||||
|
||||
## File structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/theme_2.py` | Modify | Replace hardcoded `_PALETTES` dict with a load-from-TOML pipeline. Keep `apply()` public API. Expose new helpers `get_syntax_palette_for_theme(name)` and `apply_syntax_palette(palette_id)`. |
|
||||
| `src/paths.py` | Modify | Add `get_global_themes_path()` and `get_project_themes_path(project_root)`. Defaults: `themes.toml` (global) and `project_themes.toml` (project). Override via `SLOP_GLOBAL_THEMES` env var. |
|
||||
| `src/theme_models.py` | Create | Pydantic/dataclass schema for theme TOML files. `ThemePalette` has all `imgui.Col_` keys, `syntax_palette` is a string (one of the 4 IDs). `to_dict()` / `from_dict()` round-trip. |
|
||||
| `themes/solarized_dark.toml` | Create | Authoring artifact. RGB triples in standard `#RRGGBB` form. |
|
||||
| `themes/solarized_light.toml` | Create | Same. |
|
||||
| `themes/gruvbox_dark.toml` | Create | Same. |
|
||||
| `themes/moss.toml` | Create | Same. |
|
||||
| `tests/test_theme_models.py` | Create | Round-trip tests for `ThemePalette` from/to TOML. |
|
||||
| `tests/test_theme.py` | Modify | Add tests for the 4 new palettes, TOML loading, scope merge, and syntax palette mapping. |
|
||||
| `tests/fixtures/themes/minimal.toml` | Create | Minimal valid TOML fixture for loader tests. |
|
||||
| `tests/fixtures/themes/missing_keys.toml` | Create | TOML missing required keys — should raise a clear error. |
|
||||
| `docs/guide_themes.md` | Create | Authoring guide: schema, file locations, scope rules, syntax palette mapping, env vars. |
|
||||
|
||||
## Theme TOML schema (reference, not implementation in this plan)
|
||||
|
||||
```toml
|
||||
# theme name (informational)
|
||||
name = "Solarized Dark"
|
||||
|
||||
# optional: which built-in imgui_color_text_edit palette to use
|
||||
# one of: dark | light | mariana | retro_blue
|
||||
syntax_palette = "dark"
|
||||
|
||||
# which imgui style colors this theme overrides
|
||||
# any key not listed falls back to the base imgui dark/light defaults
|
||||
[colors]
|
||||
window_bg = [ 0, 43, 54] # 0x002b36 base03
|
||||
child_bg = [ 7, 54, 66] # 0x073642 base02
|
||||
text = [147, 161, 161] # 0x93a1a1 base1
|
||||
text_disabled = [ 88, 110, 117] # 0x586e75 base01
|
||||
button_hovered = [ 38, 139, 210] # 0x268bd2 blue
|
||||
check_mark = [ 38, 139, 210]
|
||||
slider_grab = [ 38, 139, 210]
|
||||
tab_selected = [ 88, 110, 117]
|
||||
tab_hovered = [ 38, 139, 210]
|
||||
# ... remaining colors omitted
|
||||
```
|
||||
|
||||
Values are 3-element RGB arrays (0-255) for the body and the syntax palette is a string identifier.
|
||||
|
||||
## Syntax palette mapping (built-in only)
|
||||
|
||||
| Theme | Syntax palette |
|
||||
|---|---|
|
||||
| Solarized Dark | `dark` (closest dark base) |
|
||||
| Solarized Light | `light` |
|
||||
| Gruvbox Dark | `retro_blue` (warm retro feel) |
|
||||
| Moss | `mariana` (deep blue-green base) |
|
||||
| 10x Dark | `dark` |
|
||||
| Nord Dark | `dark` |
|
||||
| Monokai | `dark` |
|
||||
| Binks | `light` |
|
||||
| ImGui Dark | `dark` |
|
||||
| NERV | `dark` (NERV's own custom palette via `theme_nerv.apply_nerv()`) |
|
||||
|
||||
The mapping lives in `src/theme_2.py` as a small dict and is overridable per theme via the TOML `syntax_palette` field.
|
||||
|
||||
## Public API
|
||||
|
||||
Existing `src.theme_2` callsites must continue to work. New surface:
|
||||
|
||||
- `theme.get_palette_names() -> list[str]` — already exists, now also returns TOML-loaded themes
|
||||
- `theme.apply(name) -> None` — already exists, applies the named theme (built-in OR TOML)
|
||||
- `theme.get_syntax_palette_for_theme(name) -> PaletteId` — new
|
||||
- `theme.apply_syntax_palette(palette_id) -> None` — new, calls `editor.set_default_palette(palette_id)`
|
||||
- `theme.load_themes_from_disk() -> None` — new, public for hot-reload
|
||||
@@ -1,251 +0,0 @@
|
||||
# Live-GUI Fragility Fixes — Design
|
||||
|
||||
**Date:** 2026-06-05
|
||||
**Status:** Draft
|
||||
**Track follow-up to:** regression_fixes_20260605
|
||||
**Scope:** Fix 3 failing live_gui tests discovered in the 2026-06-05 batched test run, harden the defer-not-catch pattern doc, restore 100% pass rate on the 272-file test suite.
|
||||
|
||||
## 1. Background
|
||||
|
||||
### Scope decisions (per user review 2026-06-05)
|
||||
- Change 1 (the `b""` → `""` fix): **in scope, critical path.**
|
||||
- Change 2 (test mock fix for prior session test): **SCOPE REDUCED during execution.** The test was more under-mocked than the spec assumed. Initial error at `src/gui_2.py:2333` (imscope.window tuple unpack) was the first of several un-mocked dependencies. After fixing imscope.window, the next failure surfaces at `src/gui_2.py:4496` (render_theme_panel: imgui.begin returning bool where 2-tuple expected). The test calls `render_main_interface` which is a kitchen-sink function requiring 50+ mocks. **Decision: defer Change 2 to a separate follow-up track** that focuses on refactoring the test to either (a) exercise a narrow prior-session render path instead of `render_main_interface`, or (b) add the missing 50+ mocks. The imscope.window fix is still applied as a defensive change (and as a model for future test work).
|
||||
- Change 3 (regression unit test): **in scope, critical path.**
|
||||
- Change 4 (doc hardening of defer-not-catch sections): **DEFERRED to end of track** — user wants to see how long the critical path takes first. If time permits at the end, do Change 4 as a final commit; otherwise leave for a follow-up patch.
|
||||
|
||||
### Revised pass-rate target
|
||||
- Before track: 269/272 (98.9%)
|
||||
- After Change 1: 271/272 (99.6%) — both `test_auto_switch_sim` and `test_workspace_profiles_restoration` should pass; `test_prior_session_no_pop_imbalance` is deferred to a follow-up.
|
||||
- After Change 3: 272/272 if Change 2 also fixed, else 271/272 + new regression unit test passes.
|
||||
|
||||
### Follow-up track: prior_session_test_harden_20260605
|
||||
A new track to be queued in `conductor/tracks.md` covering the `test_prior_session_no_pop_imbalance` test's comprehensive mock setup (or refactor to test a narrow path).
|
||||
|
||||
### Failures (3)
|
||||
|
||||
| Test | File | Symptom | Root cause |
|
||||
|---|---|---|---|
|
||||
| `test_auto_switch_sim` | `tests/test_auto_switch_sim.py:47` | `assert False == True` after triggering tier-3 auto-switch | Category A: profile save raises TypeError → no profile saved → load is no-op |
|
||||
| `test_workspace_profiles_restoration` | `tests/test_workspace_profiles_sim.py:81` | `assert False is True` after `load_workspace_profile` | Category A: same as above |
|
||||
| `test_no_extraneous_pop_when_prior_session_renders` | `tests/test_prior_session_no_pop_imbalance.py:135` | `TypeError: cannot unpack non-iterable NoneType object` at `src/gui_2.py:2333` | Category B: test mock setup for `imscope.window` returns non-iterable, but production code expects `(opened, visible)` tuple |
|
||||
|
||||
### Test run results (2026-06-05, batched via `scripts/run_tests_batched.py`)
|
||||
|
||||
- **272 test files, 68 batches, 269/272 passing (98.9%).**
|
||||
- 3 failing tests, all in `live_gui` (session-scoped fixture) or `integration` marker category.
|
||||
- 0 failing tests in any other category (unit, headless, mock_app, simulation).
|
||||
|
||||
### Root cause analysis (Category A — both profile failures)
|
||||
|
||||
A regression introduced by commit `d7487af4` ("fix(gui_2): defer save_ini_settings on first capture to avoid early-render crash"). That commit added a defer-not-catch guard in `_capture_workspace_profile` (`src/gui_2.py:601-606`):
|
||||
|
||||
```python
|
||||
def _capture_workspace_profile(self, name: str) -> models.WorkspaceProfile:
|
||||
if not getattr(self, "_ini_capture_ready", False):
|
||||
self._ini_capture_ready = True
|
||||
ini = b"" # <-- BUG: bytes, not str
|
||||
else:
|
||||
try:
|
||||
ini = imgui.save_ini_settings_to_memory() # returns str
|
||||
except Exception:
|
||||
ini = b"" # <-- BUG: same
|
||||
...
|
||||
```
|
||||
|
||||
The bug: `ini = b""` is a `bytes` literal, but the `WorkspaceProfile` dataclass declares `ini_content: str` (`src/models.py:799`), AND `tomli_w` (the TOML serializer) raises `TypeError: Object of type 'bytes' is not TOML serializable`.
|
||||
|
||||
Verified empirically:
|
||||
```python
|
||||
>>> import tomli_w
|
||||
>>> tomli_w.dump({"ini_content": b""}, io.BytesIO())
|
||||
TypeError: Object of type 'bytes' is not TOML serializable
|
||||
```
|
||||
|
||||
Trace path for the failure:
|
||||
1. Test: `set_value('ui_separate_tier1', True)` → field is `True` in app state.
|
||||
2. Test: `push_event("custom_callback", {"callback": "save_workspace_profile", ...})`.
|
||||
3. GUI: `_process_pending_gui_tasks` → `_cb_save_workspace_profile` (`src/app_controller.py:2870`).
|
||||
4. App: `_capture_workspace_profile(name)` → returns `WorkspaceProfile(..., ini_content=b"", ...)`.
|
||||
5. `workspace_manager.save_profile(profile)` → `profile.to_dict()` → `{"ini_content": b"", ...}`.
|
||||
6. `_save_file` → `tomli_w.dump(data, f)` → **TypeError raised**.
|
||||
7. Exception propagates; profile is **NOT saved to disk**; `workspace_profiles` is **NOT reloaded**; `self._app.workspace_profiles` is **NOT updated**.
|
||||
8. Test: `set_value('ui_separate_tier1', False)` → field is `False`.
|
||||
9. Test: `push_event("custom_callback", {"callback": "load_workspace_profile", ...})`.
|
||||
10. App: `_cb_load_workspace_profile(name)` → `if name in self.workspace_profiles:` → `False` (save failed) → **does nothing**.
|
||||
11. Test: `assert get_value('ui_separate_tier1') is True` → **fails** (still `False`).
|
||||
|
||||
The original pre-defer code (`ini = imgui.save_ini_settings_to_memory()`) returned a `str` that round-tripped through TOML successfully; tests passed. The defer fix introduced a type-incompatible sentinel value that broke the serialization contract.
|
||||
|
||||
The 1-line fix: change `ini = b""` to `ini = ""` (and add a defensive str-coerce for the non-defer path).
|
||||
|
||||
### Root cause analysis (Category B — prior session test)
|
||||
|
||||
The test mocks `imscope.window(...)` to return a `MagicMock()` whose `__enter__` returns the bare mock. Production code at `src/gui_2.py:2333` does `with imscope.window(...) as (opened, visible):` which expects a 2-tuple. The test's setup (lines ~70-80) sets `__enter__` for many imscope context managers to return non-iterable `MagicMock()` but for `popup_modal` (line ~91) correctly returns `(True, None)`. The `imscope.window` setup is missing the tuple-return — purely a test-authoring bug.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. **Restore 100% pass rate on the 272-file test suite** (no regressions in any other test).
|
||||
2. **Preserve the defer-not-catch safety property** of commit `d7487af4` (avoid C-level crash on early-render C calls).
|
||||
3. **Harden the defer-not-catch documentation** to call out the str/bytes type contract (avoid future regressions of the same kind).
|
||||
4. **Tighten the test-authoring contract** for the prior session test: mock imscope context managers with the correct return shape.
|
||||
5. **OPTIONAL/DEFERRED:** Harden the defer-not-catch pattern doc with a "sentinel must match consumer type contract" note. Per user review (2026-06-05), this is deferred to the end of the track. If time permits, do it; otherwise leave for a follow-up patch.
|
||||
|
||||
## 3. Non-Goals
|
||||
|
||||
- Not refactoring the workspace profile save/load architecture.
|
||||
- Not adding wait-for-ready semantics to the test framework (deferred to a separate live_gui harden track; tracked as backlog item 0 in `conductor/tracks.md`).
|
||||
- Not fixing the broader test fragility / session-state issues (deferred).
|
||||
- Not addressing `sloppy.py` startup latency (separate track, also backlog).
|
||||
|
||||
## 4. Design
|
||||
|
||||
### Change 1: Fix `ini = b""` → `ini = ""` in `_capture_workspace_profile`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py:601-606` (the defer branch)
|
||||
- Modify: `src/gui_2.py:606-609` (the non-defer branch's `except` handler)
|
||||
|
||||
**Approach:** Change `ini = b""` to `ini = ""` in both places. The pre-fix code returned a `str`; we're restoring that contract. Additionally, defensively coerce the non-defer result: `ini = imgui.save_ini_settings_to_memory()` returns a `str` per `imgui-bundle` docs, but to be safe against future imgui-bundle changes, wrap it: `ini = str(imgui.save_ini_settings_to_memory() or "")`.
|
||||
|
||||
```python
|
||||
def _capture_workspace_profile(self, name: str) -> models.WorkspaceProfile:
|
||||
if not getattr(self, "_ini_capture_ready", False):
|
||||
self._ini_capture_ready = True
|
||||
ini = ""
|
||||
else:
|
||||
try:
|
||||
ini = str(imgui.save_ini_settings_to_memory() or "")
|
||||
except Exception:
|
||||
ini = ""
|
||||
panel_states = { ... }
|
||||
return models.WorkspaceProfile(...)
|
||||
```
|
||||
|
||||
**Why:** `WorkspaceProfile.ini_content: str` (`src/models.py:799`); `tomli_w` rejects `bytes`. `imgui.load_ini_settings_from_memory(ini_data: str, ...)` also expects `str`. Restoring the `str` contract is the minimal fix.
|
||||
|
||||
**Alternatives considered:**
|
||||
- A2 — Use `imgui.save_ini_settings_to_disk(path)` then read the file. **Rejected**: adds a side-effect path that's not idempotent; tests can pollute the test artifacts dir.
|
||||
- A3 — Force a frame render in `__init__` so the first call is safe. **Rejected**: changes init semantics; interacts badly with hot-reload (`src/hot_reloader.py`); may regress startup latency (the very thing the new sloppy.py startup track is meant to address).
|
||||
|
||||
### Change 2: Fix the prior session test mock
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_prior_session_no_pop_imbalance.py` (the imscope.window mock setup)
|
||||
|
||||
**Approach:** Add the tuple-return to `imscope.window`'s `__enter__` mock, matching the pattern already used for `popup_modal` at line 91:
|
||||
|
||||
```python
|
||||
mock_imscope.window.return_value.__enter__ = MagicMock(return_value=(True, True))
|
||||
mock_imscope.window.return_value.__exit__ = MagicMock(side_effect=_scope_exit)
|
||||
```
|
||||
|
||||
**Why:** The test's `imscope.window` setup is the only one missing the tuple-return; all other imscope context managers that production code expects to unpack as tuples already have it. This is a 2-line test-only fix.
|
||||
|
||||
### Change 3: Add a regression test for the ini_content type contract
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_workspace_profile_serialization.py`
|
||||
|
||||
**Approach:** Add a unit test that verifies a `WorkspaceProfile` with `ini_content=""` (empty str) round-trips through TOML via `to_dict` → `tomli_w.dump` → `tomllib.load` → `from_dict` without raising. This is the contract that the defer fix violated.
|
||||
|
||||
```python
|
||||
def test_workspace_profile_empty_ini_content_roundtrips():
|
||||
from src.models import WorkspaceProfile
|
||||
profile = WorkspaceProfile(name="t", ini_content="", show_windows={"A": True}, panel_states={"x": 1})
|
||||
d = profile.to_dict()
|
||||
import io, tomli_w, tomllib
|
||||
buf = io.BytesIO()
|
||||
tomli_w.dump({profile.name: d}, buf) # this is what save_profile does
|
||||
buf.seek(0)
|
||||
back = tomllib.load(buf)
|
||||
loaded = WorkspaceProfile.from_dict("t", back["t"])
|
||||
assert loaded.ini_content == ""
|
||||
assert loaded.show_windows == {"A": True}
|
||||
assert loaded.panel_states == {"x": 1}
|
||||
```
|
||||
|
||||
**Why:** This test would have caught the `d7487af4` regression. It encodes the type contract for future contributors. It's a pure unit test, no live_gui, runs in <1s.
|
||||
|
||||
### Change 4: Harden the defer-not-catch doc
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/guide_gui_2.md` "Workspace Profile Defer-Not-Catch" section
|
||||
- Modify: `docs/guide_testing.md` "Early-Render C-Level Crashes" section
|
||||
- Modify: `conductor/workflow.md` "Defer-Not-Catch Pattern for Native Crashes" section
|
||||
|
||||
**Approach:** Add a note: "When implementing a defer-not-catch guard for a return value, **ensure the sentinel value matches the type contract of the downstream consumer**. For `WorkspaceProfile.ini_content: str`, the sentinel must be `""` (str), not `b""` (bytes) — TOML serialization rejects bytes."
|
||||
|
||||
**Why:** Future contributors applying the defer-not-catch pattern should not silently introduce type-incompatible sentinels.
|
||||
|
||||
## 5. Data Flow
|
||||
|
||||
### Before (buggy)
|
||||
```
|
||||
set_value(True) → app.ui_separate_tier1 = True
|
||||
save_workspace_profile → _capture_workspace_profile → ini=b"" (bytes)
|
||||
→ to_dict() → {"ini_content": b""}
|
||||
→ tomli_w.dump → TypeError
|
||||
→ profile NOT saved
|
||||
set_value(False) → app.ui_separate_tier1 = False
|
||||
load_workspace_profile → name not in workspace_profiles → no-op
|
||||
assert get_value is True → FAILS (still False)
|
||||
```
|
||||
|
||||
### After (fixed)
|
||||
```
|
||||
set_value(True) → app.ui_separate_tier1 = True
|
||||
save_workspace_profile → _capture_workspace_profile → ini="" (str)
|
||||
→ to_dict() → {"ini_content": ""}
|
||||
→ tomli_w.dump → OK
|
||||
→ profile saved
|
||||
set_value(False) → app.ui_separate_tier1 = False
|
||||
load_workspace_profile → name in workspace_profiles → _apply_workspace_profile
|
||||
→ setattr(self, "ui_separate_tier1", True)
|
||||
assert get_value is True → PASSES
|
||||
```
|
||||
|
||||
## 6. Error Handling
|
||||
|
||||
- The defer branch and the `except` branch both set `ini = ""`. Empty string is a valid `str` and is safe for `tomli_w`, for the dataclass, and for `imgui.load_ini_settings_from_memory("")` (which is a no-op that lets ImGui use its defaults).
|
||||
- No new exceptions are introduced. The `TypeError` from the buggy `b""` goes away because the type is now `str`.
|
||||
- The new regression test (`test_workspace_profile_serialization.py`) is itself a forward-looking guard: if a future change reintroduces a bytes sentinel, the test will fail with a clear message.
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### New tests
|
||||
- `tests/test_workspace_profile_serialization.py::test_workspace_profile_empty_ini_content_roundtrips` — pure unit test, <1s, encodes the str contract.
|
||||
|
||||
### Existing tests that should now pass
|
||||
- `tests/test_auto_switch_sim::test_auto_switch_sim` — saves+loads workspace profile.
|
||||
- `tests/test_workspace_profiles_sim::test_workspace_profiles_restoration` — saves+loads workspace profile.
|
||||
- `tests/test_prior_session_no_pop_imbalance::test_no_extraneous_pop_when_prior_session_renders` — mock setup fix.
|
||||
|
||||
### Regression check
|
||||
- Re-run the full batched test suite (`scripts/run_tests_batched.py`) after the fixes; expect 272/272 pass.
|
||||
- Re-run targeted batches of theme tests (`test_theme*`, `test_log_pruner*`, `test_view_presets*`, `test_gui_progress*`, `test_gui_phase4*`) to verify the prior doc-track fixes still pass.
|
||||
|
||||
## 8. Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| The `str()` coercion in the non-defer branch changes behavior | Low | Low | `imgui.save_ini_settings_to_memory()` is documented to return `str`; the coercion is defensive only. The `or ""` handles a `None` return (which `imgui-bundle` does not produce but we don't want to crash on). |
|
||||
| The new unit test depends on `tomli_w` semantics that change | Very low | Low | `tomli_w` is a stable dep; the test would only break if `bytes` becomes serializable, which would be a major version change. |
|
||||
| The mock fix in the prior session test changes other behavior | Low | Low | The fix only adds the missing tuple-return; existing mocks for other imscope context managers are untouched. |
|
||||
| Removing the `b""` sentinel causes the early-render C crash to return | Very low | High | The `try/except Exception` around `imgui.save_ini_settings_to_memory()` is preserved; the flag-based defer is preserved. Only the type of the sentinel changes. |
|
||||
|
||||
## 9. Out of Scope (Tracked Separately)
|
||||
|
||||
- **live_gui session-state contract** (test-authoring rigor, wait-for-ready pattern) — see [docs/guide_testing.md#authoring-robust-live_gui-tests-dont-assume-clean-state] (added in this session). This is a doc-only change; tests will be hardened over time as they break.
|
||||
- **sloppy.py startup latency** — new backlog item 0 in `conductor/tracks.md`, planned via superpowers writing-plans skill in a future session.
|
||||
- **Other live_gui tests still flagged as fragile in the regression-fixes plan** (MMA engine state transitions, RAG status timing) — these were in the deferred category of the `regression_fixes_20260605` plan; not addressed by this design.
|
||||
|
||||
## 10. References
|
||||
|
||||
- Commit `d7487af4` — the defer-not-catch fix that introduced the `b""` sentinel.
|
||||
- `src/gui_2.py:601-606` — current defer code.
|
||||
- `src/models.py:797-823` — `WorkspaceProfile` dataclass with `ini_content: str`.
|
||||
- `src/workspace_manager.py:48-58` — `save_profile` that calls `to_dict` then `tomli_w.dump`.
|
||||
- `docs/guide_gui_2.md#workspace-profile-defer-not-catch` — the defer-not-catch section to harden.
|
||||
- `docs/guide_testing.md#known-gotchas-2026-06-05` — the early-render C-crash section to harden.
|
||||
- `conductor/tracks.md` — `regression_fixes_20260605` and `multi_themes_20260604` entries.
|
||||
- `conductor/tracks.md` — new backlog item 0 (sloppy.py startup speedup).
|
||||
@@ -1,200 +0,0 @@
|
||||
# Live-GUI State Sync — Design
|
||||
|
||||
**Date:** 2026-06-05
|
||||
**Status:** Draft
|
||||
**Track:** live_gui_state_sync_20260605 (sub-project of v2)
|
||||
|
||||
## Problem Statement
|
||||
|
||||
`App` (`src/gui_2.py`) and `AppController` (`src/app_controller.py`) maintain **parallel state** for the same logical fields. `set_value` writes to the **Controller**, but several code paths read from the **App**, returning stale or wrong values.
|
||||
|
||||
### Concrete failures (from 2026-06-05 batched test run, batches 7, 46, 65, 68)
|
||||
|
||||
1. **`test_auto_switch_sim::test_auto_switch_sim`** — sets `ui_separate_tier1=True` and `show_windows['Diagnostics']=True`, saves `Tier3Profile`, sets to False, triggers tier-3 auto-switch. Expects `show_windows['Diagnostics']=True` restored. **Fails: profile captures from App but is set on Controller.**
|
||||
|
||||
2. **`test_workspace_profiles_restoration::test_workspace_profiles_restoration`** — sets `ui_separate_tier1=True`, saves `test_restore`, sets to False, loads. Expects True. **Fails: same root cause.**
|
||||
|
||||
3. **`test_undo_redo_lifecycle::test_undo_redo_lifecycle`** (NEW regression) — sets `ai_input="Initial Input"`, modifies to `"Modified Input"`, clicks `btn_undo`. Expects `ai_input="Initial Input"`. **Fails: snapshot reads `app.ui_ai_input` but `set_value` writes to `controller.ui_ai_input`.**
|
||||
|
||||
### Discovery (2026-06-05 execution): State sync is NOT the root cause
|
||||
|
||||
Initial hypothesis: App and Controller maintain parallel state for settable fields. Verified during execution: **the App class already has `__getattr__` (line 478) and `__setattr__` (line 483) that auto-delegate to the controller.** Writes go through `__setattr__` → controller. Reads go through `__getattr__` → controller. The state is correctly synced at the descriptor level. The original spec assumption was wrong.
|
||||
|
||||
## REAL root cause: `_capture_workspace_profile` is not a class method
|
||||
|
||||
During execution, AST analysis of `src/gui_2.py` reveals the actual bug:
|
||||
|
||||
```
|
||||
$ uv run python -c "import ast; ..."
|
||||
App methods (count): 59
|
||||
WORKSPACE METHOD: _apply_workspace_profile # ← exists
|
||||
# ← _capture_workspace_profile MISSING
|
||||
```
|
||||
|
||||
`_capture_workspace_profile` is defined at line 607 of `src/gui_2.py` with 2-space indent (intended as a class method), but the AST walks it as **nested inside `_apply_snapshot`** (line 572). The body of `_apply_snapshot` (lines 573-635) absorbs the next `def` as a nested function.
|
||||
|
||||
This means when the live_gui calls `self._app._capture_workspace_profile(name)`, Python's normal class lookup fails to find `_capture_workspace_profile` on the App class. `__getattr__('_capture_workspace_profile')` is triggered, which delegates to `self.controller._capture_workspace_profile`. The controller does NOT have this method. `AttributeError` is raised. The save callback fails silently. The test's `load_workspace_profile` finds no profile to load (because save failed). The test fails.
|
||||
|
||||
### Why AST sees it as nested
|
||||
|
||||
The likely cause is the user's recent cleanup commit `873edf42` ("began to go through the files and organize imports and gui_2.py's new context defs") which touched `src/gui_2.py:261` lines. The cleanup reorganized method placement. Either:
|
||||
- Indentation was accidentally off by 1 space on some lines.
|
||||
- A blank line or comment that closed a function body was removed.
|
||||
- Method definitions were moved but their indentation wasn't updated.
|
||||
|
||||
Specific to the bug: `_apply_snapshot` has a `try:` (line 574) without an `except` (only a `finally:` at line 604). This is valid Python syntax, but the indentation of subsequent lines may have been off, causing the AST to consume the next `def` into the `try` block.
|
||||
|
||||
## Audit of duplicated fields (retained from original spec, for context)
|
||||
|
||||
Static analysis of the 71 settable fields in `AppController._settable_fields` vs the 12 `panel_states` keys captured in `App._capture_workspace_profile`, plus the `show_windows` dict and snapshot fields:
|
||||
|
||||
| Field | In `_settable_fields` (Controller)? | Read by App code? | Sync bug? |
|
||||
|---|---|---|---|
|
||||
| `show_windows` | yes | `_capture_workspace_profile` (line 627), `_apply_workspace_profile` (line 633) | **YES** |
|
||||
| `ui_separate_task_dag` | yes | `_capture_workspace_profile` (line 615) | **YES** |
|
||||
| `ui_separate_usage_analytics` | yes | `_capture_workspace_profile` (line 616) | **YES** |
|
||||
| `ui_separate_tier1` | yes | `_capture_workspace_profile` (line 617) | **YES** |
|
||||
| `ui_separate_tier2` | yes | `_capture_workspace_profile` (line 618) | **YES** |
|
||||
| `ui_separate_tier3` | yes | `_capture_workspace_profile` (line 619) | **YES** |
|
||||
| `ui_separate_tier4` | yes | `_capture_workspace_profile` (line 620) | **YES** |
|
||||
| `ui_ai_input` | yes (`ai_input -> ui_ai_input`) | `_take_snapshot` (line 551), `_apply_snapshot` (line 569) | **YES** |
|
||||
| `ui_separate_context_preview` | no (NOT in settable_fields) | `_capture_workspace_profile` (line 611) | no — App-only |
|
||||
| `ui_separate_message_panel` | no | `_capture_workspace_profile` (line 612) | no — App-only |
|
||||
| `ui_separate_response_panel` | no | `_capture_workspace_profile` (line 613) | no — App-only |
|
||||
| `ui_separate_tool_calls_panel` | no | `_capture_workspace_profile` (line 614) | no — App-only |
|
||||
| `ui_separate_external_tools` | no | `_capture_workspace_profile` (line 621) | no — App-only |
|
||||
| `ui_discussion_split_h` | no | `_capture_workspace_profile` (line 622) | no — App-only |
|
||||
|
||||
**8 confirmed sync bugs.** Plus `ui_ai_input` (snapshot) is a 9th.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`App.__init__` creates a separate `AppController` instance and later sets `self.controller._app = self` (bidirectional link). The two objects each declare their own `self.ui_separate_tier1 = False` (App) and `self.ui_separate_tier1 = False` (Controller) in their respective `__init__`s. They are independent Python attributes.
|
||||
|
||||
`set_value` (`src/api_hooks.py`, line 614) calls `setattr(controller, attr_name, value)` — writes to Controller. But `_capture_workspace_profile` reads `self.ui_separate_tier1` where `self` is the App — never updated.
|
||||
|
||||
## Design
|
||||
|
||||
### Goal
|
||||
|
||||
Eliminate the dual state. **Single source of truth: the Controller.** The App becomes a thin "view" layer that exposes Controller fields as Python properties. `set_value` continues to write to the Controller. All reads (from save, snapshot, render) transparently read from the Controller.
|
||||
|
||||
### Approach: Properties on App that delegate to Controller
|
||||
|
||||
Add `@property` definitions on the `App` class for each field that has a Controller counterpart. The getter returns `self.controller.X`. The setter (where App code writes, e.g. snapshot restore) also delegates to `self.controller.X`.
|
||||
|
||||
**Hypothetical example for `ui_separate_tier1`:**
|
||||
|
||||
```python
|
||||
# In App class (src/gui_2.py)
|
||||
|
||||
@property
|
||||
def ui_separate_tier1(self) -> bool:
|
||||
return self.controller.ui_separate_tier1
|
||||
|
||||
@ui_separate_tier1.setter
|
||||
def ui_separate_tier1(self, value: bool) -> None:
|
||||
self.controller.ui_separate_tier1 = value
|
||||
```
|
||||
|
||||
This makes `app.ui_separate_tier1` and `controller.ui_separate_tier1` the same value, regardless of which path writes. The only writes are via the property setter (or `set_value` via the Controller directly), and all reads go through the getter.
|
||||
|
||||
### Why this approach
|
||||
|
||||
- **Minimal blast radius**: The App class only adds properties; no method bodies change. Methods that read `self.X` continue to work — they just get the Controller's value via the property.
|
||||
- **Bidirectional**: Setter support is critical for `_apply_snapshot` and `_apply_workspace_profile` which set App fields directly (`self.ui_ai_input = snapshot.ai_input`). They go through the property setter, which writes to the Controller.
|
||||
- **No double-write footgun**: A "sync on set_value" alternative requires remembering to write to BOTH objects. A property approach is a single point of truth.
|
||||
- **Easy to migrate incrementally**: Each field is one property pair. Can be added one at a time with a regression test for each.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- **A2: Merge App and Controller into one class.** Rejected: would be a 5532-line → 4000-line merge with high risk. The Controller already lives in a separate file; the App delegates to it via `self.controller.X`. Merging would lose the existing boundary.
|
||||
- **A3: Sync on every set_value (write to both).** Rejected: requires touching every writer; easy to miss a site. Property approach is one place per field.
|
||||
- **A4: Pass Controller as a method argument everywhere.** Rejected: invasive; requires changing method signatures throughout `gui_2.py` and `app_controller.py`.
|
||||
|
||||
## File Changes
|
||||
|
||||
### Modify: `src/gui_2.py` (App class)
|
||||
|
||||
Add `@property` + `@X.setter` for each of the 8 sync-bug fields, plus `ui_ai_input`:
|
||||
|
||||
```python
|
||||
@property
|
||||
def ui_separate_tier1(self) -> bool:
|
||||
return self.controller.ui_separate_tier1
|
||||
|
||||
@ui_separate_tier1.setter
|
||||
def ui_separate_tier1(self, value: bool) -> None:
|
||||
self.controller.ui_separate_tier1 = value
|
||||
```
|
||||
|
||||
Fields to add properties for:
|
||||
- `ui_ai_input` (snapshot bug)
|
||||
- `ui_separate_task_dag`
|
||||
- `ui_separate_usage_analytics`
|
||||
- `ui_separate_tier1` through `ui_separate_tier4`
|
||||
- `show_windows` (special: dict, not bool)
|
||||
|
||||
For `show_windows`, the property needs care — `set_value` may pass a new dict; the property should do `self.controller.show_windows = value` to allow full replacement, but for in-place updates (`self.show_windows["X"] = True`), the property getter returns the Controller's dict reference (so in-place mutations work) and the property setter can either replace or do nothing (since the dict is shared).
|
||||
|
||||
```python
|
||||
@property
|
||||
def show_windows(self) -> Dict[str, bool]:
|
||||
return self.controller.show_windows
|
||||
|
||||
@show_windows.setter
|
||||
def show_windows(self, value: Dict[str, bool]) -> None:
|
||||
self.controller.show_windows = value
|
||||
```
|
||||
|
||||
**Do NOT** add properties for fields that are App-only (no Controller counterpart): `ui_separate_context_preview`, `ui_separate_message_panel`, `ui_separate_response_panel`, `ui_separate_tool_calls_panel`, `ui_separate_external_tools`, `ui_discussion_split_h`, etc. — they remain as plain App attributes.
|
||||
|
||||
### Add: `tests/test_app_controller_state_sync.py` (new)
|
||||
|
||||
A new unit test that encodes the contract: **for every field in `_settable_fields` that is also referenced as `self.X` in the App class's `_capture_workspace_profile` and `_take_snapshot`/`_apply_snapshot`, writes to `app.X` and `controller.X` must be observed by both.**
|
||||
|
||||
```python
|
||||
def test_ui_separate_tier1_setter_delegates_to_controller():
|
||||
"""The App's ui_separate_tier1 property is a delegate to the Controller.
|
||||
Writes through app.ui_separate_tier1 = X are visible at controller.ui_separate_tier1,
|
||||
and writes through set_value (which goes to controller) are visible at app.ui_separate_tier1."""
|
||||
from src import app_controller, gui_2
|
||||
from src.app_controller import AppController
|
||||
# Don't fully init App (too heavy); use lightweight setup
|
||||
app = gui_2.App.__new__(gui_2.App)
|
||||
app.controller = AppController()
|
||||
app._app = app # back-ref
|
||||
# set_value goes to controller
|
||||
app.controller.ui_separate_tier1 = True
|
||||
assert app.ui_separate_tier1 is True # reads through property
|
||||
# direct set through app's property
|
||||
app.ui_separate_tier1 = False
|
||||
assert app.controller.ui_separate_tier1 is False # write visible at controller
|
||||
```
|
||||
|
||||
This is a regression test for the contract.
|
||||
|
||||
### Test impact
|
||||
|
||||
After the fix, these tests should pass:
|
||||
- `test_auto_switch_sim::test_auto_switch_sim` (writes to `app.show_windows` and `app.ui_separate_tier1` are observed by save)
|
||||
- `test_workspace_profiles_sim::test_workspace_profiles_restoration` (same)
|
||||
- `test_undo_redo_lifecycle::test_undo_redo_lifecycle` (snapshot reads from `app.ui_ai_input` get the Controller's value)
|
||||
|
||||
If `test_undo_redo_lifecycle` is **also** a flake or a regression from the user's recent cleanup commit `873edf42`, the property fix may not be sufficient. In that case, the test will continue to fail and need its own investigation track.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Existing App code does `del app.ui_X` to reset state | Low | Low | property setter can be a no-op for `del` (raises AttributeError); review call sites |
|
||||
| App class is 5532 lines — risk of regression | High | Medium | Per-field property addition; one regression test per field; ship in a single atomic commit |
|
||||
| User's recent cleanup commit `873edf42` may have added or removed attribute references | Medium | Low | Run targeted regression test after each property addition |
|
||||
| New properties shadow existing class attributes | Low | High | Use `dir(app)` to verify no shadow before commit |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- **prior_session test mock setup** — separate track (`prior_session_test_harden_20260605`).
|
||||
- **wait-for-ready test pattern** — separate track (`wait_for_ready_test_pattern_20260605`).
|
||||
- **Other App/Controller sync bugs not in the 8 listed** — audit will continue; if more found, queue as v3 sub-track.
|
||||
- **Refactoring App and Controller into one class** — deferred; property approach is sufficient for now.
|
||||
@@ -1,118 +0,0 @@
|
||||
# prior_session_test_harden_20260605 — Design
|
||||
|
||||
**Date:** 2026-06-05
|
||||
**Status:** Draft
|
||||
**Track:** prior_session_test_harden_20260605 (sub-project of v2)
|
||||
|
||||
## Problem Statement
|
||||
|
||||
`tests/test_prior_session_no_pop_imbalance.py::test_no_extraneous_pop_when_prior_session_renders` fails with `TypeError: cannot unpack non-iterable NoneType object` at `src/gui_2.py:2333` (`imscope.window(...) as (opened, visible):`).
|
||||
|
||||
Root cause: the test mocks `imscope.window`'s `__enter__` to return a non-iterable `MagicMock()`, but the production code expects a 2-tuple. **AND** the test exercises `gui_2.render_main_interface(app_instance)`, a kitchen-sink function that calls dozens of other render functions, each with their own mock-shape requirements. After fixing the imscope.window tuple-return, the next failure surfaces at `src/gui_2.py:4496` (render_theme_panel: imgui.begin returning bool where 2-tuple expected). The test would need 50+ mocks to fully exercise `render_main_interface`.
|
||||
|
||||
## Test's Actual Intent
|
||||
|
||||
The test's only assertion is `assert push_count["n"] == pop_count["n"]` — verify that `imscope.style_color` push and pop counts balance when the prior-session render runs. This is a narrow, well-defined contract.
|
||||
|
||||
The test does NOT need to exercise the entire `render_main_interface`. It only needs to exercise the prior-session render path.
|
||||
|
||||
## Design
|
||||
|
||||
### Approach: Call the narrow prior-session render function, not the kitchen sink
|
||||
|
||||
`src/gui_2.py` has a dedicated `render_prior_session_view(app)` function (line ~4400) that handles the prior-session rendering. It's a ~30-line function with a finite, mockable set of imgui/imscope calls.
|
||||
|
||||
**Hypothetical refactor:**
|
||||
|
||||
```python
|
||||
def test_no_extraneous_pop_when_prior_session_renders():
|
||||
from src import gui_2
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
app_instance = MagicMock()
|
||||
app_instance.is_viewing_prior_session = True
|
||||
app_instance.perf_profiling_enabled = False
|
||||
app_instance.prior_disc_entries = [
|
||||
{"role": "User", "content": "test", "collapsed": False, "ts": "t1"}
|
||||
]
|
||||
|
||||
push_count = {"n": 0}
|
||||
pop_count = {"n": 0}
|
||||
def _track_push(*a, **k): push_count["n"] += 1
|
||||
def _track_pop(*a, **k): pop_count["n"] += 1
|
||||
|
||||
with patch("src.gui_2.imgui") as mock_imgui, \
|
||||
patch("src.gui_2.imscope") as mock_imscope, \
|
||||
patch("src.gui_2.theme") as mock_theme, \
|
||||
patch("src.gui_2.markdown_helper") as mock_md:
|
||||
|
||||
# Wire push/pop tracking on imscope.style_color
|
||||
mock_imscope.style_color.return_value.__enter__.side_effect = _track_push
|
||||
mock_imscope.style_color.return_value.__exit__.side_effect = lambda *a: (pop_count.__setitem__("n", pop_count["n"] + 1) or False)
|
||||
|
||||
# Set up tuple-return for ALL imscope context managers (style_color, child, id, etc.)
|
||||
for sc in [mock_imscope.style_color, mock_imscope.child, mock_imscope.id]:
|
||||
sc.return_value.__enter__ = MagicMock()
|
||||
sc.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# Mock the small finite set of imgui calls used by render_prior_session_view
|
||||
mock_imgui.Col_ = MagicMock()
|
||||
mock_imgui.button = MagicMock(return_value=False)
|
||||
mock_imgui.same_line = MagicMock()
|
||||
mock_imgui.text_colored = MagicMock()
|
||||
mock_imgui.separator = MagicMock()
|
||||
mock_imgui.get_content_region_avail = MagicMock(return_value=MagicMock(x=800.0, y=600.0))
|
||||
mock_imgui.ImVec2 = lambda *a: MagicMock(x=a[0], y=a[1])
|
||||
mock_imgui.WindowFlags_ = MagicMock()
|
||||
mock_imgui.text = MagicMock()
|
||||
|
||||
mock_theme.get_color = MagicMock(return_value=MagicMock(x=0,y=0,z=0,w=0))
|
||||
mock_theme.ai_text_style.return_value.__enter__ = MagicMock()
|
||||
mock_theme.ai_text_style.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
mock_md.render = MagicMock()
|
||||
|
||||
# Call the narrow function, NOT the kitchen sink
|
||||
gui_2.render_prior_session_view(app_instance)
|
||||
|
||||
assert push_count["n"] == pop_count["n"], f"Push/pop imbalance: pushes={push_count['n']}, pops={pop_count['n']}"
|
||||
```
|
||||
|
||||
This is ~30 mocks instead of 50+, scoped to what `render_prior_session_view` actually uses. The imscope mocks all return their own context-manager defaults (no need to return a tuple for `style_color` since `with imscope.style_color(...) as c:` doesn't unpack). The test's actual assertion (push/pop balance) is preserved.
|
||||
|
||||
### Why this approach
|
||||
|
||||
- **Smallest change to the test**: removes 50+ mocks, replaces with 30+ scoped mocks. Test runs faster.
|
||||
- **Preserves test intent**: the assertion is still about push/pop balance in the prior-session render.
|
||||
- **Survives future refactors**: as long as `render_prior_session_view` exists, the test is meaningful. If the function is renamed/restructured, the test is localized to that function.
|
||||
- **Aligns with the live_gui test philosophy**: tests should exercise narrow paths, not kitchen sinks. (This is consistent with the [docs/guide_testing.md Authoring Robust live_gui Tests] rules I just authored.)
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- **A2: Add 50+ mocks to make `render_main_interface` work.** Rejected: the test becomes a maintenance burden (any change to any sub-render function breaks the test). It also tests too much (push/pop balance in the entire GUI, not just prior-session).
|
||||
- **A3: Skip the test entirely, mark as known-flake.** Rejected: the test is meaningful and verifies a real contract. Better to make it work.
|
||||
|
||||
## File Changes
|
||||
|
||||
### Modify: `tests/test_prior_session_no_pop_imbalance.py`
|
||||
|
||||
Replace the `render_main_interface(app_instance)` call with `render_prior_session_view(app_instance)`. Remove the mocks for the 50+ imgui methods that are NOT used by `render_prior_session_view` (e.g. `selectable`, `tree_node`, `set_scroll_here_y`, etc.). Keep the mocks for the 30+ methods that ARE used.
|
||||
|
||||
### No production code changes
|
||||
|
||||
The test is rewritten; `render_prior_session_view` itself does not change.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| `render_prior_session_view` signature/name changes | Low | Medium | The test is local to this function; future refactors will update both |
|
||||
| Mocking too aggressively (mocking something the function actually uses) | Medium | Low | Run the test; if it fails, add the missing mock |
|
||||
| Test was testing more than just push/pop balance (e.g. some side effect) | Low | Low | Read the original test docstring; the only assertion is push/pop balance |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- **State sync fix** — separate track (`live_gui_state_sync_20260605`).
|
||||
- **Wait-for-ready pattern** — separate track (`wait_for_ready_test_pattern_20260605`).
|
||||
- **undo_redo_lifecycle** — separate track (`undo_redo_lifecycle_fix_20260605`).
|
||||
- **Refactoring `render_main_interface` to be smaller** — deferred; out of scope for this track.
|
||||
@@ -1,83 +0,0 @@
|
||||
# undo_redo_lifecycle_fix_20260605 — Design
|
||||
|
||||
**Date:** 2026-06-05
|
||||
**Status:** Draft
|
||||
**Track:** undo_redo_lifecycle_fix_20260605 (sub-project of v2)
|
||||
|
||||
## Problem Statement
|
||||
|
||||
`tests/test_undo_redo_sim.py::test_undo_redo_lifecycle` failed in the 2026-06-05 second batched test run (after the first run had it passing). The test:
|
||||
|
||||
1. Sets `temperature=0.5` and `ai_input="Initial Input"`.
|
||||
2. Modifies to `temperature=1.5` and `ai_input="Modified Input"`.
|
||||
3. Asserts current state — passes.
|
||||
4. Clicks `btn_undo`.
|
||||
5. Asserts `ai_input == "Initial Input"` and `temperature == 0.5`.
|
||||
6. **Fails on the `ai_input` assertion**: gets `''` (empty string).
|
||||
|
||||
The undo restores `temperature` correctly but not `ai_input`. The other 2 tests in the same file (`test_undo_redo_discussion_mutation`, `test_undo_redo_context_mutation`) pass — they don't exercise `ai_input`.
|
||||
|
||||
### Possible causes
|
||||
|
||||
1. **App/Controller state sync bug for `ai_input`.** The snapshot at `src/gui_2.py:551` reads `self.ui_ai_input` (App), but `set_value` writes to `controller.ui_ai_input`. The snapshot captures the App's (stale) value. **This should be fixed by the `live_gui_state_sync_20260605` track** (which adds an `ui_ai_input` property on the App that delegates to the Controller).
|
||||
|
||||
2. **Snapshot doesn't include `ai_input` field at all.** Check `src/history.py:UISnapshot` — if `ai_input` isn't a field, the snapshot stores nothing, and the apply can't restore.
|
||||
|
||||
3. **Test flake.** The test was passing in the first run, failing in the second. The `live_gui` fixture is session-scoped, and different test orders can produce different state. The test's `time.sleep(2.0)` after `btn_undo` may not be enough if the GUI is under load.
|
||||
|
||||
4. **Recent user commit `873edf42` regression.** The user's cleanup commit touched 53 files including `src/gui_2.py:261` lines. If the cleanup accidentally changed the snapshot mechanism, this could break the test.
|
||||
|
||||
## Design
|
||||
|
||||
### Approach: Two-phase investigation
|
||||
|
||||
**Phase 1: Re-run the test after the `live_gui_state_sync_20260605` track lands.**
|
||||
|
||||
If the state-sync property fix for `ui_ai_input` unblocks the test, the issue is resolved. No further work needed.
|
||||
|
||||
**Phase 2: If the test still fails, deep-dive into the snapshot mechanism.**
|
||||
|
||||
Investigate in this order:
|
||||
1. Check `src/history.py:UISnapshot` to see if `ai_input` is a field. If not, add it.
|
||||
2. Check `src/gui_2.py:_apply_snapshot` to see if it restores `ai_input`. If not, add the restore line.
|
||||
3. Check if there's a per-tick snapshot filter that excludes certain fields.
|
||||
4. Add a regression test that explicitly verifies the snapshot/undo round-trip for `ai_input`.
|
||||
|
||||
**Phase 3: If still failing, test-ordering / flake investigation.**
|
||||
|
||||
The test uses `time.sleep(2.0)` after `btn_undo`. Convert to polling (`wait_for_load_completion` from the `wait_for_ready_test_pattern_20260605` track). If the test passes with polling, it was a flake.
|
||||
|
||||
### Why this approach
|
||||
|
||||
- **Sequential investigation**: cheapest fixes first. State-sync is the most likely cause (it just landed as a property fix). Snapshot mechanism is the second most likely. Flake is the third.
|
||||
- **No speculative changes**: don't add `ai_input` to the snapshot if it's already there. Don't change the undo mechanism if the state-sync fix is sufficient.
|
||||
|
||||
## File Changes
|
||||
|
||||
### Phase 1: None (state-sync fix is in a different track)
|
||||
|
||||
### Phase 2 (if needed):
|
||||
|
||||
- Modify: `src/history.py` (add `ai_input` field to UISnapshot if missing)
|
||||
- Modify: `src/gui_2.py:_apply_snapshot` (add `ai_input` restore line if missing)
|
||||
- Add: `tests/test_undo_redo_ai_input_snapshot.py` (regression test for the round-trip)
|
||||
|
||||
### Phase 3 (if needed):
|
||||
|
||||
- Modify: `tests/test_undo_redo_sim.py` (replace `time.sleep(2.0)` with `wait_for_load_completion`)
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Phase 1 fixes the issue | High | None | Done |
|
||||
| Phase 2 needed: snapshot already has ai_input but apply doesn't restore | Medium | Low | Check first, then add the restore line |
|
||||
| Phase 2 needed: snapshot doesn't have ai_input | Low | Low | Add the field + apply line |
|
||||
| Phase 3 needed: it's a flake | Low | None | Replace sleeps with polling |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- **State sync fix** — separate track (`live_gui_state_sync_20260605`).
|
||||
- **prior_session test** — separate track (`prior_session_test_harden_20260605`).
|
||||
- **wait_for_ready pattern** — separate track (`wait_for_ready_test_pattern_20260605`).
|
||||
- **General undo/redo system improvements** — out of scope.
|
||||
@@ -1,112 +0,0 @@
|
||||
# wait_for_ready_test_pattern_20260605 — Design
|
||||
|
||||
**Date:** 2026-06-05
|
||||
**Status:** Draft
|
||||
**Track:** wait_for_ready_test_pattern_20260605 (sub-project of v2)
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Two failing live_gui tests use `time.sleep(N)` to wait for asynchronous GUI operations to complete:
|
||||
|
||||
- `tests/test_workspace_profiles_sim.py` — `time.sleep(2.0)` after save and after load; `time.sleep(1.0)` after each set_value.
|
||||
- `tests/test_auto_switch_sim.py` — `time.sleep(1)` after each `push_event`.
|
||||
|
||||
Fixed sleeps are a fragile test pattern:
|
||||
- On slow machines the sleep may be insufficient; the assertion runs before the operation completes.
|
||||
- On fast machines the sleep is wasted; the test takes longer than necessary.
|
||||
- Tests that pass with `time.sleep(2.0)` in CI may fail on a developer machine with different load.
|
||||
|
||||
After the state-sync fix (`live_gui_state_sync_20260605`) lands, these tests should pass at the current 2-second sleep. **But the test pattern is still wrong** — the tests should poll for completion, not assume timing.
|
||||
|
||||
## Design
|
||||
|
||||
### Approach: Migrate `time.sleep` to a wait-for-ready helper
|
||||
|
||||
`src/api_hook_client.py` already exposes `wait_for_event(event_type, timeout)` and `get_value(item)`. The tests can use these directly.
|
||||
|
||||
**Hypothetical example — the current pattern:**
|
||||
|
||||
```python
|
||||
client.set_value('ui_separate_tier1', True)
|
||||
time.sleep(1.0)
|
||||
client.push_event("custom_callback", {"callback": "save_workspace_profile", "args": ["test_restore", "project"]})
|
||||
time.sleep(2.0) # HOPE the save completes within 2s
|
||||
client.set_value('ui_separate_tier1', False)
|
||||
time.sleep(1.0)
|
||||
client.push_event("custom_callback", {"callback": "load_workspace_profile", "args": ["test_restore"]})
|
||||
time.sleep(2.0) # HOPE the load completes within 2s
|
||||
assert client.get_value('ui_separate_tier1') is True
|
||||
```
|
||||
|
||||
**Migrated pattern:**
|
||||
|
||||
```python
|
||||
def wait_for_save_completion(client, profile_name, timeout=5.0):
|
||||
"""Poll until the saved profile appears in the workspace profiles."""
|
||||
import time
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
profiles = client.get_value('workspace_profiles') or {}
|
||||
if profile_name in profiles:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"Save did not complete within {timeout}s")
|
||||
|
||||
def wait_for_load_completion(client, item, expected, timeout=5.0):
|
||||
"""Poll until the item's value matches expected."""
|
||||
import time
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if client.get_value(item) == expected:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"Load did not apply {item}={expected} within {timeout}s")
|
||||
|
||||
client.set_value('ui_separate_tier1', True)
|
||||
# No sleep needed; set_value returns when the value is set on the controller
|
||||
client.push_event("custom_callback", {"callback": "save_workspace_profile", "args": ["test_restore", "project"]})
|
||||
wait_for_save_completion(client, "test_restore")
|
||||
client.set_value('ui_separate_tier1', False)
|
||||
client.push_event("custom_callback", {"callback": "load_workspace_profile", "args": ["test_restore"]})
|
||||
wait_for_load_completion(client, 'ui_separate_tier1', True)
|
||||
```
|
||||
|
||||
### Why this approach
|
||||
|
||||
- **Polling, not fixed sleeps**: 100ms poll interval is responsive without busy-waiting.
|
||||
- **Generous timeouts**: 5s default is well over the typical ~100ms operation; catches genuine hangs.
|
||||
- **Reusable helpers**: `wait_for_save_completion` and `wait_for_load_completion` are simple and can be added to a shared test helper module.
|
||||
- **Failure messages are clear**: TimeoutError explicitly says which operation timed out.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- **A2: Add wait_for_X helpers to ApiHookClient itself.** Rejected: ApiHookClient should remain a thin transport; test-helper logic doesn't belong there. Keep helpers in `tests/conftest.py` or a `tests/helpers.py` module.
|
||||
- **A3: Use `wait_for_event` exclusively.** The Hook API's `wait_for_event` listens for events the GUI emits. save/load may not emit events in a way the test can match. Polling `get_value` is more direct.
|
||||
|
||||
## File Changes
|
||||
|
||||
### Modify: `tests/test_workspace_profiles_sim.py`
|
||||
|
||||
Replace `time.sleep(...)` with `wait_for_save_completion` and `wait_for_load_completion` calls. Add the helper functions at the top of the file (or import from a shared helper).
|
||||
|
||||
### Modify: `tests/test_auto_switch_sim.py`
|
||||
|
||||
Replace `time.sleep(...)` with similar polling helpers.
|
||||
|
||||
### Optionally: Create: `tests/helpers.py`
|
||||
|
||||
If multiple tests need the same helpers, extract them to a shared module. For now, keep them inline (2 tests, ~30 lines of helpers total).
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| The polling masks a slow operation that's now flaky | Low | Medium | Generous 5s timeout; if a test times out, the test message points to which operation |
|
||||
| Helper functions added in 2 places diverge | Medium | Low | If 3+ tests need the same helper, extract to `tests/helpers.py` |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- **State sync fix** — separate track (`live_gui_state_sync_20260605`).
|
||||
- **prior_session test** — separate track (`prior_session_test_harden_20260605`).
|
||||
- **Migrating other live_gui tests that use `time.sleep`** — out of scope for now. Track as a follow-up if more flakes appear.
|
||||
- **Replacing `time.sleep` with `asyncio.sleep`** — out of scope; the live_gui tests are sync, and the GUI event queue is sync.
|
||||
@@ -1,148 +0,0 @@
|
||||
# Prior-Session Sepia Tint — Design
|
||||
|
||||
**Date:** 2026-06-10
|
||||
**Status:** Approved (pending user spec review)
|
||||
**Track:** `prior_session_sepia_20260610`
|
||||
**Spec:** [../../../conductor/tracks/prior_session_sepia_20260610/spec.md](../../../conductor/tracks/prior_session_sepia_20260610/spec.md)
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The current prior-session (Historical Replay) mode uses `theme.get_color("bubble_vendor")` as a
|
||||
background tint at three call sites in `src/gui_2.py` (lines 1028, 3960, plus the "HISTORICAL
|
||||
VIEW" banners at 5142 and 5440). This is a **semantic overload** — `bubble_vendor` is the
|
||||
"Vendor API" role's bubble color, not a dedicated prior-session slot. Theme authors cannot tune
|
||||
the prior-session feel without changing the Vendor API bubble.
|
||||
|
||||
Beyond the missing dedicated slot, the **content** (text, markdown) renders at full
|
||||
palette saturation. The "obviously looking at the past" cue is carried only by the bg, not by
|
||||
the prose.
|
||||
|
||||
**Pre-existing related bug** (surfaced by the user during brainstorming): code blocks rendered
|
||||
by `imgui_color_text_edit` are not tonemap-aware. The library has 4 hardcoded palettes (`dark`,
|
||||
`light`, `mariana`, `retro_blue`) and their colors are never passed through `theme._tone_map()`.
|
||||
The user has called this "disappointing" because it defeats the primary purpose of the tonemapper:
|
||||
allowing a light theme to be usable on a bright monitor without searing the user's retinas.
|
||||
**This bug is NOT fixable in this track** — see "Honest constraint" below.
|
||||
|
||||
## Brainstorm Q&A
|
||||
|
||||
### Q1. Tint scope (data only vs. data+chrome vs. whole window)
|
||||
|
||||
**A1 confirmed sepia. A2 chose "anything that is actually affected by the prior session (is
|
||||
hosting old information, old state)" → scope (ii) data + chrome inside prior-session views, with
|
||||
fallback to (iii) whole window if scope (ii) doesn't look "obviously old" in manual review.**
|
||||
|
||||
### Q2. Two independent effects (distinct bg + tint) or single transform
|
||||
|
||||
**A — Two independent effects, slider only controls the content tint (recommended and chosen).**
|
||||
New theme slots: `prior_session_bg` (flat warm color), `prior_session_tint` (sepia color),
|
||||
`prior_session_amount` (per-palette float 0.0-1.0). Slider controls only the amount; bg is
|
||||
whatever the theme says.
|
||||
|
||||
### Q3. (a) Slider location, (b) tint scope, (c) default per-theme, (d) code blocks
|
||||
|
||||
**(a) Theme Settings panel** (under the existing Tone Mapping section). Mirrors the
|
||||
tonemap pattern exactly.
|
||||
|
||||
**(b) Scope (ii) data + chrome inside prior-session views**, with fallback to (iii) whole
|
||||
window only if (ii) doesn't look obviously old.
|
||||
|
||||
**(c) 0.3 (subtle)** — the user can slide up if they want more aggressive. The default is
|
||||
intentionally subtle because the user said "obviously looking at the past" but the
|
||||
prior-session feature is a niche mode, not a primary view.
|
||||
|
||||
**(d) Originally proposed: bundle the code-block tonemap fix into this track.** The user
|
||||
revealed the pre-existing disappointment and said: *"if you can somehow tint the code blocks
|
||||
lmk cause right now they are not affected by tonemapping."* The fix was proposed as
|
||||
"mutate the `Palette` struct's color slots via `editor.get_palette()` and re-apply."
|
||||
|
||||
### Q4. Float-only math (HARD CONSTRAINT)
|
||||
|
||||
> "Make sure that all math you do is not integer based. I want to have as much accuracy as
|
||||
> possible for smooth calculations."
|
||||
|
||||
Applied to: `apply_prior_tint`, the slider (`slider_float` not `slider_int`), the per-palette
|
||||
state dict (stores `float` not `int`), the TOML key (`prior_session_amount: float`), the
|
||||
code-block palette mutation. The transform pipeline never truncates to int.
|
||||
|
||||
## Approach: A1 — Per-render explicit transform
|
||||
|
||||
Add `apply_prior_tint(rgba, palette) -> rgba` helper in `src/theme_2.py`. Per-palette state
|
||||
lives in `_prior_session_amount: dict[str, float]` mirroring `_brightness` exactly. Each
|
||||
prior-session rendering site in `src/gui_2.py` wraps its `theme.get_color()` call with
|
||||
`apply_prior_tint(...)` (one-line wrap).
|
||||
|
||||
### Honest constraint surfaced during self-review
|
||||
|
||||
I verified the upstream `imgui_bundle 1.92.5` API before writing the plan:
|
||||
|
||||
```
|
||||
$ python -c "from imgui_bundle import imgui_color_text_edit as ed; help(ed.TextEditor.get_palette)"
|
||||
get_palette(self) -> imgui_bundle._imgui_bundle.imgui_color_text_edit.TextEditor.PaletteId
|
||||
$ python -c "from imgui_bundle import imgui_color_text_edit as ed; help(ed.TextEditor.set_palette)"
|
||||
set_palette(self, a_value: imgui_bundle._imgui_bundle.imgui_color_text_edit.TextEditor.PaletteId) -> None
|
||||
```
|
||||
|
||||
**`PaletteId` is a 4-value enum** (`dark`, `light`, `mariana`, `retro_blue`). There is
|
||||
no `Palette` struct with mutable per-color slots. The original brainstorm proposed
|
||||
`apply_color_grades_to_editor_palette(editor, palette)` that would mutate the struct
|
||||
in-place — but the struct doesn't exist in this API surface.
|
||||
|
||||
**This means the code-block tonemap-awareness is NOT fixable in this track** (or any
|
||||
track that doesn't fork the library). The user's disappointment is real and the
|
||||
pre-existing behavior persists. The same constraint forced the `multi_themes_20260604`
|
||||
track to ship a `syntax_palette` enum field rather than custom token colors. The
|
||||
honest answer is in the spec's §1.1.1.
|
||||
|
||||
### Why A1 over A2 (transparent via get_color) and A3 (context manager)
|
||||
|
||||
- **A2** would auto-apply sepia inside `theme.get_color()` when `is_viewing_prior_session` is
|
||||
True. Rejected: violates the data-oriented "view composes" principle; risks accidentally
|
||||
tinting status indicators that must stay saturated.
|
||||
- **A3** would require a `with prior_session_view():` wrapper at every prior-session site.
|
||||
Rejected: same number of wraps as A1 but uglier.
|
||||
|
||||
### Float-only math contract
|
||||
|
||||
```
|
||||
result = lerp(desaturate(input), tint_color, amount)
|
||||
```
|
||||
|
||||
- `desaturate(rgba)`: BT.709 luma `0.2126*r + 0.7152*g + 0.0722*b` (all float, all 0.0-1.0).
|
||||
- `lerp(a, b, t)`: `a + (b - a) * t` per channel, `t` clamped to [0.0, 1.0].
|
||||
- `apply_prior_tint(rgba, palette)`: identity at amount=0.0; pure tint at amount=1.0; alpha
|
||||
passed through unchanged; output is `tuple[float, float, float, float]`.
|
||||
|
||||
## File changes (high-level)
|
||||
|
||||
| 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; persist to 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 at the 2 banners + 2 render functions; 1 new Theme panel section |
|
||||
| `themes/*.toml` (8 files) | Modify | Add 3 new keys with per-theme defaults |
|
||||
| `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 | slider → save → restart round-trip |
|
||||
|
||||
## Risk assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Float math accumulates rounding error over many frames | Low | Low | Each `apply_prior_tint` call is independent; no accumulation; output clamped to [0.0, 1.0] |
|
||||
| The 8 themes don't all have sensible defaults for the new keys | Low | Low | Defaults in the fallback dict cover missing keys |
|
||||
| Light themes need a different default for `prior_session_bg` (cream vs. dark brown) | Med | Low | Defaults table in the spec sets per-light-theme `prior_session_bg = (235, 220, 190)` |
|
||||
| Live-gui tests regress because of the new `apply_prior_tint` wraps | Low | Med | Phase 4 batch-verifies the full suite; per `live_gui_test_hardening_20260605` rule, batch is the only verification that matters |
|
||||
| The scope (ii) "data + chrome inside prior-session views" doesn't look "obviously old" at the 0.3 default | Med | Low | Phase 4 manual smoke escalates to scope (iii) if needed; the wrap is local to 6 sites, easy to expand |
|
||||
| **Code-block tonemap-awareness disappoints the user again** | High | Low | Explicit §1.1.1 in the spec; the pre-existing bug persists; user was told upfront in the design doc that the API doesn't support per-instance Palette |
|
||||
|
||||
## Out of scope (matches spec §9)
|
||||
|
||||
- Per-language syntax color customization (upstream limitation)
|
||||
- Film grain / vignette / scanline post-effect
|
||||
- Per-theme user overrides via `config.toml` (TOML key is factory default only)
|
||||
- Modifying the upstream `imgui_color_text_edit` library
|
||||
- Process-isolation of the pure helper
|
||||
- Reorganizing the existing tonemap state dicts
|
||||
@@ -1,444 +0,0 @@
|
||||
# Chronology v2 Redo — Design Spec
|
||||
|
||||
**Date:** 2026-07-01
|
||||
**Track ID:** `chronology_v2_20260701`
|
||||
**Priority:** A (meta-tooling / infrastructure)
|
||||
**Status:** design (pre-spec)
|
||||
**Ancestors:**
|
||||
- `conductor/tracks/chronology_20260619/spec.md` (the v2 rewrite spec, 354 lines — designed but never executed)
|
||||
- `docs/reports/2026-06-15/CHRONOLOGY_TRACK_HANDOVER_20260620.md` (the v1 failure report, 128 lines)
|
||||
- `docs/reports/2026-06-15/CHRONOLOGY_MIGRATION_20260619.md` (the v1 migration report)
|
||||
- `docs/reports/2026-06-15/TRACK_COMPLETION_chronology_20260619.md` (the v1 end-of-track report)
|
||||
|
||||
## Overview
|
||||
|
||||
The `chronology_20260619` track produced a broken `conductor/chronology.md` (v1):
|
||||
167 of 216 rows had wrong status (the classifier read stale `metadata.json.status`
|
||||
instead of git history), summaries were metadata-field text instead of track
|
||||
descriptions, and the per-row cross-check was bypassed. A v2 rewrite was specced
|
||||
and planned in detail but never executed. The track sits at `current_phase=10`
|
||||
pending user sign-off that never came, blocking `superpowers_review_20260619`.
|
||||
|
||||
This track is the redo: a **fresh track** that closes out the old one, adopts
|
||||
the v2 design as a starting point, revises it for the current project state
|
||||
(5+ days of desync, new track patterns, the tracks.md bloat), and executes it
|
||||
through to a user sign-off that is actionable this time.
|
||||
|
||||
## What v1 Got Wrong (from the records)
|
||||
|
||||
Per `CHRONOLOGY_TRACK_HANDOVER_20260620.md`:
|
||||
|
||||
1. **`_classify_status()` reads `metadata.json.status`** — a stale field set when
|
||||
each track was created, rarely updated when work completed or was abandoned.
|
||||
167/216 rows had wrong status.
|
||||
2. **Summaries are metadata-field text** (`**Priority:** A (foundational...)`,
|
||||
`**Date:** 2026-06-20`) not actual track descriptions.
|
||||
3. **Phase 8 per-row cross-check was bypassed** in favor of bulk structural
|
||||
verification; the manual summary-adequacy check was partial (15-row sample).
|
||||
4. **Phase 6 user review gate was bypassed** in the autonomous session.
|
||||
5. **No quality gate** to detect a broken classifier before the chronology ships.
|
||||
6. **No maintenance plan** — the chronology desynced within days because nobody
|
||||
regenerated it after new tracks shipped.
|
||||
|
||||
The five lessons from the handover (lines 88-98):
|
||||
1. Bypassing the manual review clause was the original sin.
|
||||
2. `metadata.json` is a snapshot, not a source of truth.
|
||||
3. Git history is the project's audit log — use it.
|
||||
4. Default to "when in doubt, ask" — the chronology is read by humans.
|
||||
5. The user said "manual review" twice; both times an interpretation was found
|
||||
to be less strict — listen to the literal request.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Produce a correct `conductor/chronology.md` where every row's status is
|
||||
backed by git-history evidence, not stale metadata.
|
||||
2. Produce a per-row evidence artifact (the quality report) so the user can
|
||||
audit the classification without re-deriving it.
|
||||
3. Close out `chronology_20260619` (mark superseded, archive, unblock
|
||||
`superpowers_review_20260619`).
|
||||
4. De-gunk `conductor/tracks.md` — remove shipped/completed tracks from the
|
||||
active queue, remove the Phase 0-9 history sections that duplicate
|
||||
chronology.md, leave only the active queue + standby + a pointer.
|
||||
5. Add a `conductor/workflow.md` maintenance rule so the chronology is
|
||||
regenerated after each track ships (closes the desync root cause).
|
||||
6. Ship a quality-gate script that catches a broken classifier before it
|
||||
ships (closes the "no quality gate" root cause).
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Fixing or executing `superpowers_review_20260619` (just unblocking it).
|
||||
- Changing how `metadata.json.status` is maintained going forward (the
|
||||
classifier uses git history, not metadata; metadata staleness is no longer
|
||||
the problem).
|
||||
- Archiving the 66 folders in `conductor/tracks/` that are already shipped
|
||||
(separate cleanup; the chronology indexes them regardless of location).
|
||||
- Renaming or restructuring `conductor/archive/` (out of scope; the
|
||||
chronology walks it as-is).
|
||||
- A broader `workflow.md` review for other stale rules (the only workflow.md
|
||||
change is the chronology maintenance section).
|
||||
|
||||
## Design
|
||||
|
||||
### 1. New track identity + old track close-out
|
||||
|
||||
**New track:** `chronology_v2_20260701` (Priority A; meta-tooling/infrastructure).
|
||||
Fresh track, not a continuation of `chronology_20260619`.
|
||||
|
||||
**Old track close-out (Phase 1):**
|
||||
- Mark `chronology_20260619` as superseded in its `state.toml`
|
||||
(`status = "superseded"`, `current_phase = 10`, add a `[supersession]`
|
||||
section pointing to `chronology_v2_20260701`).
|
||||
- Update its tracks.md row (line 64) to reflect supersession.
|
||||
- Archive `conductor/tracks/chronology_20260619/` →
|
||||
`conductor/archive/chronology_20260619/`. The v2 spec/plan are preserved
|
||||
in git history; the new track references them by commit SHA.
|
||||
- **Unblock `superpowers_review_20260619`** — remove `chronology_20260619`
|
||||
from its `state.toml` `[blocked_by]` entirely (no re-gating on the new
|
||||
track).
|
||||
|
||||
**v2 design adoption:** The new track's spec explicitly cites
|
||||
`conductor/tracks/chronology_20260619/spec.md` (the v2 rewrite spec) and
|
||||
`CHRONOLOGY_TRACK_HANDOVER_20260620.md` (the failure report) as its design
|
||||
ancestors. It adopts the v2 status enum, the git-history classifier approach,
|
||||
and the quality-gate concept — with the revisions below.
|
||||
|
||||
### 2. The six revisions to v2
|
||||
|
||||
#### Revision 1 — The desync gap (regenerate from current filesystem)
|
||||
|
||||
v2 was specced when the newest track was ~2026-06-20. The chronology now needs
|
||||
to cover 5+ more days of tracks: the layout saga
|
||||
(`default_layout_install_20260629`, `default_layout_extract_20260629`,
|
||||
`default_layout_install_followup_20260629`), the MMA quarantine
|
||||
(`mma_quarantine_rag_test_decoupling_20260701`), the module_taxonomy abort +
|
||||
cleanup (`module_taxonomy_refactor_20260627`, `post_module_taxonomy_de_cruft_20260627`),
|
||||
`cruft_elimination_20260627`, `directive_hotswap_harness_20260627`,
|
||||
`enforcement_gap_closure_20260627`, `test_engine_integration_20260627`,
|
||||
`fix_mma_concurrent_tracks_sim_20260627`, `type_alias_unfuck_20260626`,
|
||||
`video_analysis_campaign_2_20260627`.
|
||||
|
||||
**Change:** The new track's first generation pass runs against the **current**
|
||||
filesystem (all `conductor/tracks/` + `conductor/archive/` as of execution
|
||||
day), not the 2026-06-19 snapshot. The generation script walks both directories
|
||||
fresh each run.
|
||||
|
||||
#### Revision 2 — `superpowers_review_20260619` blocker resolution
|
||||
|
||||
v2 didn't address this because it was rewriting the same track in place. The
|
||||
new track explicitly closes out `chronology_20260619` and removes it from
|
||||
`superpowers_review_20260619/state.toml` `[blocked_by]` (no re-gating).
|
||||
|
||||
#### Revision 3 — Classifier heuristics updated for recent track patterns
|
||||
|
||||
v2's 5-step git-history algorithm was designed 2026-06-20. Since then, new
|
||||
patterns emerged that it would misclassify:
|
||||
|
||||
- **Aborted tracks** (`module_taxonomy_refactor_20260627`): many
|
||||
`conductor(track):` + `conductor(plan):` commits but a
|
||||
`TRACK_ABORTED_*.md` report — classifier must detect the abort report as
|
||||
an `Abandoned`/`Superseded` signal.
|
||||
- **Phase 9 patches after "completion"** (`result_migration_cruft_removal_20260620`):
|
||||
a track that "shipped" then got a patch commit days later — classifier must
|
||||
look at the latest commit, not just count.
|
||||
- **Tier 2 autonomous tracks**: produce many `conductor(plan):` commits (one
|
||||
per task) — the "feat/fix/refactor vs chore/docs" heuristic must not count
|
||||
`conductor(plan):` as a work commit.
|
||||
- **Follow-up tracks** (`default_layout_install_followup_20260629`): short,
|
||||
few commits, but legitimately `Completed` — the "0-1 commits + >14 days
|
||||
old = Abandoned" rule would misfire.
|
||||
|
||||
**Change:** The classifier's commit-message pattern list is extended:
|
||||
|
||||
- `conductor(plan):`, `conductor(state):`, `conductor(track):`,
|
||||
`docs(spec):`, `docs(plan):` are **metadata commits**, not work commits
|
||||
(don't count toward the "≥3 work commits = Completed" threshold).
|
||||
- `feat:`, `fix:`, `refactor:`, `perf:`, `test:`, `docs(report):` are work
|
||||
commits.
|
||||
- Presence of `TRACK_ABORTED_*.md` or `TRACK_COMPLETION_*.md` in
|
||||
`docs/reports/` matching the track ID is a **strong signal** that
|
||||
overrides commit-count heuristics.
|
||||
- The "last commit > 14 days = Abandoned" rule is **removed**; replaced
|
||||
with "no work commits AND no completion/abort report = Needs Review".
|
||||
- Confidence is reported per-row; anything below a threshold goes to the
|
||||
Needs Review queue for manual classification.
|
||||
|
||||
#### Revision 4 — tracks.md de-gunk
|
||||
|
||||
v2's scope was only chronology.md. The new track also restructures
|
||||
`conductor/tracks.md`:
|
||||
|
||||
**Current state (96KB, bloated):**
|
||||
- 60-row "Active Tracks (Current Queue)" table — ~40 of these rows are
|
||||
shipped/completed tracks that belong in history, not the active queue.
|
||||
- Phase 0-9 chronological sections with Completed/Archived subsections —
|
||||
duplicates chronology.md.
|
||||
- 4 backlog/follow-up sections — some entries are shipped, some pending.
|
||||
- "Recently Shipped Tracks (2026-06-29)" section at the bottom.
|
||||
|
||||
**Target state:**
|
||||
- **Section 1: Active Queue** — only tracks that are genuinely unblocked and
|
||||
ready to start OR in-progress. Shipped tracks are removed (they're in
|
||||
chronology.md). Each row: `| # | Priority | Track | Status | Blocked By |`
|
||||
(same columns, filtered to active-only).
|
||||
- **Section 2: Standby / Pending Spec** — tracks with spec TBD or pending
|
||||
decision (the backlog). Same columns.
|
||||
- **Section 3: Pointer** — one line:
|
||||
`> Full project history: see [chronology.md](./chronology.md)`
|
||||
- **Delete:** Phase 0-9 sections, Completed/Archived subsections, backlog/
|
||||
follow-up sections that duplicate chronology.md, the "Recently Shipped"
|
||||
section, the "Archived (Closed 2026-06-23)" video analysis section.
|
||||
- **Keep:** the "Editing this file" / archiving convention notes at the
|
||||
bottom (from v1 Phase 4).
|
||||
|
||||
**Migration safety:** the full tracks.md is preserved in git history; the
|
||||
de-gunk is a single commit. If anything is lost,
|
||||
`git show HEAD~1:conductor/tracks.md` recovers it.
|
||||
|
||||
#### Revision 5 — workflow.md maintenance rule
|
||||
|
||||
v2 had no maintenance plan (the root cause of the desync). The new track adds
|
||||
a section to `conductor/workflow.md`:
|
||||
|
||||
**New subsection under "Documentation Refresh Protocol"** (or a new top-level
|
||||
section "Chronology Maintenance"):
|
||||
|
||||
> **Chronology regeneration cadence.** After every track ships (completion
|
||||
> commit + TRACK_COMPLETION report), the implementing agent must run
|
||||
> `uv run python scripts/audit/generate_chronology.py` to regenerate
|
||||
> `conductor/chronology.md`. The regeneration is a single atomic commit
|
||||
> (`docs(chronology): regenerate after <track-id> shipped`). If the
|
||||
> regeneration produces a diff beyond the new row (e.g., status changes on
|
||||
> other rows), the agent must investigate before committing — a status drift
|
||||
> on an unrelated row indicates a stale classifier, not a chronology bug.
|
||||
>
|
||||
> **Quality gate.** `scripts/audit/chronology_quality_gate.py` runs as part
|
||||
> of the regeneration. It fails (exit 1) if >30% of rows are classified as
|
||||
> `Needs Review`. A failing quality gate blocks the regeneration commit.
|
||||
|
||||
This makes regeneration a per-track-shipping obligation, not a one-shot.
|
||||
|
||||
#### Revision 6 — Report(s)
|
||||
|
||||
Two reports:
|
||||
|
||||
1. **`docs/reports/TRACK_COMPLETION_chronology_v2_20260701.md`** — the
|
||||
standard end-of-track report (what was done, files changed, verification
|
||||
results).
|
||||
2. **`docs/reports/CHRONOLOGY_QUALITY_20260701.md`** — the chronology-quality
|
||||
report (new, not in v1). Contents:
|
||||
- Total rows generated + breakdown by status (Active / In Progress /
|
||||
Completed / Abandoned / Superseded / Special / Needs Review)
|
||||
- Confidence distribution (high / medium / low)
|
||||
- The Needs Review queue (list of rows that need manual classification,
|
||||
with the evidence the classifier found)
|
||||
- Comparison vs v1 (row count delta, status-correction count: "N rows
|
||||
changed status vs v1")
|
||||
- The desync gap closed (list of tracks added that were missing from v1)
|
||||
- Classifier heuristics summary (which patterns matched, which were
|
||||
overridden by completion/abort reports)
|
||||
|
||||
The quality report is the evidence artifact — it's what makes this track
|
||||
auditable rather than "trust the script." v1 failed because there was no
|
||||
quality gate and no evidence per row; this report is the fix.
|
||||
|
||||
### 3. Architecture — the generation script + quality gate
|
||||
|
||||
#### `scripts/audit/generate_chronology.py` (rewritten)
|
||||
|
||||
**Inputs:** `conductor/tracks/` + `conductor/archive/` (walked fresh each
|
||||
run); `git log` per folder for commit evidence; `docs/reports/TRACK_COMPLETION_*.md`
|
||||
+ `TRACK_ABORTED_*.md` for override signals.
|
||||
|
||||
**Extraction pipeline (per folder):**
|
||||
1. **Date** — slug date from folder name (regex, unchanged from v1).
|
||||
2. **ID** — folder name (unchanged).
|
||||
3. **Status** — the new classifier (see below), returns
|
||||
`(status, confidence, reason)`.
|
||||
4. **Summary** — rewritten extractor: rejects lines starting with
|
||||
`**Priority:**`, `**Date:**`, `**Initialized:**`, `**Track:**`,
|
||||
`**Parent umbrella:**`, `**Status:**`, `**Confidence:**`; prefers
|
||||
`metadata.json.description` if it's actual prose (not metadata-field
|
||||
text); falls back to first non-heading, non-metadata line of `spec.md`;
|
||||
truncates to 25 words.
|
||||
5. **Folder** — path (unchanged).
|
||||
6. **Range** — `git log --oneline -- <folder>` → first + last SHA + count.
|
||||
|
||||
**The new classifier (`_classify_status`, returning `(status, confidence, reason)`):**
|
||||
|
||||
Evidence sources, in priority order:
|
||||
|
||||
1. **Override signals (highest confidence):**
|
||||
- `TRACK_COMPLETION_*.md` exists in `docs/reports/` matching this track
|
||||
ID → `Completed`, confidence=high, reason="completion report found".
|
||||
- `TRACK_ABORTED_*.md` exists → `Abandoned`, confidence=high,
|
||||
reason="abort report found". (If `state.toml` also says `superseded`,
|
||||
the `Superseded` classification wins — see next row.)
|
||||
- `state.toml` `status = "superseded"` → `Superseded`,
|
||||
confidence=high (overrides the abort-report signal if both exist).
|
||||
2. **Git commit evidence (medium confidence):**
|
||||
- Count work commits (`feat/fix/refactor/perf/test/docs(report):` prefixes)
|
||||
via `git log --oneline -- <folder>`, excluding metadata commits
|
||||
(`conductor(plan):`, `conductor(state):`, `conductor(track):`,
|
||||
`docs(spec):`, `docs(plan):`).
|
||||
- ≥3 work commits → `Completed`, confidence=medium, reason="N work commits".
|
||||
- 1-2 work commits + in `tracks/` → `In Progress`, confidence=medium.
|
||||
- 0 work commits + in `tracks/` → `Active` (spec/plan only),
|
||||
confidence=medium.
|
||||
3. **Directory location (low confidence):**
|
||||
- In `archive/` + no override signal → `Completed`, confidence=low,
|
||||
reason="archived but no completion report".
|
||||
- In `archive/` + 0 commits → `Abandoned`, confidence=low,
|
||||
reason="archived with 0 commits".
|
||||
4. **Fallback:** `Needs Review`, confidence=none,
|
||||
reason="classifier inconclusive".
|
||||
|
||||
**Status enum:** `Active` / `In Progress` / `Completed` / `Abandoned` /
|
||||
`Superseded` / `Special` / `Needs Review` (7 values; v2 had 5, adding
|
||||
`Superseded` + `Needs Review`).
|
||||
|
||||
**Output format:** Markdown table with 6 columns (Date, ID, Status, Summary,
|
||||
Folder, Range) + a **"Needs Review" section** at the bottom listing rows with
|
||||
`Needs Review` status, each with its evidence reason. Sorted newest-first. A
|
||||
preamble header with generation date + row count.
|
||||
|
||||
#### `scripts/audit/chronology_quality_gate.py` (new)
|
||||
|
||||
**Purpose:** detect a broken classifier before the chronology ships.
|
||||
|
||||
**Checks:**
|
||||
- **Needs Review threshold:** if >30% of rows are `Needs Review`, exit 1
|
||||
(the classifier is failing on too many rows).
|
||||
- **Status distribution sanity:** if 0 rows are `Completed`, exit 1 (the
|
||||
classifier is misclassifying everything).
|
||||
- **Summary quality:** if >20% of summaries still contain metadata-field
|
||||
text (`**Priority:**` etc.), exit 1 (the summary extractor is broken).
|
||||
- **Per-row evidence:** every row must have a non-empty `reason` from the
|
||||
classifier; if any row has no reason, exit 1.
|
||||
|
||||
**Modes:** default informational (exits 0, prints report); `--strict` CI
|
||||
gate (exits 1 on any violation). Follows the project's audit-script
|
||||
convention (per `conductor/workflow.md` "Audit Script Policy").
|
||||
|
||||
#### Tests (TDD)
|
||||
|
||||
`tests/test_generate_chronology.py` (rewritten) +
|
||||
`tests/test_chronology_quality_gate.py` (new). Tests for:
|
||||
- The classifier's 7 status values + the evidence priority chain (override
|
||||
signals > git evidence > directory > fallback).
|
||||
- The summary extractor's rejection of metadata-field lines.
|
||||
- The quality gate's 4 checks.
|
||||
- Edge cases: aborted tracks with completion reports (override conflict),
|
||||
tracks with 0 commits, archive folders with no metadata.json.
|
||||
|
||||
### 4. Execution plan structure (phases)
|
||||
|
||||
6 phases, each a checkpoint with atomic per-task commits.
|
||||
|
||||
#### Phase 1: Close out the old track + scaffold the new one
|
||||
- Task 1.1: Update `chronology_20260619/state.toml` →
|
||||
`status = "superseded"`, add `[supersession]` section. Commit.
|
||||
- Task 1.2: Update `chronology_20260619` row in tracks.md (line 64) to
|
||||
"superseded by `chronology_v2_20260701`". Commit.
|
||||
- Task 1.3: Archive `conductor/tracks/chronology_20260619/` →
|
||||
`conductor/archive/chronology_20260619/`. Commit.
|
||||
- Task 1.4: Update `superpowers_review_20260619/state.toml` `[blocked_by]` —
|
||||
remove `chronology_20260619` entirely. Commit.
|
||||
- Task 1.5: Create `conductor/tracks/chronology_v2_20260701/` with
|
||||
`spec.md`, `metadata.json`, `state.toml`, `plan.md`. Commit.
|
||||
|
||||
#### Phase 2: TDD the classifier + quality gate (Red)
|
||||
- Task 2.1: Write `tests/test_generate_chronology.py` — tests for the
|
||||
7-status classifier, evidence priority chain, summary extractor. Red.
|
||||
- Task 2.2: Write `tests/test_chronology_quality_gate.py` — tests for the
|
||||
4 quality-gate checks. Red.
|
||||
|
||||
#### Phase 3: Implement the classifier + quality gate (Green)
|
||||
- Task 3.1: Rewrite `scripts/audit/generate_chronology.py` — the new
|
||||
`_classify_status` returning `(status, confidence, reason)`, the
|
||||
rewritten summary extractor, the git-history evidence pipeline. Green.
|
||||
- Task 3.2: Create `scripts/audit/chronology_quality_gate.py` — the 4
|
||||
checks + `--strict` mode. Green.
|
||||
|
||||
#### Phase 4: Regenerate chronology.md + write the quality report
|
||||
- Task 4.1: Run the generator against the current filesystem. Capture
|
||||
output to `conductor/chronology.md` (replacing v1). Commit.
|
||||
- Task 4.2: Run the quality gate. If it fails, iterate on the classifier
|
||||
(back to Phase 3) until it passes. Commit the passing state.
|
||||
- Task 4.3: Write `docs/reports/CHRONOLOGY_QUALITY_20260701.md` — the
|
||||
quality report. Commit.
|
||||
|
||||
#### Phase 5: De-gunk tracks.md + add workflow.md maintenance rule
|
||||
- Task 5.1: Restructure `conductor/tracks.md` — remove shipped/completed
|
||||
rows from the active queue, remove Phase 0-9 history sections, remove
|
||||
backlog/follow-up sections that duplicate chronology.md, add the pointer
|
||||
to chronology.md, keep the "Editing this file" notes. Single commit.
|
||||
- Task 5.2: Add the "Chronology Maintenance" section to
|
||||
`conductor/workflow.md` — the regeneration cadence + quality gate
|
||||
obligation. Commit.
|
||||
|
||||
#### Phase 6: Verification + end-of-track report
|
||||
- Task 6.1: Run the quality gate `--strict` mode. Confirm exit 0. Commit.
|
||||
- Task 6.2: Verify the Needs Review queue is empty or small (the user
|
||||
reviews any remaining rows). Commit.
|
||||
- Task 6.3: Write
|
||||
`docs/reports/TRACK_COMPLETION_chronology_v2_20260701.md`. Commit.
|
||||
- Task 6.4: User sign-off (the final gate — same as v1's Phase 10, but
|
||||
this time the quality report + evidence per row makes it actionable).
|
||||
|
||||
### Commit strategy
|
||||
|
||||
- Per-task atomic commits (no batching).
|
||||
- Git notes per commit (task summary).
|
||||
- Phase checkpoints after each phase (per the workflow protocol).
|
||||
|
||||
## Verification Criteria
|
||||
|
||||
1. `conductor/chronology.md` exists with one row per track folder (tracks/
|
||||
+ archive/), sorted newest-first, 6 columns, generated from the current
|
||||
filesystem (no 2026-06-19 snapshot pin).
|
||||
2. Every row's status is backed by git-history evidence (not
|
||||
`metadata.json.status`); the evidence `reason` is non-empty for every
|
||||
row.
|
||||
3. No summary contains metadata-field text (`**Priority:**`, `**Date:**`,
|
||||
`**Initialized:**`, `**Track:**`, `**Parent umbrella:**`,
|
||||
`**Status:**`, `**Confidence:**`).
|
||||
4. `scripts/audit/chronology_quality_gate.py --strict` exits 0.
|
||||
5. `conductor/tracks.md` contains only the active queue + standby/pending +
|
||||
a pointer to chronology.md + the "Editing this file" notes. No Phase 0-9
|
||||
history sections, no shipped-track rows in the active queue.
|
||||
6. `conductor/workflow.md` contains the "Chronology Maintenance" section
|
||||
(regeneration cadence + quality gate obligation).
|
||||
7. `docs/reports/CHRONOLOGY_QUALITY_20260701.md` exists with the status
|
||||
distribution, confidence distribution, Needs Review queue, v1
|
||||
comparison, desync gap list, and heuristics summary.
|
||||
8. `docs/reports/TRACK_COMPLETION_chronology_v2_20260701.md` exists.
|
||||
9. `chronology_20260619` is archived (in `conductor/archive/`) with
|
||||
`status = "superseded"` in its state.toml.
|
||||
10. `superpowers_review_20260619/state.toml` `[blocked_by]` no longer
|
||||
contains `chronology_20260619`.
|
||||
11. `tests/test_generate_chronology.py` +
|
||||
`tests/test_chronology_quality_gate.py` pass.
|
||||
12. User sign-off recorded in the TRACK_COMPLETION report.
|
||||
|
||||
## Risks
|
||||
|
||||
- **R1 (medium):** The git-history classifier may still misclassify some
|
||||
edge cases (e.g., tracks with `conductor(checkpoint):` commits only).
|
||||
Mitigation: the Needs Review queue surfaces these for manual
|
||||
classification; the quality gate fails if >30% are Needs Review.
|
||||
- **R2 (medium):** The tracks.md de-gunk may accidentally remove a row
|
||||
that's still active. Mitigation: the full tracks.md is preserved in git
|
||||
history; recovery is `git show HEAD~1:conductor/tracks.md`.
|
||||
- **R3 (low):** The workflow.md maintenance rule may not be followed by
|
||||
future agents. Mitigation: the rule is in the operational workflow doc
|
||||
that agents read at session start; the quality gate catches a desync
|
||||
when the next regeneration runs.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Fixing or executing `superpowers_review_20260619` (just unblocking it).
|
||||
- Changing how `metadata.json.status` is maintained going forward.
|
||||
- Archiving the 66 folders in `conductor/tracks/` that are already shipped.
|
||||
- Renaming or restructuring `conductor/archive/`.
|
||||
- A broader `workflow.md` review for other stale rules.
|
||||
- The `superpowers_review_20260619` track's execution.
|
||||
@@ -1,192 +0,0 @@
|
||||
# Design: MMA Quarantine + RAG Test Decoupling
|
||||
|
||||
**Date:** 2026-07-01
|
||||
**Status:** Draft (pending user review)
|
||||
**Scope:** Two surgical interventions. (1) Quarantine the MMA automation engine behind a config flag so it stops consuming test-suite time and stops breaking when adjacent code changes. (2) Decouple the RAG tests from the live_gui subprocess + chromadb file locks so the default test batch stops bleeding on RAG.
|
||||
|
||||
**Out of scope:** The discussion/session system redesign (owned by the nagent research track). Full removal of MMA code (deferred to a follow-up track if quarantine maintenance becomes painful). Any change to the RAG algorithm itself (`index_file`, `search`, chunking — unchanged). The `conductor/` track system (the `conductor_tech_lead.py` / `project_manager.py` / `dag_engine.py` shared-types layer is preserved as load-bearing).
|
||||
|
||||
## Context
|
||||
|
||||
### Why this work (grounded in git history, not track docs)
|
||||
|
||||
The last 10 days of git history (2026-06-20 through 2026-06-30) show two subsystems consuming disproportionate debugging time:
|
||||
|
||||
**RAG test debugging churn (2026-06-27):** ~10 commits chasing RAG test failures — `_get_chromadb()` NameError in dim check, file-lock dim-check failures, silent `index_file` no-ops on missing files, session-scoped subprocess pollution, hotpatched state instead of project-switch. Two "ADDENDUM" reports because the first root-cause diagnosis was wrong. The commits are fighting the *environment* (chromadb file locks under the live_gui subprocess, CWD drift across the spawn boundary, lazy global teardown/rebuild), not the RAG algorithm.
|
||||
|
||||
**MMA concurrent tracks sim fix (2026-06-27):** `fix_mma_concurrent_tracks_sim_20260627` — 5 fixes to `mock_concurrent_mma.py` (session_id fallback removal, epic branch catch-all, sprint routing by prompt content) plus a `refresh_from_project` task that was overwriting `self.tracks`. The mock itself is brittle; the production engine is brittle in the same shape.
|
||||
|
||||
The user's direction: sunset MMA constructively (quarantine, not delete — full removal is a follow-up if quarantine maintenance hurts), keep RAG but make the tests sane.
|
||||
|
||||
### The shared-types finding (why this is quarantine, not removal)
|
||||
|
||||
`src/mma.py` is a shared types module, not just the MMA engine. Non-MMA code depends on it:
|
||||
- `thinking_parser.py` → `ThinkingSegment` (thinking-trace parsing, used in every AI turn — NOT an MMA feature)
|
||||
- `project_manager.py` → `TrackState`, `EMPTY_TRACK_STATE` (the conductor track system — persists `conductor/tracks/<id>/state.toml`)
|
||||
- `models.py` → `TrackMetadata` (re-exported as legacy `Metadata` alias)
|
||||
- `dag_engine.py` → `Ticket`
|
||||
- `conductor_tech_lead.py` → `Ticket`, `TrackDAG`
|
||||
|
||||
`dag_engine.py` is shared between the MMA loop AND `conductor_tech_lead.py` (the Tier 2 tech lead system). Full removal would require migrating `ThinkingSegment` and `TrackState`/`TrackMetadata` out of `mma.py` first — a mutation of the shared-types module that risks the conductor track system. That work is deferred to a follow-up track.
|
||||
|
||||
### The RAG fragility root cause
|
||||
|
||||
The RAG algorithm is ~50 lines (`index_file` + `search` + chunking). The other ~250 lines of `rag_engine.py` are defensive scaffolding for the test/subprocess environment:
|
||||
- `_validate_collection_dim_result` uses `shutil.rmtree(ignore_errors=True)` to survive Windows file locks held by the live_gui subprocess
|
||||
- `index_file` has a CWD-fallback band-aid for path resolution drift across the spawn boundary
|
||||
- `_sync_rag_engine` is called from 6 sites in `app_controller.py` (files change, project switch, session reset, RAG toggle) — 6 race surfaces
|
||||
- Lazy module globals (`_CHROMADB`, `_GOOGLE_GENAI`, `_SENTENCE_TRANSFORMERS`) tear down and rebuild per-test with no guarantee of state
|
||||
|
||||
The RAG *tests* are testing "does RAG survive the live_gui subprocess lifecycle + chromadb file locks + CWD drift + lazy global teardown/rebuild" — not "does RAG retrieve relevant chunks." That's the bleed.
|
||||
|
||||
## Section 1: MMA Quarantine
|
||||
|
||||
### Mechanism
|
||||
|
||||
Config flag `mma.enabled`, default `false`, in `[ai_settings.toml]` (the per-project settings file, not `manual_slop.toml` which is project-static config). Per `conductor/code_styleguides/feature_flags.md` §2: this is a persistent preference (off by default, not recoverable by a single regenerate command), so config flag + GUI checkbox is the correct pattern — not file presence, not env-var-only.
|
||||
|
||||
### What the flag gates
|
||||
|
||||
#### 1.1 `app_controller.py` (~20 sites)
|
||||
|
||||
State fields initialized to empty/zero-init when `mma.enabled == false`:
|
||||
- `self.engines: Dict[str, ConductorEngine] = {}` (stays empty)
|
||||
- `self.mma_streams: Dict[str, str] = {}` (stays empty)
|
||||
- `self.mma_step_mode: bool = False` (stays False)
|
||||
- `self.mma_tier_usage: Dict[str, Metadata] = {...}` (stays zero-init)
|
||||
- `self.tracks: list[Metadata] = []` (stays empty — note: this is the MMA track list, NOT the conductor track system; `project_manager.get_all_tracks` is still callable for the conductor UI if needed)
|
||||
|
||||
Engine methods return early when the flag is off:
|
||||
- `start_mma` / engine-start paths — return early, no `ConductorEngine` instantiation
|
||||
- `approve_step` / `approve_spawn` — return early, no-op
|
||||
- abort / reset paths — clear the (already empty) state
|
||||
|
||||
The `multi_agent_conductor` import becomes lazy: imported inside the gated methods only, gated on the flag. When the flag is off, the module is never imported at runtime (reduces import-graph weight).
|
||||
|
||||
The `rag_engine` sync paths are untouched — RAG is a separate subsystem (Section 2).
|
||||
|
||||
#### 1.2 `gui_2.py` (12 render functions + dashboard window + modals)
|
||||
|
||||
Every `render_mma_*` function and `render_task_dag_panel` checks `if not app.controller.mma_enabled: return` at the top:
|
||||
- `render_mma_dashboard` (gui_2.py:6610)
|
||||
- `render_mma_modals` (gui_2.py:6658)
|
||||
- `render_mma_track_summary` (gui_2.py:6757)
|
||||
- `render_mma_epic_planner` (gui_2.py:6800)
|
||||
- `render_mma_conductor_setup` (gui_2.py:6818)
|
||||
- `render_mma_track_browser` (gui_2.py:6837)
|
||||
- `render_mma_global_controls` (gui_2.py:6884)
|
||||
- `render_mma_usage_section` (gui_2.py:6924)
|
||||
- `render_mma_ticket_editor` (gui_2.py:7003)
|
||||
- `render_mma_agent_streams` (gui_2.py:7043)
|
||||
- `render_task_dag_panel` (gui_2.py:7351)
|
||||
- `render_mma_focus_selector` (gui_2.py:7570)
|
||||
|
||||
The MMA Dashboard window is not registered in the panel registry when the flag is off (gui_2.py:1995 `_render_window_if_open("MMA Dashboard", ...)` — gated on flag). The approval modals (MMA Step Approval, MMA Spawn Approval) no-op. The ~600 lines of render code stay in-tree but are dead at runtime.
|
||||
|
||||
#### 1.3 `multi_agent_conductor.py`
|
||||
|
||||
Kept in-tree. Imported lazily only inside the gated `app_controller` methods. `ConductorEngine` / `WorkerPool` never instantiate when the flag is off.
|
||||
|
||||
#### 1.4 What stays active (shared types, NOT gated)
|
||||
|
||||
- `mma.py` — `Ticket`, `Track`, `TrackState`, `TrackMetadata`, `WorkerContext`, `ThinkingSegment` — load-bearing for non-MMA code
|
||||
- `dag_engine.py` — `TrackDAG`, `ExecutionEngine` — used by `conductor_tech_lead.py`
|
||||
- `mma_prompts.py` — used by `ai_client.py`, `conductor_tech_lead.py`, `orchestrator_pm.py` (verify whether these usages are MMA-specific or general during implementation; if MMA-specific, gate; if general, leave)
|
||||
- `mma.py` / `dag_engine.py` / `mma_prompts.py` imports in non-MMA consumers remain unchanged
|
||||
|
||||
#### 1.5 GUI surface
|
||||
|
||||
A single `[ ] Enable MMA (deprecated, quarantined)` checkbox in AI Settings. The full MMA dashboard is hidden when the flag is off. The checkbox is the only MMA UI surface. Per `feature_flags.md` §2: the GUI checkbox is a projection of the config file; the config file is the source of truth.
|
||||
|
||||
#### 1.6 Tests (env-gated, opt-in)
|
||||
|
||||
MMA tests gated behind `SLOP_MMA_TESTS=1` env var via `@pytest.mark.skipif`. Skip reason documents: "MMA is quarantined; run with `SLOP_MMA_TESTS=1` to enable." This is the *test* gate — separate from the *runtime* config flag, per `feature_flags.md` §6 (layered flags: file presence / config for runtime, env var for test opt-in).
|
||||
|
||||
Affected test files (the `test_mma_*`, `test_concurrent_*`, `test_conductor_*`, `test_dag_*`, `test_parallel_*`, `test_worker_*`, `test_visual_mma*`, `test_*sim*mma*` set):
|
||||
- `test_mma_agent_focus_phase1.py`, `test_mma_agent_focus_phase3.py`
|
||||
- `test_mma_approval_indicators.py`, `test_mma_concurrent_tracks_sim.py`, `test_mma_concurrent_tracks_stress_sim.py`
|
||||
- `test_mma_dashboard_refresh.py`, `test_mma_dashboard_streams.py`, `test_mma_models.py`, `test_mma_node_editor.py`
|
||||
- `test_mma_orchestration_gui.py`, `test_mma_prompts.py`, `test_mma_skeleton.py`, `test_mma_step_mode_sim.py`
|
||||
- `test_mma_ticket_actions.py`, `test_mma_tier_usage_reset_fix.py`, `test_mma_usage_stats.py`
|
||||
- `test_mma_concurrent_tracks_sim.py`, `test_mma_concurrent_tracks_stress_sim.py`
|
||||
- `test_reset_session_clears_mma_and_rag.py` (split: MMA portion gated, RAG portion stays — see Section 2)
|
||||
- `test_visual_mma.py`, `test_visual_sim_mma_v2.py`
|
||||
- `mock_concurrent_mma.py` (test helper, not a test file — kept as-is for opt-in runs)
|
||||
- `test_conductor_abort_event.py`, `test_conductor_api_hook_integration.py`, `test_conductor_engine_abort.py`, `test_conductor_engine_v2.py`, `test_conductor_tech_lead.py`
|
||||
- `test_dag_engine.py`, `test_gui_dag_beads.py`, `test_perf_dag.py`, `test_task_dag_popout_sim.py`
|
||||
- `test_parallel_execution.py`, `test_run_worker_lifecycle_abort.py`
|
||||
|
||||
Note: `test_conductor_tech_lead.py` and `test_dag_engine.py` may test the shared-types layer (not the MMA engine). During implementation, classify each: if it tests the shared `dag_engine`/`conductor_tech_lead` layer (not the MMA engine), it stays in the default batch. If it tests the MMA engine specifically, it's gated. The classifier: does the test import or instantiate `multi_agent_conductor.ConductorEngine` / `WorkerPool`? If yes → gated. If no → stays.
|
||||
|
||||
## Section 2: RAG Test Decoupling
|
||||
|
||||
### Mechanism
|
||||
|
||||
Three-tier test classification. The RAG algorithm is unchanged. The mock provider already exists (`rag_engine.py`: `provider == 'mock'` short-circuits chromadb). The tests are reclassified, not rewritten from scratch.
|
||||
|
||||
### Tier 1 — Unit tests (default batch, no chromadb, no subprocess)
|
||||
|
||||
Test the 50-line algorithm in isolation against the mock provider:
|
||||
- `test_rag_chunk.py` — `RAGChunk` dataclass (already isolated; stays)
|
||||
- `test_rag_engine.py` — rewrite to use mock provider exclusively; test `index_file` no-op behavior on mock, `search` returns `[]` on mock, chunking logic (`_chunk_text`, `_chunk_code_result`), `is_empty` semantics, `RAGChunk.from_dict`/`to_dict` round-trip
|
||||
- `test_rag_engine_result.py` — Result wrapping (already isolated; stays)
|
||||
- `test_rag_sync_none_error.py` — sync error handling (mock the engine, not chromadb)
|
||||
|
||||
### Tier 2 — Controller lifecycle tests (default batch, mock RAGEngine, no chromadb)
|
||||
|
||||
Test `app_controller.py`'s RAG lifecycle wiring using a mock/stub `RAGEngine`:
|
||||
- `test_rag_engine_ready_status_bug.py` — mock the `rag_engine` on `AppController`, test the ready-status state machine
|
||||
- `test_rag_gui_presence.py` — test the GUI panel renders when RAG is enabled/disabled (no real engine)
|
||||
- `test_sync_rag_engine_coalescing.py` — test `_sync_rag_engine` coalescing logic with a mock engine (controller logic, not RAG logic)
|
||||
- `test_reset_session_clears_mma_and_rag.py` — RAG portion stays (test reset clears the RAG state fields with a mock engine); MMA portion gated per Section 1.6
|
||||
|
||||
### Tier 3 — Integration tests (opt-in, env-gated, real chromadb / live_gui)
|
||||
|
||||
Gated behind `SLOP_RAG_INTEGRATION=1` env var via `@pytest.mark.skipif`. Skip reason: "Integration test requires real chromadb + live_gui subprocess; run with `SLOP_RAG_INTEGRATION=1` to enable." These are the tests that kept breaking — now opt-in, not default batch:
|
||||
- `test_rag_phase4_final_verify.py` — the 3-ADDENDUM fragile one
|
||||
- `test_rag_phase4_stress.py` — stress test
|
||||
- `test_rag_visual_sim.py` — live_gui visual sim
|
||||
- `test_rag_integration.py` — full integration
|
||||
|
||||
### What this kills
|
||||
|
||||
The recurring bleed. The default test batch no longer touches chromadb file locks, the live_gui subprocess RAG state, or CWD drift. When adjacent code changes, Tier 1+2 verify RAG logic + controller wiring in isolation. Tier 3 only runs on explicit opt-in (e.g., before a release, or when actively working on RAG).
|
||||
|
||||
### What this preserves
|
||||
|
||||
The RAG feature itself. `RAGEngine` is unchanged. The mock provider already exists. The integration tests still exist — just not in the default batch.
|
||||
|
||||
## Verification
|
||||
|
||||
### MMA quarantine
|
||||
- `mma.enabled = false` (default): MMA dashboard does not render; engine methods no-op; `multi_agent_conductor` not imported at runtime; MMA tests skipped (not failed)
|
||||
- `mma.enabled = true`: MMA dashboard renders; engine starts; MMA tests run when `SLOP_MMA_TESTS=1`
|
||||
- No regression in non-MMA code (thinking_parser, project_manager, models, conductor_tech_lead — shared types intact)
|
||||
|
||||
### RAG test decoupling
|
||||
- Default batch: Tier 1+2 run in milliseconds, no chromadb, no subprocess, no file locks
|
||||
- `SLOP_RAG_INTEGRATION=1`: Tier 3 runs (the previously-fragile tests)
|
||||
- RAG feature functional end-to-end when `rag.enabled = true` in config (unchanged)
|
||||
|
||||
## Risks
|
||||
|
||||
- **Lazy import correctness:** the `multi_agent_conductor` lazy import inside gated methods must not introduce a circular import or a startup-time import when the flag is off. Verify via `scripts/audit_main_thread_imports.py` after implementation.
|
||||
- **Shared-types boundary:** `mma.py` / `dag_engine.py` / `mma_prompts.py` must remain importable by non-MMA consumers. The flag gates the *engine*, not the *types*. If `mma_prompts.py` usages in `ai_client.py` / `conductor_tech_lead.py` / `orchestrator_pm.py` are MMA-specific (e.g., prompt templates only used by the engine), gate them; if general, leave. Classify during implementation.
|
||||
- **Test classifier drift:** the MMA test classifier ("does it import `ConductorEngine` / `WorkerPool`?") must be applied consistently. A test that tests shared `dag_engine` types but not the engine stays in the default batch.
|
||||
- **Quarantine maintenance cost:** if the quarantined code rots (imports drift, shared types change underneath it), follow-up full removal (Option A) becomes necessary. The quarantine is the lower-risk path *now*; it's not a permanent commitment.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- The discussion/session system redesign (nagent research track)
|
||||
- Full removal of MMA code (follow-up track if quarantine maintenance hurts)
|
||||
- RAG algorithm changes (`index_file`, `search`, chunking — unchanged)
|
||||
- The `conductor/` track system (`conductor_tech_lead.py` / `project_manager.py` / `dag_engine.py` shared-types layer preserved)
|
||||
- Migrating `ThinkingSegment` / `TrackState` / `TrackMetadata` out of `mma.py` (follow-up to full removal)
|
||||
|
||||
## See Also
|
||||
|
||||
- `conductor/code_styleguides/feature_flags.md` — the config-flag-vs-file-presence decision tree (§2: persistent preference → config flag + GUI checkbox)
|
||||
- `docs/guide_mma.md` — the MMA engine architecture (the thing being quarantined)
|
||||
- `docs/guide_rag.md` — the RAG subsystem architecture (unchanged)
|
||||
- `conductor/tracks/fix_mma_concurrent_tracks_sim_20260627/` — the recent MMA brittleness
|
||||
- Git history 2026-06-27 — the RAG test debugging churn (10 commits, 2 ADDENDUM reports)
|
||||
Reference in New Issue
Block a user