# Test Batching — Post-Refactor Polish 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:** Polish the test batching orchestrator and live_gui fixture AFTER `test_batching_refactor_20260606` ships. Deliver: (1) shared `_extract_failed_files` library used by both the legacy and new orchestrators, (2) per-file failure list in the new orchestrator's SUMMARY, (3) `live_gui` subprocess window foregrounding, (4) `focus_test_panel` helper wired into 3 starter sims, (5) `tests/artifacts/` scratch cleanup.

**Architecture:** New `scripts/test_failure_parser.py` module (str-ops-only FAILED-line parser, no regex). New module-level functions in `tests/conftest.py` (lazy-import `win32gui`, `ApiHookClient`). Surgical edits to the post-refactor `scripts/run_tests_batched.py:_run_batch` to wire the parser into the SUMMARY. No new files in `src/`.

**Tech Stack:** Python 3.11+ (stdlib `subprocess`, `os`, `sys`, `time`). `pywin32` (already a project dep; used lazily). `ApiHookClient` (existing).

**Blocked by:** `test_batching_refactor_20260606` (must be SHIPPED — this plan reads from the new orchestrator's `_run_batch` and the legacy's `_extract_failed_files`).

**Parent track:** None. **Child tracks:** None.

---

## Constraints (re-stated from the user's standing rules)

- **Do NOT use the native `edit` tool on `.py` files.** It destroys 1-space indentation. Use `manual-slop_edit_file` (exact match), `manual-slop_set_file_slice` (single-line surgical only), or `manual-slop_py_update_definition` (function rewrites).
- **Do NOT use `git restore`, `git checkout -- <file>`, or `git reset` without explicit user permission in the same message.** HARD BAN.
- **Do NOT commit `config.toml`, `project.toml`, `project_history.toml`, or repo-root `manualslop_layout.ini`.** These are the user's. Stage and commit only the files listed in each task.
- **Do NOT add `re` (regex) to the failure parser.** Use `str.startswith`, `str.find`, `str.split`, `str.replace`. Verify with `grep -n "import re\|from re" scripts/test_failure_parser.py` returning empty after Phase 1.
- **1-space indentation for all Python code.** 2-space for class bodies. 0 leading spaces for module-level. CRLF line endings on Windows.
- **Do NOT add inline comments to source code.** Docstrings are fine; `#` comments are not.
- **Type hints required** for all new functions.

---

## Phase 1: Shared `_extract_failed_files` library

Focus: Extract the FAILED-line parser to a shared module that both the legacy and new orchestrators can import. Str-ops-only contract, no regex, with comprehensive unit tests.

**Files:**
- Create: `scripts/test_failure_parser.py` (~35 lines)
- Create: `tests/test_test_failure_parser.py` (~120 lines; 11 unit tests)
- Modify: `scripts/run_tests_batched.py` (the post-refactor new orchestrator; if the legacy is still present and has a local copy, also update it)

### Task 1.1: Red — add 11 unit tests for the shared parser

**Files:** Create `tests/test_test_failure_parser.py`.

- [ ] **Step 1: Write the failing test file**

```python
"""
Unit tests for the FAILED-line parser in scripts/test_failure_parser.py.
Shared by both the legacy run_tests_batched.py and the new orchestrator.
Str-ops-only contract; no regex.
"""
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import test_failure_parser as tfp


def test_extract_empty():
    assert tfp.extract_failed_files("") == []


def test_extract_no_failed_lines():
    out = "tests/test_foo.py .. [ 12%]\ntests/test_bar.py F [100%]\n===== 1 passed, 1 failed in 0.5s =====\n"
    assert tfp.extract_failed_files(out) == []


def test_extract_single_failed_line():
    out = "FAILED tests/test_foo.py::test_bar - AssertionError: nope\n"
    assert tfp.extract_failed_files(out) == ["test_foo.py"]


def test_extract_multiple_failed_lines_same_file():
    out = (
        "FAILED tests/test_foo.py::test_a - AssertionError\n"
        "FAILED tests/test_foo.py::test_b - AssertionError\n"
    )
    assert tfp.extract_failed_files(out) == ["test_foo.py"]


def test_extract_multiple_failed_lines_different_files():
    out = (
        "FAILED tests/test_foo.py::test_a - AssertionError\n"
        "FAILED tests/test_bar.py::test_b - AssertionError\n"
    )
    assert tfp.extract_failed_files(out) == ["test_foo.py", "test_bar.py"]


def test_extract_failed_line_no_test_id():
    out = "FAILED tests/test_foo.py - collection error\n"
    assert tfp.extract_failed_files(out) == ["test_foo.py"]


def test_extract_failed_line_windows_path():
    out = "FAILED tests\\test_foo.py::test_bar - AssertionError\n"
    assert tfp.extract_failed_files(out) == ["test_foo.py"]


def test_extract_failed_line_class_method():
    out = "FAILED tests/test_foo.py::TestClass::test_method - AssertionError\n"
    assert tfp.extract_failed_files(out) == ["test_foo.py"]


def test_extract_failed_line_parametrized():
    out = "FAILED tests/test_foo.py::test_bar[1] - AssertionError\n"
    assert tfp.extract_failed_files(out) == ["test_foo.py"]


def test_extract_ignores_lines_that_contain_failed_but_dont_start_with_it():
    out = "===== 1 failed, 2 passed in 0.5s =====\n"
    assert tfp.extract_failed_files(out) == []


def test_extract_real_pytest_summary_block():
    out = (
        "===== short test summary info =====\n"
        "FAILED tests/test_alpha.py::test_one - AssertionError: 1 != 2\n"
        "FAILED tests/test_alpha.py::test_two - AssertionError: 3 != 4\n"
        "FAILED tests/test_beta.py::TestThing::test_x - TypeError\n"
        "===== 3 failed, 5 passed in 1.2s =====\n"
    )
    assert tfp.extract_failed_files(out) == ["test_alpha.py", "test_beta.py"]
```

- [ ] **Step 2: Run the test, verify it FAILS (no module yet)**

Run: `uv run pytest tests/test_test_failure_parser.py -v`
Expected: ALL 11 tests FAIL with `ImportError: No module named 'test_failure_parser'`.

- [ ] **Step 3: Commit the failing test (TDD red phase)**

```powershell
git add tests/test_test_failure_parser.py
git commit -m "test(failure_parser): add 11 unit tests for shared FAILED-line parser"
```

### Task 1.2: Green — implement `extract_failed_files` in `scripts/test_failure_parser.py`

**Files:** Create `scripts/test_failure_parser.py`.

- [ ] **Step 1: Create the module**

```python
"""
Shared FAILED-line parser for pytest output.

Used by both scripts/run_tests_batched.py (the legacy and the new
post-refactor orchestrator). Str-ops-only by design: no regex import
per AGENTS.md standing ban across the codebase.

Contract:
  - Input: full captured stdout+stderr from a pytest invocation.
  - Lines that begin with the literal 7-character prefix "FAILED "
    (note the trailing space) are parsed for the test ID.
  - The test ID portion ends at the first " - " (space-dash-space)
    separator that introduces the error message.
  - If the test ID contains "::", the file path is everything before
    the first "::". Otherwise the test ID IS the file path.
  - Backslashes are normalized to forward slashes (Windows safety).
  - A leading "tests/" prefix is stripped so returned strings match
    the bare filenames in the test file list.
  - Returns the unique file paths in first-occurrence order.

Lines that merely contain the substring "failed" (e.g. the
"1 failed, 2 passed" summary footer) are NOT parsed.

[C: scripts/run_tests_batched.py:_run_batch (post-refactor),
 scripts/run_tests_batched.py:run_tests (legacy, if not yet
 deleted by the refactor's Phase 4)]
"""
from __future__ import annotations

_FAILED_PREFIX: str = "FAILED "


def extract_failed_files(output: str) -> list[str]:
 failed: list[str] = []
 seen: set[str] = set()
 for line in output.splitlines():
  if not line.startswith(_FAILED_PREFIX):
   continue
  rest: str = line[len(_FAILED_PREFIX):]
  dash_idx: int = rest.find(" - ")
  test_id: str = rest if dash_idx == -1 else rest[:dash_idx]
  colon_colon_idx: int = test_id.find("::")
  filepath: str = test_id if colon_colon_idx == -1 else test_id[:colon_colon_idx]
  filepath = filepath.replace("\\", "/")
  if filepath.startswith("tests/"):
   filepath = filepath[len("tests/"):]
  if filepath and filepath not in seen:
   seen.add(filepath)
   failed.append(filepath)
 return failed
```

- [ ] **Step 2: Run the test, verify it PASSES**

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

- [ ] **Step 3: Verify no `re` import**

Run: `grep -n "import re\|from re" scripts/test_failure_parser.py`
Expected: no output (empty).

- [ ] **Step 4: Commit the parser module**

```powershell
git add scripts/test_failure_parser.py
git commit -m "feat(scripts): add shared test_failure_parser module (no regex)"
```

### Task 1.3: Wire the shared parser into the post-refactor orchestrator

**Files:** Modify `scripts/run_tests_batched.py` (the new orchestrator from the refactor's Phase 3).

This task assumes the refactor's Phase 3 is SHIPPED. The new orchestrator's `_run_batch` is at the section documented in the refactor's plan.md around line 1295-1308:
```python
def _run_batch(b: Batch, durations: dict[str, float]) -> tuple[int, float, dict[str, float]]:
 if b.skip_reason:
  return 0, 0.0, {}
 cmd = ["uv", "run", "pytest", "-v", "--durations=0"] + b.pytest_args + [str(f) for f in b.files]
 print(f"\n>>> Running {b.label} ({len(b.files)} files)")
 t0 = time.monotonic()
 proc = subprocess.run(cmd, capture_output=True, text=True)
 elapsed = time.monotonic() - t0
 new_durs = _parse_durations_from_pytest_output(proc.stdout)
 print(proc.stdout[-2000:] if proc.returncode != 0 else f"<<< {b.label} PASS in {elapsed:.1f}s")
 if proc.returncode != 0:
  print(f"<<< {b.label} FAIL (exit {proc.returncode}) in {elapsed:.1f}s")
 print(proc.stderr[-1000:])
 return proc.returncode, elapsed, new_durs
```

- [ ] **Step 1: Add the import at the top of the new orchestrator**

Read the current top of `scripts/run_tests_batched.py` (post-refactor) to identify the import block. Add:

```python
from scripts.test_failure_parser import extract_failed_files
```

- [ ] **Step 2: Refactor `_run_batch` to capture and surface per-file failure lists**

Replace `_run_batch` with a version that:
- Returns a `tuple[int, float, dict[str, float], list[str]]` (4-tuple; the 4th element is the per-file failure list)
- On `returncode != 0`, calls `extract_failed_files(proc.stdout + "\n" + proc.stderr)` to get the actual failed files
- On `subprocess.TimeoutExpired` (raised when the batch exceeds `--timeout` if the caller wraps with a timeout), fall back to all files in the batch with a `(timeout)` annotation
- Returns `[]` for skipped batches or successful runs

```python
def _run_batch(
 b: Batch,
 durations: dict[str, float],
 timeout: int | None = None,
) -> tuple[int, float, dict[str, float], list[tuple[str, str]]]:
 if b.skip_reason:
  return 0, 0.0, {}, []
 cmd = ["uv", "run", "pytest", "-v", "--durations=0"] + b.pytest_args + [str(f) for f in b.files]
 print(f"\n>>> Running {b.label} ({len(b.files)} files)")
 t0 = time.monotonic()
 failed: list[tuple[str, str]] = []
 try:
  proc = subprocess.run(
   cmd,
   capture_output=True,
   text=True,
   timeout=timeout,
  )
  elapsed = time.monotonic() - t0
  new_durs = _parse_durations_from_pytest_output(proc.stdout)
  if proc.returncode == 0:
   print(f"<<< {b.label} PASS in {elapsed:.1f}s")
  else:
   actual: list[str] = extract_failed_files(proc.stdout + "\n" + proc.stderr)
   if actual:
    for f in actual:
     failed.append((f, ""))
    print(f"<<< {b.label} FAIL (exit {proc.returncode}) in {elapsed:.1f}s; {len(actual)} actually-failed file(s)")
   else:
    for f in b.files:
     failed.append((str(f), "(no FAILED lines; treating as batch failure)"))
    print(f"<<< {b.label} FAIL (exit {proc.returncode}) in {elapsed:.1f}s; no FAILED lines found, listing whole batch")
  return proc.returncode, elapsed, new_durs, failed
 except subprocess.TimeoutExpired:
  elapsed = time.monotonic() - t0
  for f in b.files:
   failed.append((str(f), "(timeout)"))
  print(f"<<< {b.label} TIMED OUT after {elapsed:.1f}s (limit {timeout}s)")
  return 1, elapsed, {}, failed
```

- [ ] **Step 3: Update `_print_summary` to display the per-file failure list**

The refactor's `_print_summary` takes `results: list[tuple[Batch, int, float]]` (3-tuple). Update to 4-tuple and add the per-file listing:

```python
def _print_summary(results: list[tuple[Batch, int, float, list[tuple[str, str]]]]) -> int:
 print("\n" + "=" * 60)
 print("SUMMARY")
 print("=" * 60)
 worst: int = 0
 any_failed: bool = False
 for b, code, elapsed, failed in results:
  if b.skip_reason:
   status: str = "SKIPPED"
  elif code == 0:
   status = "PASS"
  else:
   status = "FAIL"
   any_failed = True
  worst = max(worst, code)
  n: int = len(b.files)
  print(f"[{b.tier}] {b.label:40s} {status:8s} {n} files {elapsed:6.1f}s")
  for f, note in failed:
   suffix: str = f"  {note}" if note else ""
   print(f"   - {f}{suffix}")
 return 1 if any_failed else worst
```

- [ ] **Step 4: Update the `main()` callsite to thread the 4-tuple through**

Find the loop in `main()` that calls `_run_batch` and accumulates results. Change the tuple unpacking from 3-tuple to 4-tuple and pass the `failed` list to `_print_summary`.

Before:
```python
for b in batches:
 code, elapsed, new_durs = _run_batch(b, merged_durations)
 results.append((b, code, elapsed))
```

After:
```python
timeout_arg: int | None = options.timeout
for b in batches:
 code, elapsed, new_durs, failed = _run_batch(b, merged_durations, timeout=timeout_arg)
 results.append((b, code, elapsed, failed))
```

Also add a `--timeout` argument to the `argparse.ArgumentParser` in `main()` (the refactor's spec doesn't have one; default 600s = 10 minutes per batch):

```python
p.add_argument("--timeout", type=int, default=600, help="seconds per batch (default: 600)")
```

- [ ] **Step 5: Verify the script still parses and the new tests pass**

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

Run: `uv run python scripts/run_tests_batched.py --plan --tiers 1 2>&1 | head -20`
Expected: prints tier-1 batches (no execution; just plan output).

- [ ] **Step 6: Run a small tier-1 batch end-to-end to confirm the new path works**

Run: `uv run python scripts/run_tests_batched.py --tiers 1 --no-xdist 2>&1 | tail -30`
Expected: runs the unit tier; SUMMARY table printed; if any tests fail, the per-file failure list is shown under the failing tier.

- [ ] **Step 7: Commit the integration**

```powershell
git add scripts/run_tests_batched.py
git commit -m "feat(orchestrator): wire shared failure parser into _run_batch; per-file SUMMARY"
```

### Task 1.4: Conductor — User Manual Verification (Phase 1)

- [ ] **Step 1: Run the unit tests**

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

- [ ] **Step 2: Run a small tier with a deliberate failure to confirm end-to-end**

  Create a temporary failing test:
  ```python
  # tests/test_zzz_fake_failure.py
  def test_zzz_fake_failure():
      assert False, "intentional failure"
  ```

  Run: `uv run python scripts/run_tests_batched.py --tiers 1 --no-xdist 2>&1 | tail -30`
  Expected: SUMMARY shows the tier failed, the per-file listing shows `test_zzz_fake_failure.py`. Then delete the temp file.

  If the run fails: capture the output to a log file and spawn a Tier 4 QA agent. Do not attempt more than 2 fix cycles; if still failing, report and stop.

- [ ] **Step 3: PAUSE and present verification result**

  > "Phase 1 verification: 11/11 unit tests pass; end-to-end run on tier 1 with a deliberate failure shows the file in the per-file listing. Ready to commit Phase 1 checkpoint and move to Phase 2? (yes / changes needed)"

- [ ] **Step 4: Create the Phase 1 checkpoint**

  Capture the most recent commit hash. Attach a git note. Update `plan.md` Phase 1 status to `[x]` and append the hash.

  ```powershell
  git notes add -m "Phase 1 of test_batching_post_refactor_polish_20260607: shared scripts/test_failure_parser.py with 11 unit tests; integrated into new orchestrator's _run_batch + SUMMARY. Per-file failure list now surfaced for non-zero exits; whole-batch fallback on timeout or no-FAILED-lines." <commit_sha>
  ```

---

## Phase 2: `live_gui` Window Foregrounding

Focus: Add `_foreground_subprocess_window` helper to `tests/conftest.py` and wire it into the `live_gui` fixture. Str-ops-only contract; no regex; lazy-import `win32gui`/`win32con`; never raises.

**Files:**
- Modify: `tests/conftest.py` (add helper + call from fixture)
- Create: `tests/test_live_gui_foregrounding.py` (3 unit tests)

### Task 2.1: Red — add unit tests for the foregrounding helper

**Files:** Create `tests/test_live_gui_foregrounding.py`.

- [ ] **Step 1: Write the failing test file**

```python
"""
Unit tests for the sloppy.py window-foregrounding helper in
tests/conftest.py. Platform-dispatched: Windows uses win32gui;
non-Windows is a no-op. Tests must not require a real GUI subprocess.
"""
import os
import sys

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

import conftest


def test_foreground_helper_exists():
 assert hasattr(conftest, "_foreground_subprocess_window")
 assert callable(conftest._foreground_subprocess_window)


def test_foreground_helper_noop_on_invalid_pid():
 conftest._foreground_subprocess_window(pid=0)
 conftest._foreground_subprocess_window(pid=0xFFFFFFFE)


def test_foreground_helper_noop_when_win32gui_unavailable(monkeypatch):
 real_import = __builtins__.__import__ if hasattr(__builtins__, "__import__") else __import__

 def fake_import(name, *args, **kwargs):
  if name in ("win32gui", "win32con"):
   raise ImportError(f"simulated missing {name}")
  return real_import(name, *args, **kwargs)

 monkeypatch.setattr("builtins.__import__", fake_import)
 conftest._foreground_subprocess_window(pid=0)
```

- [ ] **Step 2: Run the test, verify it FAILS**

Run: `uv run pytest tests/test_live_gui_foregrounding.py -v`
Expected: ALL 3 FAIL with `AttributeError: module 'conftest' has no attribute '_foreground_subprocess_window'`.

- [ ] **Step 3: Commit the failing test**

```powershell
git add tests/test_live_gui_foregrounding.py
git commit -m "test(fixture): add unit tests for live_gui window-foregrounding helper"
```

### Task 2.2: Green — implement `_foreground_subprocess_window` in `tests/conftest.py`

**Files:** Modify `tests/conftest.py` (add module-level function after imports, before any fixture).

- [ ] **Step 1: Add the helper function**

```python
def _foreground_subprocess_window(pid: int, attempts: int = 3, delay_s: float = 0.5) -> None:
 """
 Best-effort: bring the given subprocess's main OS window to the
 foreground. No-op on non-Windows, when pywin32 is unavailable,
 or when the window cannot be found (the subprocess may not have
 created its window yet).

 Args:
  pid: the OS process ID of the subprocess whose window to raise.
  attempts: max number of lookup attempts.
  delay_s: seconds to wait between attempts.

 Behavior:
  - Windows: uses win32gui.EnumWindows to find a top-level window
   whose owning thread/process matches `pid`, then calls
   ShowWindow(hwnd, SW_SHOWNORMAL) + SetForegroundWindow(hwnd).
  - Non-Windows: returns immediately.
  - Any exception: caught at the function boundary, logged via
   print(), and the function returns. NEVER raises into the
   test fixture (per the user's resilient-fixture preference).

 [C: tests/conftest.py:live_gui fixture]
 """
 if os.name != "nt":
  return
 try:
  import win32gui
  import win32con
 except ImportError:
  return
 for _ in range(attempts):
  try:
   hwnd_found: list[int] = []

   def _cb(hwnd: int, ctx: list[int]) -> bool:
    if win32gui.IsWindowVisible(hwnd):
     _, found_pid = win32gui.GetWindowThreadProcessId(hwnd)
     if found_pid == pid:
      ctx.append(hwnd)
      return False
    return True

   win32gui.EnumWindows(_cb, hwnd_found)
   if hwnd_found:
    hwnd: int = hwnd_found[0]
    win32gui.ShowWindow(hwnd, win32con.SW_SHOWNORMAL)
    try:
     win32gui.SetForegroundWindow(hwnd)
    except Exception:
     pass
    return
  except Exception as e:
   print(f"[Fixture] WARNING: could not foreground sloppy.py window (pid={pid}): {e}")
   return
  time.sleep(delay_s)
```

- [ ] **Step 2: Run the test, verify it PASSES**

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

- [ ] **Step 3: Commit the helper**

```powershell
git add tests/conftest.py
git commit -m "feat(fixture): add _foreground_subprocess_window helper for live_gui"
```

### Task 2.3: Wire the helper into the `live_gui` fixture

**Files:** Modify `tests/conftest.py` (the `live_gui` fixture's `subprocess.Popen(...)` call site).

- [ ] **Step 1: Locate the `subprocess.Popen(...)` call inside `live_gui`**

Use `manual-slop_get_file_slice` or `manual-slop_py_get_definition` to find the exact line. The Popen call returns a `proc` object whose `.pid` attribute is what the helper needs.

- [ ] **Step 2: Add the helper call immediately after the Popen returns**

Insert one line right after the Popen block (after `proc` is assigned, before any subsequent `wait` / `health` check):

```python
_foreground_subprocess_window(proc.pid)
```

Anchor the edit on a unique surrounding context (e.g. the line right after Popen completes — typically a `print` line about spawning, or a `health check` call). Use `manual-slop_edit_file` with the exact `old_string`/`new_string`.

- [ ] **Step 3: Verify the fixture still parses**

Run: `uv run python -c "import ast; ast.parse(open('tests/conftest.py').read())"`
Expected: no errors.

- [ ] **Step 4: Run a single live_gui test to confirm the fixture still works**

Run: `uv run pytest tests/test_hooks.py -v`
Expected: passes. The `[Fixture]` log line may or may not appear depending on whether pywin32 is available and the subprocess window is findable; both are acceptable.

- [ ] **Step 5: Commit the wiring**

```powershell
git add tests/conftest.py
git commit -m "feat(fixture): foreground sloppy.py window in live_gui fixture"
```

### Task 2.4: Conductor — User Manual Verification (Phase 2)

- [ ] **Step 1: Run the foregrounding unit tests**

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

- [ ] **Step 2: Run a small live_gui test to confirm the fixture still works**

  Run: `uv run pytest tests/test_hooks.py -v`
  Expected: passes.

- [ ] **Step 3: PAUSE and present verification result**

  > "Phase 2 verification: 3/3 unit tests pass; live_gui fixture still spawns successfully. Ready to commit Phase 2 checkpoint and move to Phase 3? (yes / changes needed)"

- [ ] **Step 4: Create the Phase 2 checkpoint**

  Capture the most recent commit hash. Attach a git note. Update `plan.md` Phase 2 status to `[x]` and append the hash.

---

## Phase 3: `focus_test_panel` Helper + Per-Test Wiring

Focus: A new `focus_test_panel(name)` helper in `tests/conftest.py` using the existing `ApiHookClient.set_value`. Wire into 3 starter `*_sim.py` tests.

**Files:**
- Modify: `tests/conftest.py` (add `focus_test_panel` helper)
- Modify: 3 `tests/test_*_sim.py` files (one-line addition each)

### Task 3.1: Add the `focus_test_panel` helper

**Files:** Modify `tests/conftest.py` (insert after `_foreground_subprocess_window`).

- [ ] **Step 1: Add the helper function**

```python
def focus_test_panel(panel_name: str, host: str = "127.0.0.1", port: int = 8999) -> bool:
 """
 For live_gui tests: assert the named panel is visible so the user
 watching the GUI subprocess can see the test's target panel.

 Uses the existing ApiHookClient (no new IPC endpoints). The
 set_value call toggles `show_windows["<name>"] = True` via the
 Hook API.

 Returns True on success, False if the hook server is not
 reachable (e.g. called outside a live_gui session; the test
 may choose to skip subsequent assertions on False).

 [C: tests/test_*_sim.py — call before assertions]
 """
 try:
  from src.api_hook_client import ApiHookClient
 except ImportError:
  return False
 try:
  client = ApiHookClient(host=host, port=port)
  if not client.wait_for_server(timeout=0.5):
   return False
  client.set_value(f'show_windows["{panel_name}"]', True)
  return True
 except Exception as e:
  print(f"[focus_test_panel] could not focus '{panel_name}': {e}")
  return False
```

- [ ] **Step 2: Verify the helper imports cleanly**

Run: `uv run python -c "import tests.conftest; print(hasattr(tests.conftest, 'focus_test_panel'))"`
Expected: prints `True`.

- [ ] **Step 3: Commit the helper**

```powershell
git add tests/conftest.py
git commit -m "feat(fixture): add focus_test_panel helper for live_gui test panels"
```

### Task 3.2: Wire `focus_test_panel` into 3 starter sim tests

**Files:** Modify 3 `tests/test_*_sim.py` files.

- [ ] **Step 1: Add to `tests/test_command_palette_sim.py`**

  Find the test that uses the Command Palette (typically the only `def test_*(live_gui):` function). Add as the FIRST line after `client.wait_for_server(...)`:

  ```python
  focus_test_panel("Command Palette")
  ```

- [ ] **Step 2: Add to `tests/test_workflow_sim.py`**

  Find the test that drives the Discussion Hub. Add:

  ```python
  focus_test_panel("Discussion Hub")
  ```

- [ ] **Step 3: Add to `tests/test_undo_redo_sim.py`**

  Find the test that exercises Undo/Redo. Add:

  ```python
  focus_test_panel("Discussion Hub")
  ```

- [ ] **Step 4: Verify each file parses**

For each:
```powershell
uv run python -c "import ast; ast.parse(open('tests/test_command_palette_sim.py').read())"
uv run python -c "import ast; ast.parse(open('tests/test_workflow_sim.py').read())"
uv run python -c "import ast; ast.parse(open('tests/test_undo_redo_sim.py').read())"
```
Expected: no errors.

- [ ] **Step 5: Run one of the modified sims to confirm the fixture still works**

Run: `uv run pytest tests/test_command_palette_sim.py -v`
Expected: passes. The new `focus_test_panel("Command Palette")` call is idempotent for an already-visible panel.

- [ ] **Step 6: Commit the wiring**

```powershell
git add tests/test_command_palette_sim.py tests/test_workflow_sim.py tests/test_undo_redo_sim.py
git commit -m "test(sim): add focus_test_panel calls to 3 starter live_gui sims"
```

### Task 3.3: Conductor — User Manual Verification (Phase 3)

- [ ] **Step 1: Run the 3 modified sim tests**

  Run: `uv run pytest tests/test_command_palette_sim.py tests/test_workflow_sim.py tests/test_undo_redo_sim.py -v`
  Expected: all pass.

- [ ] **Step 2: PAUSE and present verification result**

  > "Phase 3 verification: 3 sim tests pass with focus_test_panel calls. The helper is exported and idempotent. Ready to commit Phase 3 checkpoint and move to Phase 4? (yes / changes needed)"

- [ ] **Step 3: Create the Phase 3 checkpoint**

  Capture the most recent commit hash. Attach a git note. Update `plan.md` Phase 3 status to `[x]` and append the hash.

---

## Phase 4: `tests/artifacts/` Scratch Cleanup

Focus: Verify the candidate scratch files have NO references in the codebase, then delete them. Single atomic commit.

**Files:** Delete only; no modifications.

### Task 4.1: Verify and delete scratch files

- [ ] **Step 1: Build the candidate list and verify each is unreferenced**

  The candidate list (per spec §4.4 FR-19):
  - `test_parser.py`, `test_patterns.py`, `test_regex.py`
  - `verify_layout.py`, `check_cwd.py`, `check_cwd_uv.py`, `exists.py`, `fix_stale_names.py`, `fix_conftest_layout.py`
  - `fake_test_output.txt`
  - `agents_skip_msg.txt`, `commit_layout_diag_msg.txt`, `configpath_msg.txt`, `context_presets_msg.txt`, `hooks_dictkey_msg.txt`, `reset_layout_msg.txt`, `st2a_prompt.txt`, `st2a_task.toml`, `st2g_msg.txt`, `st2g_msg2.txt`, `st2g_msg3.txt`, `stale_test_msg.txt`, `synthesis_crash_msg.txt`, `warmup_fix_msg.txt`, `workflow_skip_msg.txt`
  - `task1.toml`, `task1.txt`, `task2.toml`, `task2_1.txt`, `task3.toml`, `task3_1.txt`, `task4.toml`, `task_1_1.txt`
  - `temp_config.toml`, `temp_data.txt`, `temp_liveaisettingssim.toml`, `temp_livecontextsim.toml`, `temp_liveexecutionsim.toml`, `temp_livetoolssim.toml`, `temp_notes.txt`, `temp_project.toml`, `temp_settings.toml`, `temp_simproject.toml`
  - `test_001.md`

  For each candidate, run a grep across `tests/`, `scripts/`, `src/`, `docs/`:
  ```powershell
  rg "<filename>" tests/ scripts/ src/ docs/
  ```
  Expected: zero matches. If any match is found, PRESERVE that file (do NOT delete) and note in the commit message.

  Also confirm each file is gitignored (or untracked):
  ```powershell
  git check-ignore -v tests/artifacts/test_parser.py
  ```
  Expected: prints a `.gitignore` rule for each. If any file is TRACKED, do NOT delete it without explicit user permission (HARD BAN on `git restore`/`git checkout --`).

- [ ] **Step 2: Delete the verified files**

  Use a single PowerShell command:
  ```powershell
  Remove-Item tests/artifacts/test_parser.py, tests/artifacts/test_patterns.py, tests/artifacts/test_regex.py, tests/artifacts/verify_layout.py, tests/artifacts/fake_test_output.txt, tests/artifacts/check_cwd.py, tests/artifacts/check_cwd_uv.py, tests/artifacts/exists.py, tests/artifacts/fix_stale_names.py, tests/artifacts/fix_conftest_layout.py, tests/artifacts/agents_skip_msg.txt, tests/artifacts/commit_layout_diag_msg.txt, tests/artifacts/configpath_msg.txt, tests/artifacts/context_presets_msg.txt, tests/artifacts/hooks_dictkey_msg.txt, tests/artifacts/reset_layout_msg.txt, tests/artifacts/st2a_prompt.txt, tests/artifacts/st2a_task.toml, tests/artifacts/st2g_msg.txt, tests/artifacts/st2g_msg2.txt, tests/artifacts/st2g_msg3.txt, tests/artifacts/stale_test_msg.txt, tests/artifacts/synthesis_crash_msg.txt, tests/artifacts/task1.toml, tests/artifacts/task1.txt, tests/artifacts/task2.toml, tests/artifacts/task2_1.txt, tests/artifacts/task3.toml, tests/artifacts/task3_1.txt, tests/artifacts/task4.toml, tests/artifacts/temp_config.toml, tests/artifacts/temp_data.txt, tests/artifacts/temp_liveaisettingssim.toml, tests/artifacts/temp_livecontextsim.toml, tests/artifacts/temp_liveexecutionsim.toml, tests/artifacts/temp_livetoolssim.toml, tests/artifacts/temp_notes.txt, tests/artifacts/temp_project.toml, tests/artifacts/temp_settings.toml, tests/artifacts/temp_simproject.toml, tests/artifacts/test_001.md, tests/artifacts/warmup_fix_msg.txt, tests/artifacts/workflow_skip_msg.txt, tests/artifacts/task_1_1.txt
  ```

  If `Remove-Item` fails because a file doesn't exist (already deleted or never existed), it's a no-op — that's fine.

- [ ] **Step 3: Verify the directory still has the preserved files**

  ```powershell
  Get-ChildItem tests/artifacts
  ```
  Expected: only the preserved entries (`.gitignore`, `manualslop_layout_default.ini`, runtime state directories, referenced TOML files). No scratch files.

- [ ] **Step 4: Commit the cleanup**

  ```powershell
  git add -A tests/artifacts
  git status   # confirm no tracked files inside tests/artifacts were deleted
  git commit -m "chore(artifacts): remove ~45 scratch files from tests/artifacts/"
  ```

  If the commit shows 0 changed files (everything was gitignored and deletion doesn't affect git), that's acceptable — the deletion is recorded in the working tree, not the git history.

### Task 4.2: Conductor — User Manual Verification (Phase 4)

- [ ] **Step 1: PAUSE and present the cleanup result**

  > "Phase 4 complete. tests/artifacts/ now contains only the preserved files. Listing: <list>. Ready to commit Phase 4 checkpoint and finalize? (yes / changes needed)"

- [ ] **Step 2: Create the Phase 4 checkpoint**

  Capture the most recent commit hash (or note that the commit was empty). Attach a git note. Update `plan.md` Phase 4 status to `[x]` and append the hash (or "no SHA; gitignored delete" if no commit SHA).

---

## Phase 5: Track Finalization (Verification + Status Update)

Focus: Re-run the full test suite (5 batches, 298 files) to confirm no regressions. Update `conductor/tracks.md`. Commit the plan update.

### Task 5.1: Full suite regression run

- [ ] **Step 1: Run the full test suite via the new orchestrator (or legacy, whichever is current default)**

  If the refactor's Phase 3 is shipped, run:
  ```powershell
  uv run python scripts/run_tests_batched.py --tiers 1,2,3
  ```
  Otherwise, run the legacy:
  ```powershell
  uv run python scripts/run_tests_batched.py --batch-size 64
  ```

  Expected: all batches 1-4 pass; batch 5 (or tier 3 for the new orchestrator) may have failures. The per-file failure list now shows the actual files.

- [ ] **Step 2: PAUSE and present the regression result**

  > "Phase 5 verification: full suite run; per-file failure list verified. No regressions in batches 1-4. The track's verification criteria are all met. Ready to mark the track complete? (yes / changes needed)"

### Task 5.2: Update `conductor/tracks.md`

- [ ] **Step 1: Add a "Phase 9" chore-track entry for this track**

  Format (mirroring existing entries):

  ```markdown
  - [x] **Track: Test Batching — Post-Refactor Polish** `[checkpoint: <sha>]`
     *Link: [./tracks/test_batching_post_refactor_polish_20260607/](./tracks/test_batching_post_refactor_polish_20260607/), Spec: [./tracks/test_batching_post_refactor_polish_20260607/spec.md](./tracks/test_batching_post_refactor_polish_20260607/spec.md), Plan: [./tracks/test_batching_post_refactor_polish_20260607/plan.md](./tracks/test_batching_post_refactor_polish_20260607/plan.md)*
     *Goal: After test_batching_refactor_20260606 ships, lift _extract_failed_files to scripts/test_failure_parser.py (shared by legacy and new orchestrator); wire per-file failure list into the new orchestrator's SUMMARY; add _foreground_subprocess_window + focus_test_panel helpers to live_gui fixture; clean up ~45 scratch files in tests/artifacts/. No new dependencies; no regex.*
  ```

- [ ] **Step 2: Commit the tracks.md update**

  ```powershell
  git add conductor/tracks.md
  git commit -m "conductor(tracks): mark test_batching_post_refactor_polish_20260607 as complete"
  ```

### Task 5.3: Final archive (optional)

- [ ] **Step 1: Ask the user whether to archive**

  > "Track complete. Archive to `conductor/tracks/archive/` now, or leave in `tracks/`? (archive / leave)"

- [ ] **Step 2: If archive chosen**

  ```powershell
  git mv conductor/tracks/test_batching_post_refactor_polish_20260607 conductor/tracks/archive/
  git commit -m "conductor(archive): archive test_batching_post_refactor_polish_20260607"
  ```

- [ ] **Step 3: Announce completion**

  > "Track `test_batching_post_refactor_polish_20260607` is complete. The refactor is now followed by observability + parser polish."
