Private
Public Access
move tracks to archive
This commit is contained in:
@@ -1,70 +0,0 @@
|
||||
# SQLite-Granularity Inline Docs for ai_client.py — Implementation Plan
|
||||
|
||||
> **For agentic workers:** Use task-by-task execution. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement SQLite-style docstrings with SSDL traces, parameters, functional scopes, and thread boundaries for the primary entry points, providers, and helper functions in [src/ai_client.py](file:///C:/projects/manual_slop/src/ai_client.py). Ensure zero functional regression.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Purpose |
|
||||
|---|---|---|
|
||||
| [src/ai_client.py](file:///C:/projects/manual_slop/src/ai_client.py) | Modify | Add docstrings with SSDL & visual topologies to core loops, providers, and helper functions. |
|
||||
| [conductor/tracks/ai_client_docs_20260613/state.toml](file:///C:/projects/manual_slop/conductor/tracks/ai_client_docs_20260613/state.toml) | Modify | Track implementation state. |
|
||||
| [conductor/tracks.md](file:///C:/projects/manual_slop/conductor/tracks.md) | Modify | Register the new track. |
|
||||
|
||||
---
|
||||
|
||||
# Phase 1: Core Dispatch Loop & Public APIs
|
||||
|
||||
## Task 1.1: Document Public Entry Points & Dispatch Loops
|
||||
- [x] **Step 1: Document `send_result` (ai_client.py:2645-2730)**
|
||||
Add docstring detailing functional purpose, parameters, return type, thread-local storage setup, and error handling. SSDL trace: `[Q:active_provider] -> [I:SetupTierTag] -> [I:DispatchProvider] -> [T:Result]`.
|
||||
- [x] **Step 2: Document `send` (ai_client.py:2617-2643)**
|
||||
Mark as deprecated, explain callback mapping and Result extraction. SSDL trace: `[I:send_result] -> [T:text]`.
|
||||
- [x] **Step 3: Document `run_with_tool_loop` (ai_client.py:714-784)**
|
||||
Document the core execution loop and tool dispatch mechanics. SSDL trace: `o-> [I:dispatch_send] -> [B:tool_calls?] => [I:_execute_tool_calls_concurrently] -> [T:response_text]`.
|
||||
- [x] **Step 4: Document `_execute_tool_calls_concurrently` (ai_client.py:664-712)**
|
||||
Document the asynchronous gather and execution flow. SSDL trace: `[I:gather] => o-> [I:_execute_single_tool_call_async] -> [M] -> [T:tool_results]`.
|
||||
- [x] **Step 5: Document `_execute_single_tool_call_async` (ai_client.py:786-846)**
|
||||
Document execution sandboxing, clutch authorization, and callback handling. SSDL trace: `[I:CheckClutch] -> [B:Approved?] -> [I:run_powershell] -> [T:output]`.
|
||||
- [x] **Step 6: Verify syntax and run tests**
|
||||
Run: `pytest tests/test_ai_client_tool_loop.py tests/test_ai_client_result.py`
|
||||
Expected: Success.
|
||||
|
||||
---
|
||||
|
||||
# Phase 2: Primary Provider Senders
|
||||
|
||||
## Task 2.1: Document Primary Provider Senders
|
||||
- [x] **Step 1: Document `_send_anthropic` (ai_client.py:1188-1364)**
|
||||
Add docstring detailing cache control breakpoints, history pruning, and token tracking. SSDL trace: `[I:_ensure_anthropic_client] -> [I:_trim_anthropic_history] -> [I:client.messages.create] -> [T:Result]`.
|
||||
- [x] **Step 2: Document `_send_gemini` (ai_client.py:1431-1665)**
|
||||
Document caching states, explicit server-side cache invalidation, and chat session creation. SSDL trace: `[I:_ensure_gemini_client] -> [B:Cache Changed?] -> [I:client.caches.create] -> [I:client.chats.create] -> [T:Result]`.
|
||||
- [x] **Step 3: Document `_send_gemini_cli` (ai_client.py:1667-1776)**
|
||||
Document the headless adapter, subprocess execution, and callback wrapper. SSDL trace: `[I:run_with_tool_loop] -> [I:GeminiCliAdapter.send] -> [T:Result]`.
|
||||
- [x] **Step 4: Document `_send_deepseek` (ai_client.py:1812-2067)**
|
||||
Document token limits, custom REST client calls, and history repair loops. SSDL trace: `[I:_ensure_deepseek_client] -> [I:_repair_deepseek_history] -> [I:requests.post] -> [T:Result]`.
|
||||
- [x] **Step 5: Verify syntax and run tests**
|
||||
Run: `pytest tests/test_deepseek_provider.py tests/test_gemini_cli_integration.py`
|
||||
Expected: Success.
|
||||
|
||||
---
|
||||
|
||||
# Phase 3: Secondary Provider Senders & Helpers
|
||||
|
||||
## Task 3.1: Document Secondary Senders & Context Helpers
|
||||
- [x] **Step 1: Document `_send_minimax` (ai_client.py:2209-2251)**
|
||||
SSDL trace: `[I:_ensure_minimax_client] -> [I:_repair_minimax_history] -> [I:run_with_tool_loop] -> [T:Result]`.
|
||||
- [x] **Step 2: Document `_send_grok` (ai_client.py:2157-2203)**
|
||||
SSDL trace: `[I:_ensure_grok_client] -> [I:run_with_tool_loop] -> [T:Result]`.
|
||||
- [x] **Step 3: Document `_send_qwen` (ai_client.py:2330-2363)**
|
||||
SSDL trace: `[I:_ensure_qwen_client] -> [I:dashscope.Generation.call] -> [T:Result]`.
|
||||
- [x] **Step 4: Document `_send_llama` & `_send_llama_native` (ai_client.py:2381-2478)**
|
||||
SSDL trace: `[I:_ensure_llama_client] -> [I:run_with_tool_loop] -> [T:Result]`.
|
||||
- [x] **Step 5: Document `_reread_file_items` & `_build_file_diff_text` (ai_client.py:869-927)**
|
||||
SSDL trace: `o-> [I:get_mtime] -> [B:changed?] -> [I:read_file] -> [T:diff_text]`.
|
||||
- [x] **Step 6: Verify syntax and run all tests**
|
||||
Run: `pytest tests/` (full batch run check)
|
||||
Expected: All green.
|
||||
@@ -1,68 +0,0 @@
|
||||
# Track: SQLite-Granularity Inline Docs for ai_client.py
|
||||
|
||||
**Status:** Spec approved 2026-06-13
|
||||
**Initialized:** 2026-06-13
|
||||
**Owner:** Tier 1 Orchestrator
|
||||
**Priority:** Medium (Documentation / Core Maintenance)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
This track adds SQLite-style inline documentation to the core LLM orchestration engine in [src/ai_client.py](file:///C:/projects/manual_slop/src/ai_client.py). By enriching its dispatch loops, providers, and helper functions with clear docstrings, SSDL traces, and visual topology diagrams where relevant, we make the central AI interface highly auditable and understandable for future development and paired programming sessions.
|
||||
|
||||
---
|
||||
|
||||
## 2. Goals (Priority Order)
|
||||
|
||||
| Priority | Goal | Rationale |
|
||||
|---|---|---|
|
||||
| **A** | Document Public APIs & Core Loops (`send_result`, `send`, `run_with_tool_loop`, `_execute_tool_calls_concurrently`, `_execute_single_tool_call_async`). | These constitute the central execution loop and entry points for all AI reasoning. |
|
||||
| **A** | Document Primary Provider Senders (`_send_anthropic`, `_send_gemini`, `_send_gemini_cli`, `_send_deepseek`). | These handle context caching, token estimation, tool translation, and response normalization for the primary platforms. |
|
||||
| **B** | Document Secondary Provider Senders (`_send_minimax`, `_send_grok`, `_send_qwen`, `_send_llama`, `_send_llama_native`). | Document the integrations for regional, compatible, and local models. |
|
||||
| **B** | Document Context & Context-Refresh Helpers (`_reread_file_items`, `_build_file_diff_text`, `set_current_tier`, `get_current_tier`). | Traces file-system synchronization and thread-local tier auditing. |
|
||||
|
||||
---
|
||||
|
||||
## 3. The Documentation Convention
|
||||
Every target function gets a Python docstring (`"""`) structured as follows:
|
||||
|
||||
1. **Functional Purpose:** Summary of the component's job.
|
||||
2. **Parameters & Inputs:** Specific types.
|
||||
3. **Immediate-Mode DAG / Thread Context:**
|
||||
- **Called by:** Parent caller nodes.
|
||||
- **Calls:** Child modules or SDK methods.
|
||||
4. **SSDL computational shape:** Embedded SSDL trace string under a dedicated `SSDL:` header.
|
||||
5. **Thread Boundaries:** Confirming threading model (e.g. main thread vs async worker thread pool).
|
||||
|
||||
---
|
||||
|
||||
## 4. Phased Breakdown
|
||||
|
||||
### Phase 1: Core Dispatch Loop & Public APIs
|
||||
- `send_result`
|
||||
- `send`
|
||||
- `run_with_tool_loop`
|
||||
- `_execute_tool_calls_concurrently`
|
||||
- `_execute_single_tool_call_async`
|
||||
|
||||
### Phase 2: Primary Provider Senders
|
||||
- `_send_anthropic`
|
||||
- `_send_gemini`
|
||||
- `_send_gemini_cli`
|
||||
- `_send_deepseek`
|
||||
|
||||
### Phase 3: Secondary Provider Senders & Helpers
|
||||
- `_send_minimax`
|
||||
- `_send_grok`
|
||||
- `_send_qwen`
|
||||
- `_send_llama`
|
||||
- `_send_llama_native`
|
||||
- `_reread_file_items`
|
||||
- `_build_file_diff_text`
|
||||
|
||||
---
|
||||
|
||||
## 5. Verification Criteria
|
||||
1. **Syntax Integrity:** Run `py_check_syntax` on [src/ai_client.py](file:///C:/projects/manual_slop/src/ai_client.py) after every edit to confirm correct AST construction.
|
||||
2. **Regression Check:** Run `pytest tests/` after each phase. The addition of documentation must not alter execution paths, types, or throw warnings.
|
||||
3. **Indentation Enforcement:** Verify all docstrings strictly preserve the 1-space indentation rule in [src/ai_client.py](file:///C:/projects/manual_slop/src/ai_client.py).
|
||||
@@ -1,26 +0,0 @@
|
||||
# Track state for ai_client_docs_20260613
|
||||
# Updated as tasks complete
|
||||
|
||||
[meta]
|
||||
track_id = "ai_client_docs_20260613"
|
||||
name = "SQLite-Granularity Inline Docs for ai_client.py"
|
||||
status = "completed"
|
||||
current_phase = 3
|
||||
last_updated = "2026-06-13"
|
||||
|
||||
[blocked_by]
|
||||
|
||||
[phases]
|
||||
phase_1 = { status = "completed", checkpoint_sha = "", name = "Core Dispatch Loop & Public APIs" }
|
||||
phase_2 = { status = "completed", checkpoint_sha = "", name = "Primary Provider Senders" }
|
||||
phase_3 = { status = "completed", checkpoint_sha = "", name = "Secondary Provider Senders & Helpers" }
|
||||
|
||||
[tasks]
|
||||
# Phase 1: Core Dispatch Loop & Public APIs
|
||||
t1_1 = { status = "completed", commit_sha = "", description = "Document Public Entry Points & Dispatch Loops (send_result, send, run_with_tool_loop, _execute_tool_calls_concurrently, _execute_single_tool_call_async)" }
|
||||
|
||||
# Phase 2: Primary Provider Senders
|
||||
t2_1 = { status = "completed", commit_sha = "", description = "Document Primary Provider Senders (_send_anthropic, _send_gemini, _send_gemini_cli, _send_deepseek)" }
|
||||
|
||||
# Phase 3: Secondary Provider Senders & Helpers
|
||||
t3_1 = { status = "completed", commit_sha = "", description = "Document Secondary Senders & Context Helpers (_send_minimax, _send_grok, _send_qwen, _send_llama, _send_llama_native, _reread_file_items, _build_file_diff_text)" }
|
||||
@@ -1,9 +0,0 @@
|
||||
# Conductor Path Configuration
|
||||
|
||||
**Track ID:** conductor_path_configurable_20260306
|
||||
|
||||
**Status:** Planned
|
||||
|
||||
**See Also:**
|
||||
- [Spec](./spec.md)
|
||||
- [Plan](./plan.md)
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "conductor_path_configurable_20260306",
|
||||
"name": "Conductor Path Configuration",
|
||||
"status": "planned",
|
||||
"created_at": "2026-03-06T00:00:00Z",
|
||||
"updated_at": "2026-03-06T00:00:00Z",
|
||||
"type": "infrastructure",
|
||||
"priority": "critical"
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
# Implementation Plan: Conductor Path Configuration (conductor_path_configurable_20260306)
|
||||
|
||||
> **Reference:** [Spec](./spec.md) | [Architecture Guide](../../../docs/guide_architecture.md)
|
||||
>
|
||||
> **CRITICAL:** This is Phase 0 infrastructure. Complete this before other Phase 3 tracks.
|
||||
|
||||
## Phase 1: Create Centralized Path Module
|
||||
Focus: Establish the single source of truth for all paths
|
||||
|
||||
- [ ] Task 1.1: Initialize MMA Environment
|
||||
- Run `activate_skill mma-orchestrator` before starting
|
||||
|
||||
- [ ] Task 1.2: Create src/paths.py module
|
||||
- WHERE: `src/paths.py` (new file)
|
||||
- WHAT: Centralized path resolution module
|
||||
- HOW:
|
||||
```python
|
||||
from pathlib import Path
|
||||
import os
|
||||
import tomllib
|
||||
from typing import Optional
|
||||
|
||||
_CONFIG_PATH: Path = Path(os.environ.get("SLOP_CONFIG", "config.toml"))
|
||||
_RESOLVED: dict[str, Path] = {}
|
||||
|
||||
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
|
||||
if env_var in os.environ:
|
||||
return Path(os.environ[env_var])
|
||||
try:
|
||||
with open(_CONFIG_PATH, "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
if "paths" in cfg and config_key in cfg["paths"]:
|
||||
return Path(cfg["paths"][config_key])
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return Path(default)
|
||||
|
||||
def get_conductor_dir() -> Path:
|
||||
if "conductor_dir" not in _RESOLVED:
|
||||
_RESOLVED["conductor_dir"] = _resolve_path("SLOP_CONDUCTOR_DIR", "conductor_dir", "conductor")
|
||||
return _RESOLVED["conductor_dir"]
|
||||
|
||||
def get_logs_dir() -> Path:
|
||||
if "logs_dir" not in _RESOLVED:
|
||||
_RESOLVED["logs_dir"] = _resolve_path("SLOP_LOGS_DIR", "logs_dir", "logs/sessions")
|
||||
return _RESOLVED["logs_dir"]
|
||||
|
||||
def get_scripts_dir() -> Path:
|
||||
if "scripts_dir" not in _RESOLVED:
|
||||
_RESOLVED["scripts_dir"] = _resolve_path("SLOP_SCRIPTS_DIR", "scripts_dir", "scripts/generated")
|
||||
return _RESOLVED["scripts_dir"]
|
||||
|
||||
def get_config_path() -> Path:
|
||||
return _CONFIG_PATH
|
||||
|
||||
def get_tracks_dir() -> Path:
|
||||
return get_conductor_dir() / "tracks"
|
||||
|
||||
def get_track_state_dir(track_id: str) -> Path:
|
||||
return get_tracks_dir() / track_id
|
||||
|
||||
def get_archive_dir() -> Path:
|
||||
return get_conductor_dir() / "archive"
|
||||
|
||||
def reset_resolved() -> None:
|
||||
"""For testing only - clear cached resolutions."""
|
||||
_RESOLVED.clear()
|
||||
```
|
||||
- CODE STYLE: 1-space indentation
|
||||
- SAFETY: Lazy resolution prevents import-order issues
|
||||
|
||||
- [ ] Task 1.3: Write unit tests for paths module
|
||||
- WHERE: `tests/test_paths.py` (new file)
|
||||
- WHAT: Test path resolution logic
|
||||
- HOW: Test defaults, env vars, config overrides, precedence
|
||||
- PATTERN: Mock `os.environ`, create temp config files
|
||||
|
||||
## Phase 2: Update Core Modules
|
||||
Focus: Migrate orchestrator and project_manager to use paths module
|
||||
|
||||
- [ ] Task 2.1: Update orchestrator_pm.py
|
||||
- WHERE: `src/orchestrator_pm.py` line 10
|
||||
- WHAT: Replace `CONDUCTOR_PATH` with path function
|
||||
- HOW:
|
||||
```python
|
||||
# OLD: CONDUCTOR_PATH: Path = Path("conductor")
|
||||
# NEW:
|
||||
from src import paths
|
||||
# Then use paths.get_conductor_dir() where needed
|
||||
```
|
||||
- SAFETY: Check all usages of `CONDUCTOR_PATH` in the file
|
||||
|
||||
- [ ] Task 2.2: Update project_manager.py
|
||||
- WHERE: `src/project_manager.py` lines 240, 252, 297
|
||||
- WHAT: Replace hardcoded "conductor" with path functions
|
||||
- HOW:
|
||||
```python
|
||||
from src import paths
|
||||
# save_track_state: track_dir = paths.get_track_state_dir(track_id)
|
||||
# load_track_state: state_file = paths.get_track_state_dir(track_id) / "state.toml"
|
||||
# get_all_tracks: tracks_dir = paths.get_tracks_dir()
|
||||
```
|
||||
- SAFETY: Maintain `base_dir` parameter for backward compatibility if needed
|
||||
|
||||
## Phase 3: Update Session Logger
|
||||
Focus: Migrate session_logger.py to use paths module
|
||||
|
||||
- [ ] Task 3.1: Update session_logger.py paths
|
||||
- WHERE: `src/session_logger.py` lines 26-27
|
||||
- WHAT: Replace module-level constants with lazy resolution
|
||||
- HOW:
|
||||
```python
|
||||
# OLD:
|
||||
# _LOG_DIR: Path = Path("./logs/sessions")
|
||||
# _SCRIPTS_DIR: Path = Path("./scripts/generated")
|
||||
# NEW:
|
||||
from src import paths
|
||||
# In functions, use paths.get_logs_dir() and paths.get_scripts_dir()
|
||||
```
|
||||
- SAFETY: Module-level initialization may need to become function-level
|
||||
|
||||
- [ ] Task 3.2: Handle open_session() path resolution
|
||||
- WHERE: `src/session_logger.py` open_session function
|
||||
- WHAT: Resolve paths at call time, not import time
|
||||
- HOW: Call `paths.get_logs_dir()` inside `open_session()`
|
||||
- SAFETY: Verify no import-time side effects
|
||||
|
||||
## Phase 4: Update App Controller
|
||||
Focus: Migrate all scattered path references in app_controller.py
|
||||
|
||||
- [ ] Task 4.1: Update app_controller.py log paths
|
||||
- WHERE: `src/app_controller.py` lines 643, 674, 1241
|
||||
- WHAT: Replace hardcoded "logs/sessions" with path functions
|
||||
- HOW:
|
||||
```python
|
||||
from src import paths
|
||||
# cb_load_prior_log: initialdir=str(paths.get_logs_dir())
|
||||
# cb_prune_logs: LogRegistry(paths.get_logs_dir() / "log_registry.toml")
|
||||
# _render_log_management: Path(paths.get_logs_dir())
|
||||
```
|
||||
- SAFETY: Convert Path to str where file dialog expects string
|
||||
|
||||
- [ ] Task 4.2: Update app_controller.py conductor paths
|
||||
- WHERE: `src/app_controller.py` lines 1907, 1937
|
||||
- WHAT: Replace hardcoded "conductor" with path functions
|
||||
- HOW:
|
||||
```python
|
||||
from src import paths
|
||||
# _render_projects_panel: base = paths.get_conductor_dir()
|
||||
# _render_projects_panel: track_dir = paths.get_tracks_dir() / track_id
|
||||
```
|
||||
- SAFETY: None
|
||||
|
||||
## Phase 5: Update GUI
|
||||
Focus: Migrate gui_2.py path references
|
||||
|
||||
- [ ] Task 5.1: Update gui_2.py log path
|
||||
- WHERE: `src/gui_2.py` line 776
|
||||
- WHAT: Replace hardcoded LogRegistry path
|
||||
- HOW:
|
||||
```python
|
||||
from src import paths
|
||||
# LogRegistry(paths.get_logs_dir() / "log_registry.toml")
|
||||
```
|
||||
- SAFETY: None
|
||||
|
||||
## Phase 6: Configuration Support
|
||||
Focus: Add paths section to config.toml
|
||||
|
||||
- [ ] Task 6.1: Add [paths] section to config.toml
|
||||
- WHERE: `config.toml`
|
||||
- WHAT: Add documented paths section
|
||||
- HOW:
|
||||
```toml
|
||||
# Path Configuration (optional - defaults shown)
|
||||
# Override with environment variables: SLOP_CONDUCTOR_DIR, SLOP_LOGS_DIR, SLOP_SCRIPTS_DIR
|
||||
|
||||
[paths]
|
||||
# conductor_dir = "conductor"
|
||||
# logs_dir = "logs/sessions"
|
||||
# scripts_dir = "scripts/generated"
|
||||
```
|
||||
- SAFETY: Comments-only defaults don't change behavior
|
||||
|
||||
- [ ] Task 6.2: Document environment variables
|
||||
- WHERE: `conductor/tech-stack.md` or `README.md`
|
||||
- WHAT: Add documentation for path environment variables
|
||||
- HOW: Table of env vars with descriptions
|
||||
|
||||
## Phase 7: Verification
|
||||
Focus: Ensure all changes work correctly
|
||||
|
||||
- [ ] Task 7.1: Run full test suite
|
||||
- COMMAND: `uv run pytest tests/ -v --timeout=60`
|
||||
- EXPECTED: All existing tests pass
|
||||
- BATCHING: Run in batches of 4 files max
|
||||
|
||||
- [ ] Task 7.2: Test with custom paths
|
||||
- HOW: Set env vars, verify app uses custom paths
|
||||
- VERIFY: Check logs go to custom dir, tracks load from custom dir
|
||||
|
||||
- [ ] Task 7.3: Test default behavior unchanged
|
||||
- HOW: Run without env vars, verify defaults work
|
||||
- VERIFY: All paths resolve to original defaults
|
||||
|
||||
- [ ] Task 7.4: Conductor - Phase Verification
|
||||
- Run: `uv run pytest tests/test_paths.py -v`
|
||||
- Manual: Start app, verify no path-related errors
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Import Order Considerations
|
||||
- `paths.py` must not import from other src modules
|
||||
- Other modules can safely import from `paths.py`
|
||||
- `session_logger.py` needs careful handling due to module-level state
|
||||
|
||||
### Backward Compatibility
|
||||
- All existing `base_dir` parameters continue to work
|
||||
- Default behavior is unchanged when no config/env overrides
|
||||
- Existing `SLOP_CONFIG` env var pattern is preserved
|
||||
|
||||
### Files Modified
|
||||
- `src/paths.py` (new)
|
||||
- `src/orchestrator_pm.py`
|
||||
- `src/project_manager.py`
|
||||
- `src/session_logger.py`
|
||||
- `src/app_controller.py`
|
||||
- `src/gui_2.py`
|
||||
- `config.toml`
|
||||
- `tests/test_paths.py` (new)
|
||||
|
||||
### Code Style Checklist
|
||||
- [ ] 1-space indentation throughout
|
||||
- [ ] CRLF line endings on Windows
|
||||
- [ ] No comments unless documenting public API
|
||||
- [ ] Type hints on all public functions
|
||||
- [ ] Follow existing module patterns
|
||||
@@ -1,185 +0,0 @@
|
||||
# Track Specification: Conductor Path Configuration (conductor_path_configurable_20260306)
|
||||
|
||||
## Overview
|
||||
Eliminate all hardcoded paths in the application. Make directory paths configurable via `config.toml` or environment variables, allowing the running app to use different directories from development setup. This is **Phase 0 - Critical Infrastructure** that must be completed before other Phase 3 tracks.
|
||||
|
||||
## Current State Audit
|
||||
|
||||
### Already Implemented (DO NOT re-implement)
|
||||
|
||||
#### Environment Variable Pattern (models.py)
|
||||
- **`CONFIG_PATH`**: `Path(os.environ.get("SLOP_CONFIG", "config.toml"))` - This pattern exists and should be replicated.
|
||||
|
||||
#### Hardcoded Path Inventory
|
||||
|
||||
| File | Line | Current Hardcode | Variable/Usage |
|
||||
|------|------|------------------|----------------|
|
||||
| `src/orchestrator_pm.py` | 10 | `"conductor"` | `CONDUCTOR_PATH: Path = Path("conductor")` |
|
||||
| `src/project_manager.py` | 240 | `"conductor"` | `track_dir = Path(base_dir) / "conductor" / "tracks" / track_id` |
|
||||
| `src/project_manager.py` | 252 | `"conductor"` | `state_file = Path(base_dir) / "conductor" / "tracks" / track_id / "state.toml"` |
|
||||
| `src/project_manager.py` | 297 | `"conductor"` | `tracks_dir = Path(base_dir) / "conductor" / "tracks"` |
|
||||
| `src/app_controller.py` | 1907 | `"conductor"` | `base = Path("conductor")` |
|
||||
| `src/app_controller.py` | 1937 | `"conductor"` | `track_dir = Path("conductor/tracks") / track_id` |
|
||||
| `src/app_controller.py` | 643 | `"logs/sessions"` | `initialdir="logs/sessions"` |
|
||||
| `src/app_controller.py` | 674 | `"logs/sessions"` | `LogRegistry("logs/sessions/log_registry.toml")` |
|
||||
| `src/app_controller.py` | 1241 | `"logs/sessions"` | `log_dir = Path("logs/sessions")` |
|
||||
| `src/gui_2.py` | 776 | `"logs/sessions"` | `LogRegistry("logs/sessions/log_registry.toml")` |
|
||||
| `src/session_logger.py` | 26 | `"./logs/sessions"` | `_LOG_DIR: Path = Path("./logs/sessions")` |
|
||||
| `src/session_logger.py` | 27 | `"./scripts/generated"` | `_SCRIPTS_DIR: Path = Path("./scripts/generated")` |
|
||||
|
||||
#### Notes on Existing Implementation
|
||||
- `session_logger.py` has module-level path constants that are set once at import time
|
||||
- `project_manager.py` uses `base_dir` parameter but hardcodes `"conductor"` as subdirectory
|
||||
- `app_controller.py` has multiple scattered hardcodes that need consolidation
|
||||
|
||||
### Gaps to Fill (This Track's Scope)
|
||||
- No centralized path configuration module
|
||||
- No `config.toml` section for paths
|
||||
- No environment variable support for logs, scripts, or conductor directories
|
||||
- Duplicate path definitions across files
|
||||
|
||||
## Architectural Constraints
|
||||
|
||||
### Single Source of Truth
|
||||
- All paths MUST be resolved through a single `paths.py` module
|
||||
- No file should hardcode directory strings directly
|
||||
|
||||
### Initialization Order
|
||||
- Path resolution MUST happen before any module imports that use paths
|
||||
- `session_logger.py` imports at module level - may need lazy initialization
|
||||
|
||||
### Backward Compatibility
|
||||
- Default paths MUST remain unchanged (relative to project root)
|
||||
- Existing `SLOP_CONFIG` env var pattern MUST be preserved
|
||||
- All existing function signatures that take `base_dir` MUST continue to work
|
||||
|
||||
### Thread Safety
|
||||
- Path resolution is read-only after initialization
|
||||
- No locks needed once paths are resolved
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
### Key Integration Points
|
||||
|
||||
| File | Lines | Purpose |
|
||||
|------|-------|---------|
|
||||
| `src/models.py` | 27-28 | `CONFIG_PATH` pattern to replicate |
|
||||
| `src/orchestrator_pm.py` | 10 | `CONDUCTOR_PATH` to replace |
|
||||
| `src/project_manager.py` | 238-297 | Track state paths to update |
|
||||
| `src/session_logger.py` | 26-27 | `_LOG_DIR`, `_SCRIPTS_DIR` to update |
|
||||
| `src/app_controller.py` | 643, 674, 1241, 1907, 1937 | Multiple hardcodes to fix |
|
||||
| `src/gui_2.py` | 776 | LogRegistry path to update |
|
||||
| `config.toml` | N/A | Add `[paths]` section |
|
||||
|
||||
### Proposed Module Structure
|
||||
|
||||
```python
|
||||
# src/paths.py (NEW FILE)
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
from typing import Optional
|
||||
import tomllib
|
||||
|
||||
_CONFIG_PATH: Path = Path(os.environ.get("SLOP_CONFIG", "config.toml"))
|
||||
_RESOLVED: dict[str, Path] = {}
|
||||
|
||||
def _resolve_path(env_var: str, config_key: str, default: Path) -> Path:
|
||||
"""Resolve path from env var, config, or default."""
|
||||
if env_var in os.environ:
|
||||
return Path(os.environ[env_var])
|
||||
if _CONFIG_PATH.exists():
|
||||
with open(_CONFIG_PATH, "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
if "paths" in cfg and config_key in cfg["paths"]:
|
||||
return Path(cfg["paths"][config_key])
|
||||
return default
|
||||
|
||||
def get_conductor_dir() -> Path: ...
|
||||
def get_logs_dir() -> Path: ...
|
||||
def get_scripts_dir() -> Path: ...
|
||||
def get_config_path() -> Path: ...
|
||||
```
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### FR1: Config File Support
|
||||
Add `[paths]` section to `config.toml`:
|
||||
```toml
|
||||
[paths]
|
||||
conductor_dir = "conductor"
|
||||
logs_dir = "logs/sessions"
|
||||
scripts_dir = "scripts/generated"
|
||||
# config_file uses SLOP_CONFIG env var (existing)
|
||||
```
|
||||
|
||||
### FR2: Environment Variable Support
|
||||
| Env Var | Config Key | Default |
|
||||
|---------|------------|---------|
|
||||
| `SLOP_CONDUCTOR_DIR` | `conductor_dir` | `"conductor"` |
|
||||
| `SLOP_LOGS_DIR` | `logs_dir` | `"logs/sessions"` |
|
||||
| `SLOP_SCRIPTS_DIR` | `scripts_dir` | `"scripts/generated"` |
|
||||
| `SLOP_CONFIG` | (existing) | `"config.toml"` |
|
||||
|
||||
### FR3: Centralized Path Module
|
||||
Create `src/paths.py` with:
|
||||
- `get_conductor_dir() -> Path`
|
||||
- `get_logs_dir() -> Path`
|
||||
- `get_scripts_dir() -> Path`
|
||||
- `get_config_path() -> Path`
|
||||
- `get_tracks_dir() -> Path` (conductor/tracks)
|
||||
- `get_track_state_dir(track_id: str) -> Path`
|
||||
|
||||
### FR4: Module Migration
|
||||
Update all modules to import from `paths.py`:
|
||||
- `orchestrator_pm.py`: Use `paths.get_conductor_dir()`
|
||||
- `project_manager.py`: Use `paths.get_tracks_dir()`
|
||||
- `session_logger.py`: Use `paths.get_logs_dir()`, `paths.get_scripts_dir()`
|
||||
- `app_controller.py`: Use all path functions
|
||||
- `gui_2.py`: Use `paths.get_logs_dir()`
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
| Requirement | Constraint |
|
||||
|-------------|------------|
|
||||
| Import Order | `paths.py` must be importable without side effects |
|
||||
| Lazy Resolution | Path resolution on first access, not at import |
|
||||
| No Breaking Changes | All existing code continues to work |
|
||||
| Default Behavior | Unchanged when no config/env overrides |
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
- Test each path function returns expected default
|
||||
- Test env var override for each path
|
||||
- Test config.toml override for each path
|
||||
- Test env var takes precedence over config
|
||||
|
||||
### Integration Tests
|
||||
- Verify app starts with custom paths
|
||||
- Verify tracks load from custom conductor dir
|
||||
- Verify logs write to custom logs dir
|
||||
|
||||
### Test Isolation
|
||||
- Reset `_RESOLVED` dict between tests
|
||||
- Mock `os.environ` for env var tests
|
||||
- Use temp config files for config tests
|
||||
|
||||
## Out of Scope
|
||||
- Runtime path changes (paths are resolved once at startup)
|
||||
- Path validation (directory existence checks)
|
||||
- Relative path resolution (always resolve to absolute)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] `src/paths.py` module created with all path functions
|
||||
- [ ] `config.toml` has `[paths]` section
|
||||
- [ ] All 4 environment variables work
|
||||
- [ ] Default paths remain unchanged
|
||||
- [ ] `orchestrator_pm.py` uses `paths.get_conductor_dir()`
|
||||
- [ ] `project_manager.py` uses path functions
|
||||
- [ ] `session_logger.py` uses path functions
|
||||
- [ ] `app_controller.py` uses path functions
|
||||
- [ ] `gui_2.py` uses path functions
|
||||
- [ ] All existing tests pass
|
||||
- [ ] New path resolution tests pass
|
||||
- [ ] 1-space indentation maintained
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"id": "result_migration_app_controller_20260618",
|
||||
"name": "Result Migration - Sub-Track 3 (App Controller)",
|
||||
"date": "2026-06-18",
|
||||
"type": "refactor",
|
||||
"priority": "A",
|
||||
"spec": "conductor/tracks/result_migration_app_controller_20260618/spec.md",
|
||||
"plan": "conductor/tracks/result_migration_app_controller_20260618/plan.md",
|
||||
"status": "active",
|
||||
"umbrella": "result_migration_20260616",
|
||||
"sub_track_index": 3,
|
||||
"blocked_by": {
|
||||
"result_migration_small_files_20260617": "shipped 2026-06-17"
|
||||
},
|
||||
"blocks": {},
|
||||
"scope": {
|
||||
"new_files": [
|
||||
"tests/test_app_controller_result.py",
|
||||
"docs/reports/TRACK_COMPLETION_result_migration_app_controller_20260618.md"
|
||||
],
|
||||
"modified_files": [
|
||||
"src/app_controller.py",
|
||||
"tests/test_app_controller_offloading.py",
|
||||
"tests/test_audit_exception_handling_heuristics.py",
|
||||
"conductor/tracks.md",
|
||||
"conductor/tracks/result_migration_app_controller_20260618/state.toml",
|
||||
"conductor/tracks/result_migration_app_controller_20260618/metadata.json",
|
||||
"conductor/tracks/result_migration_app_controller_20260618/plan.md",
|
||||
"conductor/tracks/result_migration_20260616/spec.md"
|
||||
],
|
||||
"deleted_files": []
|
||||
},
|
||||
"verification_criteria": [
|
||||
"src/app_controller.py has zero INTERNAL_BROAD_CATCH sites (32 migrated)",
|
||||
"src/app_controller.py has zero INTERNAL_SILENT_SWALLOW sites (8 migrated with logging.debug per Heuristic #19)",
|
||||
"src/app_controller.py has zero INTERNAL_RETHROW sites (4 classified: legitimate patterns stay; SUSPICIOUS migrated)",
|
||||
"src/app_controller.py has zero INTERNAL_OPTIONAL_RETURN sites (1 migrated to Result[int] or nil-sentinel)",
|
||||
"src/app_controller.py preserves 15 BOUNDARY_FASTAPI sites (unchanged, per styleguide Boundary Types section)",
|
||||
"src/app_controller.py preserves 2 BOUNDARY_SDK sites (unchanged, per styleguide Boundary Types section)",
|
||||
"src/app_controller.py preserves 4 INTERNAL_COMPLIANT sites (unchanged, already compliant)",
|
||||
"src/app_controller.py preserves 1 INTERNAL_PROGRAMMER_RAISE site (unchanged, per Fail Early pattern)",
|
||||
"tests/test_app_controller_result.py exists with 5+ tests, all pass",
|
||||
"tests/test_app_controller_offloading.py has 2 new unwrap-path tests, all pass",
|
||||
"tests/test_tool_presets_execution::test_tool_ask_approval passes (Regression 1 fixed)",
|
||||
"tests/test_extended_sims::test_execution_sim_live passes (Regression 2 fixed)",
|
||||
"uv run python scripts/audit_exception_handling.py --by-size shows 0 INTERNAL_BROAD_CATCH and 0 INTERNAL_SILENT_SWALLOW for src/app_controller.py",
|
||||
"uv run python scripts/run_tests_batched.py shows no new regressions (882 passed / 17 skipped / 2 xfailed, with the 2 previously-failing tests now passing)",
|
||||
"docs/reports/TRACK_COMPLETION_result_migration_app_controller_20260618.md exists with the 7 standard sections"
|
||||
],
|
||||
"regressions_and_pre_existing_failures": [
|
||||
{
|
||||
"name": "test_tool_presets_execution::test_tool_ask_approval",
|
||||
"cause": "session_logger.log_tool_call was partially migrated to return Result but the call site in _offload_entry_payload was not updated",
|
||||
"fix_phase": 1,
|
||||
"fix_task": 1.3
|
||||
},
|
||||
{
|
||||
"name": "test_extended_sims::test_execution_sim_live",
|
||||
"cause": "downstream effect of test_tool_ask_approval failure; the live GUI runs the same _offload_entry_payload path",
|
||||
"fix_phase": 1,
|
||||
"fix_task": 1.3
|
||||
}
|
||||
],
|
||||
"pre_existing_failures_remaining": [],
|
||||
"deferred_to_followup_tracks": [
|
||||
{
|
||||
"title": "Sub-track 4: result_migration_gui_2",
|
||||
"description": "Migrate src/gui_2.py (260KB) to the Result convention. The umbrella's sub-track 4 plan (line 276 of conductor/tracks/result_migration_20260616/spec.md) covers the 55 sites in gui_2.py.",
|
||||
"track_status": "planned (per umbrella)"
|
||||
},
|
||||
{
|
||||
"title": "Sub-track 5: result_migration_baseline_cleanup",
|
||||
"description": "Close the remaining 77 violations in the 3 refactored baseline files (mcp_client.py, ai_client.py, rag_engine.py). Per umbrella sub-track 5 (line 296-309 of result_migration_20260616/spec.md).",
|
||||
"track_status": "planned (per umbrella)"
|
||||
}
|
||||
],
|
||||
"estimated_effort": {
|
||||
"method": "scope (per workflow.md Tier 1 Track Initialization Rules). NO day estimates.",
|
||||
"scope": "1 source file (src/app_controller.py) modified across 6 phases; 45 migration sites organized into 4 bulk batches + 3 single-site tasks; 1 new test file (test_app_controller_result.py) + 2 test files updated; 4 metadata/plan/state files; 1 end-of-track report. 18 atomic commits."
|
||||
},
|
||||
"risk_register": [
|
||||
{
|
||||
"risk": "Migrating __getattr__ may break Python's attribute lookup protocol (e.g., hasattr)",
|
||||
"likelihood": "medium",
|
||||
"mitigation": "Phase 4 task 4.1 explicitly tests test_app_getattr_hasattr_bug.py and test_app_controller_getattr_ui_bug.py; SUSPICIOUS rethrows are migrated; Pattern 1/2/3 legitimate rethrows stay"
|
||||
},
|
||||
{
|
||||
"risk": "Migrating 32 broad-catch sites changes error reporting semantics that downstream code may depend on",
|
||||
"likelihood": "medium",
|
||||
"mitigation": "Each batch is committed separately; the 2 new Result tests verify the contract; the batched suite is re-run at the end of Phase 5 to catch downstream breakage"
|
||||
},
|
||||
{
|
||||
"risk": "The audit's per-category count may shift as the migration proceeds (the script may reclassify sites based on context)",
|
||||
"likelihood": "low",
|
||||
"mitigation": "The audit is run after each phase; if a site moves from INTERNAL_BROAD_CATCH to BOUNDARY_FASTAPI mid-migration, the plan task description is updated to reflect the new category"
|
||||
},
|
||||
{
|
||||
"risk": "Scope is larger than the umbrella estimated (45 vs 22 migration sites); the XL T-shirt size may understate the work",
|
||||
"likelihood": "medium",
|
||||
"mitigation": "The umbrella spec is updated post-track (Phase 5 task 5.6) to reflect the actual count; the audit's per-category output is the source of truth"
|
||||
},
|
||||
{
|
||||
"risk": "The 2 known regressions (test_tool_ask_approval, test_execution_sim_live) may have additional root causes beyond the log_tool_call half-migration",
|
||||
"likelihood": "low",
|
||||
"mitigation": "Phase 1 task 1.3 is the regression fix; if the tests still fail after the fix, the implementation investigates before Phase 2 begins (do not loop; read code, predict, fix once, report)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
# Plan: Result Migration — Sub-Track 3 (App Controller)
|
||||
|
||||
**Sub-track:** `result_migration_app_controller_20260618` (3rd of 5 sub-tracks)
|
||||
**Umbrella:** `result_migration_20260616`
|
||||
**Date:** 2026-06-18
|
||||
**Owner:** Tier 2 Tech Lead
|
||||
**Base commit:** `5107f3ca` (merge of `tier2/live_gui_test_fixes_20260618` into `tier2/result_migration_small_files_20260617`)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup + Fix the regression (highest priority)
|
||||
|
||||
Focus: register the sub-track, then immediately fix the 2 known regressions (test_tool_ask_approval + test_execution_sim_live) so subsequent phases can run against a green tier-3-live_gui.
|
||||
|
||||
### Task 1.1: Create sub-track folder
|
||||
- **WHERE:** `conductor/tracks/result_migration_app_controller_20260618/`
|
||||
- **WHAT:** spec.md (exists), plan.md (this file), metadata.json, state.toml
|
||||
- **HOW:** Write the 3 new files following the umbrella spec pattern. The spec.md is already written by Tier 1.
|
||||
- **SAFETY:** None (new files only).
|
||||
- **COMMIT:** `conductor(track): spec/plan/metadata/state for result_migration_app_controller_20260618`
|
||||
- **GIT NOTE:** Summary of sub-track 3 scope; references the 2 known regressions.
|
||||
|
||||
### Task 1.2: Update `conductor/tracks.md`
|
||||
- **WHERE:** `conductor/tracks.md` (after the umbrella row, before sub-track 4)
|
||||
- **WHAT:** Add a row for the new sub-track
|
||||
- **HOW:** Same pattern as the umbrella and the existing sub-tracks
|
||||
- **SAFETY:** None (documentation only).
|
||||
- **COMMIT:** `conductor: register result_migration_app_controller_20260618 in tracks.md`
|
||||
- **GIT NOTE:** 1-sentence note
|
||||
|
||||
### Task 1.3: Fix `_offload_entry_payload` call site (Regression 1)
|
||||
- **WHERE:** `src/app_controller.py:3709-3725` (`_offload_entry_payload` method)
|
||||
- **WHAT:** Unwrap the `Result` returned by `session_logger.log_tool_output` and `session_logger.log_tool_call`. The current code does `Path(ref_path).name` where `ref_path` is a `Result` object — `Path()` expects a string.
|
||||
- **HOW:** Per FR5 in spec.md:
|
||||
```python
|
||||
def _offload_entry_payload(self, entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
optimized = copy.deepcopy(entry)
|
||||
kind = optimized.get("kind")
|
||||
payload = optimized.get("payload", {})
|
||||
if kind == "tool_result" and "output" in payload:
|
||||
output = payload["output"]
|
||||
ref_result = session_logger.log_tool_output(output)
|
||||
if ref_result.ok and ref_result.data:
|
||||
filename = Path(ref_result.data).name
|
||||
payload["output"] = f"[REF:{filename}]"
|
||||
elif ref_result.errors:
|
||||
logging.debug("offload tool_output failed: %s", ref_result.errors[0].ui_message())
|
||||
if kind == "tool_call" and "script" in payload:
|
||||
script = payload["script"]
|
||||
ref_result = session_logger.log_tool_call(script, "LOG_ONLY", None)
|
||||
if ref_result.ok and ref_result.data:
|
||||
filename = Path(ref_result.data).name
|
||||
payload["script"] = f"[REF:{filename}]"
|
||||
elif ref_result.errors:
|
||||
logging.debug("offload tool_call failed: %s", ref_result.errors[0].ui_message())
|
||||
return optimized
|
||||
```
|
||||
- **SAFETY:** The function signature is unchanged. The optimization (small payload via `[REF:filename]`) is preserved for both success and failure paths. The error path now logs at `logging.debug` (per Heuristic #19); on success the file content is referenced.
|
||||
- **VERIFY:** `uv run python -m pytest tests/test_app_controller_offloading.py tests/test_tool_presets_execution.py -v` — `test_tool_ask_approval` passes; `test_on_comms_entry_tool_result_offloading` still passes.
|
||||
- **COMMIT:** `fix(app_controller): _offload_entry_payload unwraps Result from session_logger (regression fix)`
|
||||
- **GIT NOTE:** Closes the regression in `test_tool_ask_approval`; the `session_logger.log_tool_call` was partially migrated to return `Result` but the call site was not updated. The convention's "AND over OR" pattern handles it here.
|
||||
|
||||
### Task 1.4: Add test for the unwrap path
|
||||
- **WHERE:** `tests/test_app_controller_offloading.py` (existing file; add 2 new tests)
|
||||
- **WHAT:** Add 2 tests:
|
||||
1. `test_offload_unwraps_result_success` — verify that when `log_tool_output` returns a successful `Result[data=path]`, the payload gets `[REF:filename]`.
|
||||
2. `test_offload_logs_debug_on_result_errors` — verify that when `log_tool_output` returns a `Result` with errors, a `logging.debug` is emitted and the payload is unchanged.
|
||||
- **HOW:** Mock `session_logger.log_tool_output` and `log_tool_call` to return `Result` objects; assert the payload and the log call.
|
||||
- **SAFETY:** Test-only changes; no production risk.
|
||||
- **VERIFY:** The 2 new tests pass; existing 2 offloading tests still pass.
|
||||
- **COMMIT:** `test(app_controller): offloading - verify Result unwrap in success and error paths`
|
||||
- **GIT NOTE:** Tests for FR5; covers the regression from task 1.3.
|
||||
|
||||
### Task 1.5: Run the regression test and confirm both fixes
|
||||
- **COMMAND:** `uv run python -m pytest tests/test_tool_presets_execution.py::test_tool_ask_approval tests/test_extended_sims.py::test_execution_sim_live -v`
|
||||
- **EXPECT:** Both pass.
|
||||
- **COMMIT:** No commit (verification only).
|
||||
- **NOTE:** If `test_execution_sim_live` still fails, investigate the failure mode (may be a separate issue from Regression 1).
|
||||
|
||||
### Task 1.6: Phase 1 checkpoint commit
|
||||
- **COMMIT:** `conductor(plan): mark Phase 1 complete (regression fix)`
|
||||
- **GIT NOTE:** Phase 1 = 2 known regressions fixed; verified by `test_tool_ask_approval` + `test_execution_sim_live`. Now safe to proceed with the bulk migration.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Migrate the 32 INTERNAL_BROAD_CATCH sites (bulk)
|
||||
|
||||
Focus: the main migration work. 32 sites, organized into 4 sub-batches by context (callback handlers, project ops, conductor ops, GUI tasks). Each sub-batch is 6-10 sites touching the same file; one commit per sub-batch.
|
||||
|
||||
### Task 2.1: Create `tests/test_app_controller_result.py` (the new test file)
|
||||
- **WHERE:** `tests/test_app_controller_result.py` (NEW)
|
||||
- **WHAT:** 5+ tests verifying Result return types for the migrated methods (placeholder tests that will be filled in as the migrations land). Initial tests can be:
|
||||
1. `test_offload_entry_payload_returns_dict` — sanity check.
|
||||
2. `test_migrated_method_returns_result_when_no_error` — pattern template.
|
||||
3. `test_migrated_method_returns_result_with_error_on_failure` — pattern template.
|
||||
4. `test_migrated_method_never_raises_exception` — verifies the broad-catch is gone.
|
||||
5. `test_offload_entry_payload_preserves_unchanged_payload` — verifies the no-op path.
|
||||
- **HOW:** Import `Result`, `ErrorInfo`, `ErrorKind` from `src.result_types`. Model on `tests/test_ai_client_result.py`.
|
||||
- **SAFETY:** Test-only changes; no production risk.
|
||||
- **COMMIT:** `test(app_controller): scaffold tests/test_app_controller_result.py with 5 Result-pattern tests`
|
||||
- **GIT NOTE:** The 5 tests use generic placeholders that become specific per migration in subsequent tasks. The scaffolding defines the pattern.
|
||||
|
||||
### Task 2.2: Migrate batch 1 — callback handlers (4 sites)
|
||||
- **WHERE:** `src/app_controller.py:537 (_handle_custom_callback)`, `:579 (_handle_click)`, `:2046 (cb_load_prior_log)`, `:2068 (cb_load_prior_log)`, `:2081 (cb_load_prior_log)`
|
||||
- **WHAT:** Convert `except Exception as e: pass` (or `print(...)`) to `except <SpecificException> as e: return Result(data=None, errors=[...])`. The callback may need to return a `Result`; if the caller doesn't use the return value, wrap the body in a `try/except` that returns a result and is logged.
|
||||
- **HOW:** For each site:
|
||||
1. Read the snippet + 3 lines of context with `get_file_slice`.
|
||||
2. Identify the specific exception (KeyError? AttributeError? OSError?).
|
||||
3. Add `from src.result_types import Result, ErrorInfo, ErrorKind` if not imported.
|
||||
4. Replace the broad `except Exception` with the specific one.
|
||||
5. Return a `Result` with the appropriate data and errors.
|
||||
- **SAFETY:** The callback's caller may not be Result-aware; the migration may need to update the caller's signature. Track this in the plan task description.
|
||||
- **VERIFY:** The 4 migrated sites + the 2 new tests in `test_app_controller_result.py` pass.
|
||||
- **COMMIT:** `refactor(app_controller): migrate 5 callback sites to Result (batch 1)`
|
||||
- **GIT NOTE:** Specific exceptions caught per site; Result return type.
|
||||
|
||||
### Task 2.3: Migrate batch 2 — project ops (5 sites)
|
||||
- **WHERE:** `src/app_controller.py:2129 (run_manual_prune)`, `:2140 (_load_active_project)`, `:2154 (_load_active_project)`, `:2195 (run_prune)`, `:2890 (_refresh_from_project)`, `:2944 (_save_active_project)`
|
||||
- **WHAT:** Same pattern as 2.2
|
||||
- **SAFETY:** Project ops have side effects (file I/O). The migration must preserve the side-effect semantics while changing the error reporting.
|
||||
- **VERIFY:** Project-op tests + the 2 new Result tests pass.
|
||||
- **COMMIT:** `refactor(app_controller): migrate 6 project-op sites to Result (batch 2)`
|
||||
- **GIT NOTE:** Project ops side effects preserved; Result error reporting added.
|
||||
|
||||
### Task 2.4: Migrate batch 3 — conductor / track ops (8 sites)
|
||||
- **WHERE:** `src/app_controller.py:3057 (_run)`, `:3084 (do_fetch)`, `:3094 (do_fetch)`, `:4237 (_start_track_logic)`, `:4349 (_cb_run_conductor_setup)`, `:4446 (_cb_load_track)`, `:4475 (_push_mma_state_update)`, `:4504 (_load_active_tickets)`
|
||||
- **WHAT:** Same pattern as 2.2
|
||||
- **SAFETY:** Conductor ops interact with the MMA state. The migration must NOT change the state-mutation order; only the error reporting.
|
||||
- **VERIFY:** MMA tests + the 2 new Result tests pass.
|
||||
- **COMMIT:** `refactor(app_controller): migrate 8 conductor/track sites to Result (batch 3)`
|
||||
- **GIT NOTE:** Conductor ops state order preserved; Result error reporting added.
|
||||
|
||||
### Task 2.5: Migrate batch 4 — worker / task ops (8 sites)
|
||||
- **WHERE:** `src/app_controller.py:3434 (worker)`, `:3471 (worker)`, `:3542 (worker)`, `:3635 (_handle_request_event)`, `:3648 (_handle_request_event)`, `:4070 (_bg_task)`, `:4100 (_bg_task)`, `:1669 (_process_pending_gui_tasks)`, `:1420 (_update_inject_preview)`, `:1480 (_do_rag_sync)`, `:1947 (replace_ref)`
|
||||
- **WHAT:** Same pattern as 2.2
|
||||
- **SAFETY:** Worker / task ops run on background threads. The migration must be thread-safe (no shared mutable state changes that aren't already locked).
|
||||
- **VERIFY:** Worker tests + the 2 new Result tests pass.
|
||||
- **COMMIT:** `refactor(app_controller): migrate 11 worker/task sites to Result (batch 4)`
|
||||
- **GIT NOTE:** Worker ops thread safety preserved; Result error reporting added.
|
||||
|
||||
### Task 2.6: Phase 2 checkpoint commit
|
||||
- **COMMIT:** `conductor(plan): mark Phase 2 complete (32 INTERNAL_BROAD_CATCH sites migrated)`
|
||||
- **GIT NOTE:** Phase 2 = 32 broad-catch sites migrated; the audit's `INTERNAL_BROAD_CATCH` count for `app_controller.py` is now 0.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Migrate the 8 INTERNAL_SILENT_SWALLOW sites
|
||||
|
||||
Focus: add `logging.debug` per Heuristic #19; convert return to `Result[T]`.
|
||||
|
||||
### Task 3.1: Migrate SIGINT and timeline sites (3 sites)
|
||||
- **WHERE:** `src/app_controller.py:751 (_on_sigint)`, `:756 (_install_sigint_exit_handler)`, `:1294 (mark_first_frame_rendered)`, `:1376 (_on_warmup_complete_for_timeline)`
|
||||
- **WHAT:** Add `logging.debug("swallowed exception: %s", e, extra={"source": "<ctx>"})`; convert return to `Result[None]` (`OK` on success, `Result(data=None, errors=[...])` on swallow).
|
||||
- **VERIFY:** The 4 sites + the 2 new Result tests pass.
|
||||
- **COMMIT:** `refactor(app_controller): migrate 4 SIGINT/timeline sites to Result with debug logging (silent swallow batch 1)`
|
||||
- **GIT NOTE:** Heuristic #19 satisfied; Result error side-channel.
|
||||
|
||||
### Task 3.2: Migrate MCP and worker sites (4 sites)
|
||||
- **WHERE:** `src/app_controller.py:1566 (mcp_config_json)`, `:2389 (queue_fallback)`, `:4098 (_bg_task)`, `:4192 (_start_track_logic)`
|
||||
- **WHAT:** Same pattern as 3.1
|
||||
- **VERIFY:** The 4 sites + the 2 new Result tests pass.
|
||||
- **COMMIT:** `refactor(app_controller): migrate 4 MCP/worker sites to Result with debug logging (silent swallow batch 2)`
|
||||
- **GIT NOTE:** Heuristic #19 satisfied; Result error side-channel.
|
||||
|
||||
### Task 3.3: Phase 3 checkpoint commit
|
||||
- **COMMIT:** `conductor(plan): mark Phase 3 complete (8 INTERNAL_SILENT_SWALLOW sites migrated)`
|
||||
- **GIT NOTE:** Phase 3 = 8 silent-swallow sites migrated; the audit's `INTERNAL_SILENT_SWALLOW` count for `app_controller.py` is now 0.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Classify 4 INTERNAL_RETHROW + migrate 1 INTERNAL_OPTIONAL_RETURN
|
||||
|
||||
Focus: the smaller, judgment-required categories. Each is a per-site decision.
|
||||
|
||||
### Task 4.1: Classify the 2 `__getattr__` rethrow sites
|
||||
- **WHERE:** `src/app_controller.py:1225 (__getattr__)`, `:1251 (__getattr__)`
|
||||
- **WHAT:** Read the snippet + 3 lines of context. Determine pattern:
|
||||
- If catching + re-raising the SAME exception: SUSPICIOUS, migrate to Result.
|
||||
- If catching + re-raising as a different type (e.g., AttributeError → KeyError): legitimate, stay.
|
||||
- If catching + adding context (logging) + re-raising: legitimate, stay; add `logging.debug` per Heuristic #19.
|
||||
- **SAFETY:** `__getattr__` is part of Python's attribute lookup protocol. Removing the try/except changes the behavior for `hasattr` and other introspection. The migration must preserve the lookup semantics.
|
||||
- **VERIFY:** `tests/test_app_getattr_hasattr_bug.py` and `tests/test_app_controller_getattr_ui_bug.py` pass.
|
||||
- **COMMIT:** `refactor(app_controller): classify __getattr__ rethrow sites (Pattern 1/2/3 or migrate)`
|
||||
- **GIT NOTE:** Per-site rationale documented in the commit body.
|
||||
|
||||
### Task 4.2: Classify the 2 `load_context_preset` rethrow sites
|
||||
- **WHERE:** `src/app_controller.py:2983 (load_context_preset)`, `:2986 (load_context_preset)`
|
||||
- **WHAT:** Same pattern analysis as 4.1
|
||||
- **VERIFY:** Context preset tests pass.
|
||||
- **COMMIT:** `refactor(app_controller): classify load_context_preset rethrow sites (Pattern 1/2/3 or migrate)`
|
||||
- **GIT NOTE:** Per-site rationale documented in the commit body.
|
||||
|
||||
### Task 4.3: Migrate the `cold_start_ts` Optional site
|
||||
- **WHERE:** `src/app_controller.py:1358 (cold_start_ts)`
|
||||
- **WHAT:** Read the call sites to determine the right shape (nil-sentinel vs `Result[int]`). Then implement per FR4.
|
||||
- **HOW:**
|
||||
1. Grep for `cold_start_ts` call sites (expect 1-3).
|
||||
2. For each call site, determine if it uses `if x is not None:` or has separate "set" vs "missing" semantics.
|
||||
3. If "set vs missing" matters: use `Result[int]`.
|
||||
4. If "zero is a valid value": use a frozen `@dataclass ColdStartTs: value: int = 0; set: bool = False; NIL_COLD_START_TS = ColdStartTs()`.
|
||||
5. If neither: use `Optional[int]` → `Result[int]` (the convention says `Optional[T]` for "might fail" is an anti-pattern).
|
||||
- **VERIFY:** Warmup tests pass.
|
||||
- **COMMIT:** `refactor(app_controller): migrate cold_start_ts from Optional[int] to Result[int] (per call-site shape)`
|
||||
- **GIT NOTE:** Shape chosen based on call-site semantics.
|
||||
|
||||
### Task 4.4: Phase 4 checkpoint commit
|
||||
- **COMMIT:** `conductor(plan): mark Phase 4 complete (4 INTERNAL_RETHROW classified, 1 INTERNAL_OPTIONAL_RETURN migrated)`
|
||||
- **GIT NOTE:** Phase 4 = 5 sites (4 rethrow + 1 optional) resolved; the audit's `INTERNAL_RETHROW` and `INTERNAL_OPTIONAL_RETURN` counts for `app_controller.py` are now 0.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Verify, document, end-of-track report
|
||||
|
||||
Focus: confirm all 45 migration-target sites are migrated; re-run batched suite; write the end-of-track report.
|
||||
|
||||
### Task 5.1: Re-run audit and confirm zero migration sites
|
||||
- **COMMAND:** `uv run python scripts/audit_exception_handling.py --by-size`
|
||||
- **EXPECT:** `src/app_controller.py (V=15, S=0, ?=0, C=4, total=19)` — the 15 BOUNDARY_FASTAPI + 2 BOUNDARY_SDK + 4 INTERNAL_COMPLIANT + 1 INTERNAL_PROGRAMMER_RAISE = 22 stay (the audit may bucket BOUNDARY_FASTAPI and BOUNDARY_SDK differently — verify the actual count structure).
|
||||
- **COMMIT:** No commit (verification only).
|
||||
|
||||
### Task 5.2: Run targeted tests
|
||||
- **COMMAND:** `uv run python -m pytest tests/test_app_controller_result.py tests/test_app_controller_offloading.py tests/test_tool_presets_execution.py tests/test_extended_sims.py tests/test_audit_exception_handling_heuristics.py -v`
|
||||
- **EXPECT:** All pass.
|
||||
- **COMMIT:** No commit (verification only).
|
||||
|
||||
### Task 5.3: Run the full batched suite
|
||||
- **COMMAND:** `uv run python scripts/run_tests_batched.py`
|
||||
- **EXPECT:** 882 passed / 17 skipped / 2 xfailed (same as before this track, except the 2 previously-failing tests now pass).
|
||||
- **COMMIT:** No commit (verification only).
|
||||
- **NOTE:** If new failures appear, fix forward or skip with documented reason (per the "Report-Instead-of-Fix" anti-pattern rule: do not commit a fix that has only been verified in isolation).
|
||||
|
||||
### Task 5.4: Add audit-heuristics tests for the 2 new app_controller categories
|
||||
- **WHERE:** `tests/test_audit_exception_handling_heuristics.py` (existing file)
|
||||
- **WHAT:** Add 2 tests:
|
||||
1. `test_app_controller_post_migration_has_zero_broad_catch` — runs the audit and asserts that the 32 INTERNAL_BROAD_CATCH sites are gone (or re-classified to COMPLIANT).
|
||||
2. `test_app_controller_post_migration_has_zero_silent_swallow` — same for the 8 INTERNAL_SILENT_SWALLOW sites.
|
||||
- **SAFETY:** The audit script may emit transient counts during the migration; these tests are run only at the end of Phase 5 (after all migrations land).
|
||||
- **COMMIT:** `test(audit): add post-migration assertions for app_controller categories`
|
||||
- **GIT NOTE:** Locks in the post-migration invariant.
|
||||
|
||||
### Task 5.5: Write the end-of-track report
|
||||
- **WHERE:** `docs/reports/TRACK_COMPLETION_result_migration_app_controller_20260618.md` (NEW)
|
||||
- **WHAT:** 7-section markdown report (per the 2026-06-17 convention):
|
||||
1. Header (track, branch, dates, scope, commit count)
|
||||
2. Tasks completed (per phase)
|
||||
3. Audit results (pre vs post)
|
||||
4. Last 3 failures (Regression 1 + Regression 2 details)
|
||||
5. Files modified (1 source + 2 tests + 4 metadata/plan/state)
|
||||
6. Git state (`git log` summary)
|
||||
7. Recommendation (next sub-track — sub-track 4 `gui_2`)
|
||||
- **COMMIT:** `docs(reports): TRACK_COMPLETION_result_migration_app_controller_20260618`
|
||||
- **GIT NOTE:** End-of-track report for the user to review.
|
||||
|
||||
### Task 5.6: Mark state.toml complete + update umbrella
|
||||
- **WHERE:** `conductor/tracks/result_migration_app_controller_20260618/state.toml`, `conductor/tracks/result_migration_20260616/spec.md` (line 256)
|
||||
- **WHAT:**
|
||||
1. `state.toml` — set `status = "completed"`, `current_phase = "complete"`.
|
||||
2. `spec.md` (umbrella) — update line 256 to reflect the actual count (45 migration + 22 stay = 67 total, NOT the estimated 22 + 34 = 56). Add a note that the audit's per-category output is the source of truth, not the T-shirt-size estimate.
|
||||
- **COMMIT:** `conductor(plan): mark result_migration_app_controller_20260618 as complete; update umbrella count`
|
||||
- **GIT NOTE:** Sub-track 3 complete; the umbrella's count is updated to reflect the actual scope.
|
||||
|
||||
---
|
||||
|
||||
## End-of-Track Report (added 2026-06-17 convention)
|
||||
|
||||
On Phase 5 completion, write `docs/reports/TRACK_COMPLETION_result_migration_app_controller_20260618.md` and update `conductor/tracks/result_migration_app_controller_20260618/state.toml` to `status = "completed"`.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Track Specification: Result Migration — Sub-Track 3 (App Controller)
|
||||
|
||||
**Track ID:** `result_migration_app_controller_20260618`
|
||||
**Date:** 2026-06-18
|
||||
**Priority:** A (resolves the 2 known tier-1-unit-core + tier-3-live_gui regressions; completes the app_controller arm of the umbrella `result_migration_20260616`)
|
||||
**Type:** refactor (data-oriented error handling convention; no behavior change visible to users)
|
||||
**Umbrella:** `result_migration_20260616` (sub-track 3 of 5)
|
||||
|
||||
## Overview
|
||||
|
||||
Migrate the 45 migration-target exception-handling sites in `src/app_controller.py` to the data-oriented error handling convention (Result[T] dataclasses). 22 sites stay as-is (15 FastAPI boundary handlers, 2 SDK-boundary catches in `do_post`, 4 already-compliant, 1 programmer-error raise). The migration fixes the 2 known regressions: `test_tool_presets_execution::test_tool_ask_approval` (TypeError from a half-migrated `session_logger.log_tool_call` call site) and the downstream `test_extended_sims::test_execution_sim_live` failure.
|
||||
|
||||
After this track, the audit's `INTERNAL_BROAD_CATCH` / `INTERNAL_SILENT_SWALLOW` / `INTERNAL_RETHROW` / `INTERNAL_OPTIONAL_RETURN` counts for `src/app_controller.py` drop to zero. The FastAPI and SDK boundary counts (15 + 2) stay at their current values (per the "Boundary Types" section in `conductor/code_styleguides/error_handling.md`).
|
||||
|
||||
## Current State Audit (as of 2026-06-18, commit 5107f3ca post-merge)
|
||||
|
||||
### App controller site breakdown (via `scripts/audit_exception_handling.py`)
|
||||
|
||||
```
|
||||
src\app_controller.py (V=41, S=4, ?=0, C=22, total=67)
|
||||
```
|
||||
|
||||
The umbrella spec at `conductor/tracks/result_migration_20260616/spec.md:256` estimated 56 sites (35 V + 3 S + 2 ? + 16 C). The actual count is 67 because the audit script improved since the umbrella was written:
|
||||
|
||||
- **Heuristic A** (added in Phase 11 of `result_migration_small_files_20260617`) re-classified 8 previously-UNCLEAR sites as `INTERNAL_SILENT_SWALLOW` (the original heuristics under-counted this category).
|
||||
- **Heuristic D** (Phase 12) re-classified 1 site as `INTERNAL_OPTIONAL_RETURN` (the new line was not anticipated in the umbrella).
|
||||
- The 2 UNCLEAR sites at `app_controller.py:1842` and `:1668` (from sub-track 1) are now both COMPLIANT — no migration needed.
|
||||
|
||||
### Migration scope (45 sites)
|
||||
|
||||
| Category | Count | Treatment |
|
||||
|---|---|---|
|
||||
| `INTERNAL_BROAD_CATCH` | 32 | Catch specific exception + return `Result[T]` (or nil-sentinel for void) per Pattern 3 ("Fail early") |
|
||||
| `INTERNAL_SILENT_SWALLOW` | 8 | Add `logging.debug(..., extra={"source": "ctx"})` per Heuristic #19; convert return to `Result[T]` |
|
||||
| `INTERNAL_RETHROW` | 4 | Classify as Pattern 1/2/3; if SUSPICIOUS, convert to `Result[T]` propagation |
|
||||
| `INTERNAL_OPTIONAL_RETURN` | 1 | Replace `Optional[T]` with `Result[T]` or nil-sentinel dataclass |
|
||||
| **Total migration** | **45** | |
|
||||
|
||||
### Migration-target site list (line numbers + ctx)
|
||||
|
||||
The 32 `INTERNAL_BROAD_CATCH` sites:
|
||||
|
||||
```
|
||||
L 537 _handle_custom_callback
|
||||
L 579 _handle_click
|
||||
L 1420 _update_inject_preview
|
||||
L 1480 _do_rag_sync
|
||||
L 1669 _process_pending_gui_tasks
|
||||
L 1947 replace_ref
|
||||
L 2046 cb_load_prior_log
|
||||
L 2068 cb_load_prior_log
|
||||
L 2081 cb_load_prior_log
|
||||
L 2129 run_manual_prune
|
||||
L 2140 _load_active_project
|
||||
L 2154 _load_active_project
|
||||
L 2195 run_prune
|
||||
L 2767 _do_project_switch
|
||||
L 2779 _do_project_switch
|
||||
L 2890 _refresh_from_project
|
||||
L 2944 _save_active_project
|
||||
L 3057 _run
|
||||
L 3084 do_fetch
|
||||
L 3094 do_fetch
|
||||
L 3434 worker
|
||||
L 3471 worker
|
||||
L 3542 worker
|
||||
L 3635 _handle_request_event
|
||||
L 3648 _handle_request_event
|
||||
L 4070 _bg_task
|
||||
L 4100 _bg_task
|
||||
L 4237 _start_track_logic
|
||||
L 4349 _cb_run_conductor_setup
|
||||
L 4446 _cb_load_track
|
||||
L 4475 _push_mma_state_update
|
||||
L 4504 _load_active_tickets
|
||||
```
|
||||
|
||||
The 8 `INTERNAL_SILENT_SWALLOW` sites:
|
||||
|
||||
```
|
||||
L 751 _on_sigint
|
||||
L 756 _install_sigint_exit_handler
|
||||
L 1294 mark_first_frame_rendered
|
||||
L 1376 _on_warmup_complete_for_timeline
|
||||
L 1566 mcp_config_json
|
||||
L 2389 queue_fallback
|
||||
L 4098 _bg_task
|
||||
L 4192 _start_track_logic
|
||||
```
|
||||
|
||||
The 4 `INTERNAL_RETHROW` sites:
|
||||
|
||||
```
|
||||
L 1225 __getattr__
|
||||
L 1251 __getattr__
|
||||
L 2983 load_context_preset
|
||||
L 2986 load_context_preset
|
||||
```
|
||||
|
||||
The 1 `INTERNAL_OPTIONAL_RETURN` site:
|
||||
|
||||
```
|
||||
L 1358 cold_start_ts
|
||||
```
|
||||
|
||||
### Sites that stay as-is (22)
|
||||
|
||||
| Category | Count | Lines | Why |
|
||||
|---|---|---|---|
|
||||
| `BOUNDARY_FASTAPI` | 15 | 96, 99, 213, 215, 239, 253, 285, 309, 312, 320, 341, 369, 380, 401, 402 | FastAPI exception handlers; per the "Boundary Types" section in `conductor/code_styleguides/error_handling.md`, HTTP-layer exceptions stay as exceptions because FastAPI's exception-handler middleware is the SDK boundary. |
|
||||
| `BOUNDARY_SDK` | 2 | 3291, 3313 (`do_post`) | SDK-boundary catches; per the same styleguide section, these are converted to `ErrorInfo` only if a Result return is feasible. `do_post` does not return Result (it's an internal helper), so the catch stays. |
|
||||
| `INTERNAL_COMPLIANT` | 4 | 1843, 2066, 2763, 3744 | Already compliant per the audit's heuristics. |
|
||||
| `INTERNAL_PROGRAMMER_RAISE` | 1 | 3124 | `raise ValueError` on a known-bad code path; per the styleguide, programmer errors stay as exceptions. |
|
||||
| **Total stay** | **22** | | |
|
||||
|
||||
### Known regressions this track fixes
|
||||
|
||||
The `INTERNAL_RETHROW` and `INTERNAL_SILENT_SWALLOW` migrations surface 2 test failures that block the batched suite:
|
||||
|
||||
**Regression 1: `tests/test_tool_presets_execution.py::test_tool_ask_approval` (tier-1-unit-core)**
|
||||
|
||||
```
|
||||
src/app_controller.py:3723: in _offload_entry_payload
|
||||
filename = Path(ref_path).name
|
||||
TypeError: expected str, bytes or os.PathLike object, not Result
|
||||
```
|
||||
|
||||
`session_logger.log_tool_call` (in `src/session_logger.py:205`) was partially migrated to return `Result[data=str(...)]` but the call site at `app_controller.py:3715, 3721` still does `Path(ref_path).name` expecting a string. The migration in this track updates the call site to unwrap the Result (per the convention's "AND over OR" pattern):
|
||||
|
||||
```python
|
||||
# Before (broken):
|
||||
ref_path = session_logger.log_tool_call(script, "LOG_ONLY", None)
|
||||
if ref_path:
|
||||
filename = Path(ref_path).name
|
||||
payload["script"] = f"[REF:{filename}]"
|
||||
|
||||
# After:
|
||||
ref_result = session_logger.log_tool_call(script, "LOG_ONLY", None)
|
||||
if ref_result.ok and ref_result.data:
|
||||
filename = Path(ref_result.data).name
|
||||
payload["script"] = f"[REF:{filename}]"
|
||||
elif ref_result.errors:
|
||||
logging.debug("offload failed: %s", ref_result.errors[0].ui_message())
|
||||
```
|
||||
|
||||
**Regression 2: `tests/test_extended_sims.py::test_execution_sim_live` (tier-3-live_gui)**
|
||||
|
||||
```
|
||||
[ABORT] Execution simulation aborted due to persistent GUI error: error
|
||||
```
|
||||
|
||||
This is a downstream effect of Regression 1: the live GUI runs the same `_offload_entry_payload` path during script execution; the offload crashes, the AI status flips to "error", the simulation aborts. Fixes itself once Regression 1 is fixed.
|
||||
|
||||
### Already Implemented (DO NOT re-implement)
|
||||
|
||||
- The data-oriented error handling convention: `src/result_types.py` defines `Result[T]`, `ErrorInfo`, `ErrorKind`, nil-sentences (`NIL_PATH`, `NIL_RAG_STATE`, `OK`).
|
||||
- The audit script: `scripts/audit_exception_handling.py` (the canonical migration site detector with 10 categories).
|
||||
- The 3 refactored baseline files (already migrated to Result[T]): `src/mcp_client.py`, `src/ai_client.py`, `src/rag_engine.py`.
|
||||
- Sub-track 2 (`result_migration_small_files_20260617`, shipped 2026-06-17 with Phase 13 complete) — the 16 small files (`outline_tool.py`, `summarize.py`, `shell_runner.py`, `log_registry.py`, `summary_cache.py`, `warmup.py`, `api_hooks.py`, `models.py`, `project_manager.py`, `orchestrator_pm.py`, `hot_reloader.py`, `file_cache.py`, `markdown_helper.py`, `theme_models.py`, `conductor_tech_lead.py`, `log_pruner.py`) were migrated. Their `__pycache__/` and `artifacts/` audit data is the reference for the migration patterns.
|
||||
- The 5-file-commit pattern from `doeh_test_thinking_cleanup_20260615`: 1 source + 1 test + 1 plan + 1 metadata + 1 state per task. Not 11 separate test mocks for 11 sites.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Zero migration-target sites in `src/app_controller.py` after this track.** Audit re-run shows `INTERNAL_BROAD_CATCH` + `INTERNAL_SILENT_SWALLOW` + `INTERNAL_RETHROW` + `INTERNAL_OPTIONAL_RETURN` all = 0 for `app_controller.py`.
|
||||
2. **22 stay-as-is sites stay as-is.** The boundary classification (15 FastAPI + 2 SDK + 4 compliant + 1 programmer-raise) is preserved.
|
||||
3. **The 2 known test regressions are fixed.** `test_tool_ask_approval` and `test_execution_sim_live` pass.
|
||||
4. **No new regressions.** The batched suite shows the same 882 passed / 17 skipped / 2 xfailed as before this track (the 1 currently-failing test_tool_ask_approval + 1 currently-failing test_execution_sim_live turn green, no new failures).
|
||||
5. **The migration uses the 5 conventions** from `conductor/code_styleguides/error_handling.md`: nil-sentinel dataclasses, zero-init, fail early, AND over OR, error-info side-channel.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
**FR1. Migrate 32 INTERNAL_BROAD_CATCH sites to `Result[T]` propagation.**
|
||||
|
||||
For each site:
|
||||
- Read the snippet + 2-3 lines of context (`get_file_slice`).
|
||||
- Replace `try: ... except Exception as e: pass # broad swallow` with:
|
||||
```python
|
||||
try:
|
||||
...
|
||||
except <SpecificException> as e:
|
||||
return Result(data=default, errors=[ErrorInfo(
|
||||
kind=ErrorKind.INTERNAL,
|
||||
message=str(e),
|
||||
source="<ctx>",
|
||||
original=e,
|
||||
)])
|
||||
```
|
||||
- The return type may change from `None` to `Result[None]` (use `OK` for the success case), or from a specific type to `Result[T]`.
|
||||
- Add `from src.result_types import Result, ErrorInfo, ErrorKind` at the top of `app_controller.py` if not already present.
|
||||
|
||||
**FR2. Migrate 8 INTERNAL_SILENT_SWALLOW sites with logging per Heuristic #19.**
|
||||
|
||||
For each site:
|
||||
- Add `logging.debug("swallowed exception: %s", e, extra={"source": "ctx"})` before the `pass` or `return None`.
|
||||
- Convert the return to `Result[T]` per FR1's pattern (the `errors=[ErrorInfo(...)]` side-channel carries the swallowed exception).
|
||||
|
||||
**FR3. Classify 4 INTERNAL_RETHROW sites.**
|
||||
|
||||
For each site, determine the pattern:
|
||||
- **Pattern 1** (catch + convert + raise as different type): legitimate. Stay as-is.
|
||||
- **Pattern 2** (catch + log + re-raise): legitimate. Add `logging.debug` for visibility, but the raise stays.
|
||||
- **Pattern 3** (catch + cleanup + re-raise): legitimate. Stay as-is.
|
||||
- **SUSPICIOUS** (catch + re-raise the same exception): migration-target. Convert to Result-based; remove the try/except.
|
||||
|
||||
The 4 sites (lines 1225, 1251, 2983, 2986) are in `__getattr__` and `load_context_preset`. Tier 2 reads each and classifies per the pattern. The Phase 1 plan task walks through this.
|
||||
|
||||
**FR4. Migrate 1 INTERNAL_OPTIONAL_RETURN site (L1358 `cold_start_ts`).**
|
||||
|
||||
Replace `Optional[int]` with a nil-sentinel dataclass or `Result[int]`:
|
||||
- If the return value is consumed by code that uses `if x is not None:`, use a frozen `@dataclass` (e.g., `class ColdStartTs: value: int = 0; set: bool = False; NIL_COLD_START_TS = ColdStartTs()`).
|
||||
- If the return value is consumed by code that needs to distinguish "missing" from "zero", use `Result[int]`.
|
||||
- Tier 2 picks the right shape based on the 1-2 call sites.
|
||||
|
||||
**FR5. Fix the half-migrated `session_logger.log_tool_call` call site (Regression 1).**
|
||||
|
||||
In `src/app_controller.py:_offload_entry_payload`:
|
||||
- Update the 2 `ref_path = session_logger.log_tool_output(...)` / `log_tool_call(...)` calls to unwrap the `Result`:
|
||||
```python
|
||||
ref_result = session_logger.log_tool_output(output)
|
||||
if ref_result.ok and ref_result.data:
|
||||
filename = Path(ref_result.data).name
|
||||
payload["output"] = f"[REF:{filename}]"
|
||||
elif ref_result.errors:
|
||||
logging.debug("offload failed: %s", ref_result.errors[0].ui_message())
|
||||
```
|
||||
- Do NOT change `src/session_logger.py` (the migration is at the call site per convention).
|
||||
|
||||
**FR6. Add tests for the new Result-based API (1 new test file + selective updates).**
|
||||
|
||||
Create `tests/test_app_controller_result.py` (modeled on `tests/test_ai_client_result.py`):
|
||||
- 5+ tests verifying Result return types and error side-channels for the migrated methods
|
||||
- 3+ tests verifying the `log_tool_call` / `log_tool_output` unwrapping in `_offload_entry_payload`
|
||||
- 1 test verifying Regression 2 (`test_execution_sim_live`) end-to-end behavior
|
||||
|
||||
Update `tests/test_app_controller_offloading.py`:
|
||||
- 1 test verifying the unwrapped path stores a `[REF:filename]` correctly when offload succeeds
|
||||
- 1 test verifying a debug log is emitted when offload fails
|
||||
|
||||
**FR7. Preserve the 22 stay-as-is sites.**
|
||||
|
||||
Do NOT touch any of the 22 sites listed above. The FastAPI handlers, SDK-boundary catches, compliant sites, and programmer-raise must remain exception-based. Add a comment at the top of each handler citing the styleguide section ("Per `conductor/code_styleguides/error_handling.md` §'Boundary Types'").
|
||||
|
||||
**FR8. Per-task atomic commits with the 5-file pattern.**
|
||||
|
||||
Each task touches 5 files (per `doeh_test_thinking_cleanup_20260615`):
|
||||
1. `src/app_controller.py` (the source change)
|
||||
2. `tests/test_app_controller_result.py` (new test) or `tests/test_app_controller_offloading.py` (update)
|
||||
3. `conductor/tracks/result_migration_app_controller_20260618/plan.md` (mark task `[x] <sha>`)
|
||||
4. `conductor/tracks/result_migration_app_controller_20260618/metadata.json` (update scope counters)
|
||||
5. `conductor/tracks/result_migration_app_controller_20260618/state.toml` (mark task `completed`)
|
||||
|
||||
Not 11 separate test mocks for 11 sites. One combined test for each Result-returning method (e.g., `_offload_entry_payload` returns Result, test the unwrap path).
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **No new dependencies.** `Result`, `ErrorInfo`, `ErrorKind` are in `src/result_types.py` (already imported by other modules).
|
||||
- **No changes to the public API.** The `_predefined_callbacks` and `_gettable_fields` Hook API registries stay identical (no callback signature changes; the internal Result types are hidden from the API surface).
|
||||
- **Thread safety preserved.** `app_controller.py` uses `threading.Lock` for several state dicts (`_pending_gui_tasks_lock`, `_api_event_queue_lock`, etc.). The migration does not change lock semantics.
|
||||
- **Hot reload compatibility.** Per the umbrella spec, the `src/app_controller.py` changes are exercised through the hot-reload mechanism (`Ctrl+Alt+R`). The user can verify each batch visually if desired.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
- **`conductor/code_styleguides/error_handling.md`** — the 5 patterns (Nil-Sentinel, Zero-Init, Fail Early, AND over OR, Error Info as Side-Channel), the data model (`Result[T]`, `ErrorInfo`, `ErrorKind`), the decision tree, and the "Boundary Types" section that determines which sites stay as exceptions.
|
||||
- **`conductor/tracks/result_migration_20260616/spec.md:254-274`** — the umbrella's sub-track 3 description. The current scope (45 migration + 22 stay) is BIGGER than the umbrella estimated (22 + 34) because the audit script improved.
|
||||
- **`conductor/tracks/result_migration_20260616/plan.md:101-200`** — sub-track 2's plan (the small-files migration that this sub-track parallels). The phase structure (Setup → Migrate → Test → Document → Verify) is the template.
|
||||
- **`conductor/tracks/result_migration_small_files_20260617/spec.md`** — the shipped sub-track 2. Look at the actual commits to see the 5-file pattern in action.
|
||||
- **`docs/guide_architecture.md`** — the threading model (background threads, `_pending_gui_tasks` queue, `_pending_tool_calls_lock`).
|
||||
- **`docs/guide_app_controller.md`** — the app_controller architecture (Hook API, MMA conductor, RAG integration).
|
||||
- **`docs/guide_testing.md`** — the test patterns (Result-based assertions, mock patterns, live_gui fixture).
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- The 3 refactored baseline files (`mcp_client.py`, `ai_client.py`, `rag_engine.py`) — already done.
|
||||
- The 16 small files (sub-track 2) — already done.
|
||||
- `src/gui_2.py` (260KB; 55 sites) — sub-track 4. **Not** part of this track.
|
||||
- The 5 baseline files' remaining 77 violations (sub-track 5) — not part of this track.
|
||||
- Migration of `session_logger.log_tool_call` to a fully Result-based signature — the half-migrated state is intentional; the convention is that call sites unwrap, not that every function returns Result. The migration at the call site in `_offload_entry_payload` (FR5) is the canonical fix.
|
||||
- The MMA conductor and RAG engine's Result propagation (the upstream of `app_controller`) — they're already Result-based; the work in this track is downstream consumption.
|
||||
- Tier 4 QA hooks — the QA callback in `app_controller:_on_comms_entry` is already Result-aware; no change needed.
|
||||
|
||||
## Test Inventory (after this track)
|
||||
|
||||
| Test file | Type | Status | Tests |
|
||||
|---|---|---|---|
|
||||
| `tests/test_app_controller_result.py` (NEW) | unit | default-on | 5+ Result return type tests |
|
||||
| `tests/test_app_controller_offloading.py` | unit | default-on | +2 unwrap path tests |
|
||||
| `tests/test_tool_presets_execution.py` | unit | default-on | `test_tool_ask_approval` (currently FAILING → fixed) |
|
||||
| `tests/test_extended_sims.py` | integration | default-on, opt-in `tier-3-live_gui` | `test_execution_sim_live` (currently FAILING → fixed) |
|
||||
| `tests/test_audit_exception_handling_heuristics.py` | unit | default-on | +2 new heuristics (INTERNAL_OPTIONAL_RETURN for app_controller; INTERNAL_RETHROW Pattern 3) |
|
||||
| `scripts/audit_exception_handling.py` | static analyzer | default-on | re-classified counts |
|
||||
|
||||
The post-track batched suite: same 882 passed / 17 skipped / 2 xfailed (the 1 currently-failing + 1 currently-failing both turn green; no new failures introduced).
|
||||
|
||||
## Verification Criteria
|
||||
|
||||
- `uv run python scripts/audit_exception_handling.py --by-size` shows `src/app_controller.py (V=0, S=0, ?=0, C=37, total=37)` after the track (the new total = 15 BOUNDARY_FASTAPI + 2 BOUNDARY_SDK + 4 INTERNAL_COMPLIANT + 1 INTERNAL_PROGRAMMER_RAISE = 22 stay + 15 stay = ... let me recompute: 22 stay + 0 migration = 22 total? no, the audit's `C` count includes both `INTERNAL_COMPLIANT` AND the `BOUNDARY_*` classes are NOT counted as violations; they show up as C.
|
||||
- Actually the audit's `compliant_sites` count includes only `INTERNAL_COMPLIANT` (4). The `BOUNDARY_FASTAPI` (15) and `BOUNDARY_SDK` (2) are in `violations`? Let me re-check the audit. If the post-track count is `V=15, S=0, ?=0, C=4, total=19` (just the FastAPI + SDK + INTERNAL_COMPLIANT + PROGRAMMER_RAISE = 19 + 2 SDK + 4 COMPLIANT + 1 PROGRAMMER_RAISE = 26), that's the target. Wait I need to verify the actual count structure.
|
||||
- The user's regression check (post-track): `uv run python scripts/run_tests_batched.py` shows 882 passed / 17 skipped / 2 xfailed (1 new from this track or maintained from before).
|
||||
- `tests/test_app_controller_result.py` exists and all 5+ tests pass.
|
||||
- `tests/test_app_controller_offloading.py` has the 2 new unwrap tests and all pass.
|
||||
- The `_offload_entry_payload` test path is exercised end-to-end (via `test_tool_ask_approval`).
|
||||
- The 22 stay-as-is sites are not modified (verified by `git diff src/app_controller.py | grep -E "L 96|L 99|L 213|..."` showing no changes at those line ranges; the line numbers may shift slightly as code is added/removed, so the verification is by `context` name not line number).
|
||||
|
||||
## Risk Register
|
||||
|
||||
- **R1:** The migration may break the 17 currently-skipped live_gui tests (the ones that require the GUI to be running). Mitigation: re-run live_gui suite at the end of Phase 5; if new failures appear, fix forward or skip with documented reason.
|
||||
- **R2:** The `INTERNAL_RETHROW` classification for `__getattr__` (L1225, L1251) is unusual — `__getattr__` should re-raise to support Python's attribute lookup protocol. Mitigation: the convention's "Fail early" pattern says programmer errors stay as exceptions; Tier 2 documents the rationale per site.
|
||||
- **R3:** The 1 `INTERNAL_OPTIONAL_RETURN` site (L1358 `cold_start_ts`) has multiple call sites. The shape (nil-sentinel vs Result) depends on how the call sites use the value. Tier 2 reads the call sites and picks the right shape.
|
||||
- **R4:** The `log_tool_call` call site in `_offload_entry_payload` (FR5) is the regression that's blocking the batched suite. It's also the FIRST thing Tier 2 should fix (in Phase 1 Task 1.x) to unblock the regression check.
|
||||
- **R5:** Scope is larger than the umbrella estimated (45 vs 22 migration). Mitigation: the umbrella spec is updated post-track to reflect the actual count; the audit's per-category output is the source of truth, not the umbrella's T-shirt-size estimate.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Track state for result_migration_app_controller_20260618
|
||||
# Updated by Tier 2 Tech Lead as tasks complete
|
||||
|
||||
[meta]
|
||||
track_id = "result_migration_app_controller_20260618"
|
||||
name = "Result Migration - Sub-Track 3 (App Controller)"
|
||||
status = "active"
|
||||
current_phase = 0
|
||||
last_updated = "2026-06-18"
|
||||
umbrella = "result_migration_20260616"
|
||||
sub_track_index = 3
|
||||
|
||||
[blocked_by]
|
||||
result_migration_small_files_20260617 = "shipped 2026-06-17"
|
||||
|
||||
[blocks]
|
||||
result_migration_gui_2_<YYYYMMDD> = "blocked by this track; will be planned after Phase 5 completion"
|
||||
|
||||
[phases]
|
||||
phase_1 = { status = "pending", checkpointsha = "", name = "Setup + Fix the regression (test_tool_ask_approval + test_execution_sim_live)" }
|
||||
phase_2 = { status = "pending", checkpointsha = "", name = "Migrate the 32 INTERNAL_BROAD_CATCH sites (4 bulk batches)" }
|
||||
phase_3 = { status = "pending", checkpointsha = "", name = "Migrate the 8 INTERNAL_SILENT_SWALLOW sites (with logging.debug per Heuristic #19)" }
|
||||
phase_4 = { status = "pending", checkpointsha = "", name = "Classify 4 INTERNAL_RETHROW + migrate 1 INTERNAL_OPTIONAL_RETURN" }
|
||||
phase_5 = { status = "pending", checkpointsha = "", name = "Verify, document, end-of-track report" }
|
||||
|
||||
[tasks]
|
||||
# Phase 1: Setup + Fix the regression
|
||||
t1_1 = { status = "pending", commit_sha = "", description = "Create sub-track folder (spec.md exists; plan.md, metadata.json, state.toml)" }
|
||||
t1_2 = { status = "pending", commit_sha = "", description = "Update conductor/tracks.md with the new sub-track row" }
|
||||
t1_3 = { status = "pending", commit_sha = "", description = "Fix _offload_entry_payload call site in src/app_controller.py:3709-3725 (unwrap Result from log_tool_output/log_tool_call)" }
|
||||
t1_4 = { status = "pending", commit_sha = "", description = "Add 2 unwrap-path tests in tests/test_app_controller_offloading.py" }
|
||||
t1_5 = { status = "pending", commit_sha = "", description = "Run targeted regression test (test_tool_ask_approval + test_execution_sim_live); verify both pass" }
|
||||
t1_6 = { status = "pending", commit_sha = "", description = "Phase 1 checkpoint commit" }
|
||||
|
||||
# Phase 2: Migrate 32 INTERNAL_BROAD_CATCH sites
|
||||
t2_1 = { status = "pending", commit_sha = "", description = "Create tests/test_app_controller_result.py with 5 scaffolding tests" }
|
||||
t2_2 = { status = "pending", commit_sha = "", description = "Migrate batch 1: 5 callback-handler sites (L537, L579, L2046, L2068, L2081)" }
|
||||
t2_3 = { status = "pending", commit_sha = "", description = "Migrate batch 2: 6 project-op sites (L2129, L2140, L2154, L2195, L2890, L2944)" }
|
||||
t2_4 = { status = "pending", commit_sha = "", description = "Migrate batch 3: 8 conductor/track sites (L3057, L3084, L3094, L4237, L4349, L4446, L4475, L4504)" }
|
||||
t2_5 = { status = "pending", commit_sha = "", description = "Migrate batch 4: 11 worker/task sites (L3434, L3471, L3542, L3635, L3648, L4070, L4100, L1669, L1420, L1480, L1947)" }
|
||||
t2_6 = { status = "pending", commit_sha = "", description = "Phase 2 checkpoint commit" }
|
||||
|
||||
# Phase 3: Migrate 8 INTERNAL_SILENT_SWALLOW sites
|
||||
t3_1 = { status = "pending", commit_sha = "", description = "Migrate batch 1: 4 SIGINT/timeline sites (L751, L756, L1294, L1376)" }
|
||||
t3_2 = { status = "pending", commit_sha = "", description = "Migrate batch 2: 4 MCP/worker sites (L1566, L2389, L4098, L4192)" }
|
||||
t3_3 = { status = "pending", commit_sha = "", description = "Phase 3 checkpoint commit" }
|
||||
|
||||
# Phase 4: Classify 4 INTERNAL_RETHROW + migrate 1 INTERNAL_OPTIONAL_RETURN
|
||||
t4_1 = { status = "pending", commit_sha = "", description = "Classify the 2 __getattr__ rethrow sites (L1225, L1251) per Pattern 1/2/3 or migrate" }
|
||||
t4_2 = { status = "pending", commit_sha = "", description = "Classify the 2 load_context_preset rethrow sites (L2983, L2986) per Pattern 1/2/3 or migrate" }
|
||||
t4_3 = { status = "pending", commit_sha = "", description = "Migrate cold_start_ts from Optional[int] to Result[int] or nil-sentinel (L1358)" }
|
||||
t4_4 = { status = "pending", commit_sha = "", description = "Phase 4 checkpoint commit" }
|
||||
|
||||
# Phase 5: Verify, document, end-of-track report
|
||||
t5_1 = { status = "pending", commit_sha = "", description = "Re-run audit_exception_handling.py; confirm 0 migration sites in src/app_controller.py" }
|
||||
t5_2 = { status = "pending", commit_sha = "", description = "Run targeted tests (test_app_controller_result, test_app_controller_offloading, test_tool_presets_execution, test_extended_sims, test_audit_exception_handling_heuristics)" }
|
||||
t5_3 = { status = "pending", commit_sha = "", description = "Run the full batched suite; confirm no new regressions" }
|
||||
t5_4 = { status = "pending", commit_sha = "", description = "Add 2 post-migration invariant tests in test_audit_exception_handling_heuristics.py" }
|
||||
t5_5 = { status = "pending", commit_sha = "", description = "Write docs/reports/TRACK_COMPLETION_result_migration_app_controller_20260618.md" }
|
||||
t5_6 = { status = "pending", commit_sha = "", description = "Mark state.toml complete; update umbrella spec count to reflect actual scope (45 migration + 22 stay = 67 total)" }
|
||||
|
||||
[verification]
|
||||
phase_1_complete = false
|
||||
phase_2_complete = false
|
||||
phase_3_complete = false
|
||||
phase_4_complete = false
|
||||
phase_5_complete = false
|
||||
regression_1_fixed = false
|
||||
regression_2_fixed = false
|
||||
batched_suite_no_new_regressions = false
|
||||
@@ -1,5 +0,0 @@
|
||||
# Track test_harness_hardening_20260310 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"track_id": "test_harness_hardening_20260310",
|
||||
"type": "chore",
|
||||
"status": "new",
|
||||
"created_at": "2026-03-10T00:15:00Z",
|
||||
"updated_at": "2026-03-10T00:15:00Z",
|
||||
"description": "Hardening the Hook API and test harness to resolve port conflicts and state serialization issues."
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# Implementation Plan: Hook API & Test Harness Hardening
|
||||
|
||||
## Phase 1: Dynamic Port Allocation
|
||||
- [ ] Task: Modify `src/api_hooks.py` to attempt binding to port `8999`. If unsuccessful, iterate up to `9010`.
|
||||
- [ ] Task: Upon successful binding, write the selected port to a temporary artifact file (e.g., `.mcp_port`).
|
||||
- [ ] Task: Update the `live_gui` fixture in `tests/conftest.py` to read the assigned port from the artifact file and use it for all `ApiHookClient` requests.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Dynamic Port Allocation' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: Graceful Teardown
|
||||
- [ ] Task: Implement a `/api/shutdown` POST endpoint in `src/api_hooks.py`.
|
||||
- [ ] Task: Ensure the `/api/shutdown` endpoint correctly signals the `AppController` and `App` to perform a clean exit (e.g., joining threads, writing pending saves).
|
||||
- [ ] Task: Update the `live_gui` fixture to call `/api/shutdown` during teardown, relying on `taskkill` only as a last resort if the process fails to exit gracefully within a timeout.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Graceful Teardown' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: State Serialization Audit
|
||||
- [ ] Task: Audit `src/app_controller.py` to identify all GUI-related state variables that should be exposed to tests (e.g., `show_windows`, individual window toggles).
|
||||
- [ ] Task: Update `_gettable_fields` to ensure all necessary state is mapped.
|
||||
- [ ] Task: Refine `_serialize_for_api` in `src/api_hooks.py` (or introduce lightweight dataclass parsing) to handle complex nested objects predictably.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: State Serialization Audit' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: Verification
|
||||
- [ ] Task: Write Tests: Create a concurrency test verifying multiple `live_gui` instances can launch simultaneously without port conflicts.
|
||||
- [ ] Task: Run the entire test suite to ensure the new dynamic port and graceful shutdown logic has not introduced regressions.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Verification' (Protocol in workflow.md)
|
||||
@@ -1,32 +0,0 @@
|
||||
# Specification: Hook API & Test Harness Hardening
|
||||
|
||||
## Overview
|
||||
This track focuses on stabilizing the local development and testing environment by hardening the Hook API and its associated `live_gui` test harness. The goal is to eliminate port conflicts, ensure graceful teardowns, and standardize state serialization, laying the groundwork for a future WebSockets implementation.
|
||||
|
||||
## Functional Requirements
|
||||
- **Dynamic Port Allocation:**
|
||||
- The Hook Server (`src/api_hooks.py`) will implement a Sequential Fallback strategy for port binding.
|
||||
- It will attempt to bind to port `8999`. If `Address already in use` is encountered, it will sequentially try ports up to `9010` until successful.
|
||||
- The successfully bound port will be written to a temporary artifact file (e.g., `.mcp_port` or similar) so the test harness can discover it.
|
||||
- **Graceful Teardown:**
|
||||
- Implement a new `/api/shutdown` POST endpoint in the Hook API.
|
||||
- When called, this endpoint should safely signal the `AppController` and `App` to initiate a clean shutdown (joining threads, closing files) rather than relying on the OS to forcefully kill the process tree.
|
||||
- Update the `live_gui` fixture to call this endpoint during the `finally` block before resorting to `taskkill` as a last-resort fallback.
|
||||
- **State Serialization Audit:**
|
||||
- Formalize the structure returned by `/api/gui/state`.
|
||||
- Ensure all fields defined in `_gettable_fields` are safely serialized using standard dataclass/dict traversal (avoiding deep Pydantic dependencies for now, keeping it lightweight but strictly typed for future WebSocket pipeline compatibility).
|
||||
- Add missing fields discovered during recent UI tracks (e.g., `show_windows` states).
|
||||
|
||||
## Non-Functional Requirements
|
||||
- **Stability:** The `live_gui` fixture must pass 100% of the time without port conflict errors when tests are run sequentially.
|
||||
- **Performance:** The port scanning logic should fail fast and bind within milliseconds.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] A test script can successfully launch two concurrent `live_gui` instances, and they bind to different ports.
|
||||
- [ ] Calling `/api/shutdown` successfully stops the GUI process and returns a 200 OK.
|
||||
- [ ] The `live_gui` fixture uses the shutdown endpoint and the test suite passes without leaving orphaned processes.
|
||||
- [ ] The state returned by `/api/gui/state` is fully documented and includes all relevant nested objects (like `show_windows`).
|
||||
|
||||
## Out of Scope
|
||||
- Implementing the WebSockets streaming logic (this track only prepares the state schema for it).
|
||||
- Fixing failing simulation tests that are unrelated to the harness itself.
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"id": "tier2_autonomous_sandbox_20260616",
|
||||
"title": "Tier 2 Autonomous Sandbox (unattended track execution with bounded blast radius)",
|
||||
"type": "feature",
|
||||
"status": "shipped",
|
||||
"priority": "high",
|
||||
"created": "2026-06-16",
|
||||
"shipped": "2026-06-16",
|
||||
"owner": "tier2-tech-lead",
|
||||
"spec": "conductor/tracks/tier2_autonomous_sandbox_20260616/spec.md",
|
||||
"plan": "conductor/tracks/tier2_autonomous_sandbox_20260616/plan.md",
|
||||
"scope": {
|
||||
"new_files": 22,
|
||||
"modified_files": 1,
|
||||
"deleted_files": 0
|
||||
},
|
||||
"depends_on": [],
|
||||
"blocks": [],
|
||||
"test_summary": {
|
||||
"default_on_tests": 31,
|
||||
"opt_in_tests_sandbox": 4,
|
||||
"opt_in_tests_smoke": 1
|
||||
},
|
||||
"verification_criteria": [
|
||||
"All failcount unit tests pass (19 tests, 100% coverage on scripts/tier2/failcount.py)",
|
||||
"Slash command spec test passes (12 contract assertions)",
|
||||
"Report writer tests pass (8 opt-in tests, 100% coverage on scripts/tier2/write_report.py)",
|
||||
"Bootstrap -WhatIf runs without error",
|
||||
"Pre-push hook refuses a push attempt (sandbox enforcement test)",
|
||||
"Smoke e2e creates a feature branch via git switch -c",
|
||||
"User guide covers bootstrap, invocation, manual verification checklist",
|
||||
"Default uv run pytest stays app-focused (opt-in tests skip without env vars)"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,612 +0,0 @@
|
||||
# Track Specification: Tier 2 Autonomous Sandbox (unattended track execution with bounded blast radius)
|
||||
|
||||
**Track ID:** `tier2_autonomous_sandbox_20260616`
|
||||
**Status:** Planned (spec pending user review)
|
||||
**Priority:** A (user-blocking; eliminates the manual `permission: ask` bottleneck for well-regularized tracks)
|
||||
**Owner:** Tier 2 Tech Lead (per `conductor/workflow.md`)
|
||||
**Type:** feature (meta-tooling — adds a new execution mode to the existing MMA workflow, not to the Manual Slop app itself)
|
||||
**Scope:** ~7 new files in main repo + 1 sibling clone at `C:\projects\manual_slop_tier2\` (one-time bootstrap)
|
||||
**Parent tracks:** `opencode_config_overhaul_20260310` (shipped; established the agent profile scaffolding this track extends)
|
||||
**Sibling tracks:** none (independent)
|
||||
|
||||
> **Note on effort estimates:** this spec measures effort by **scope**
|
||||
> only (N files, M sites, N tests). The user / Tier 2 agent decides
|
||||
> the actual pacing.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
This track adds an **unattended execution mode** for Tier 2: you open
|
||||
OpenCode in a sibling clone (`C:\projects\manual_slop_tier2\`), type
|
||||
`/tier-2-auto-execute <track-name>`, and Tier 2 runs the track
|
||||
autonomously — **no `permission: ask` prompts** — while a **3-layer
|
||||
defense-in-depth** enforcement stack prevents it from touching the
|
||||
filesystem outside its clone + an app-data temp dir, and from running
|
||||
destructive git operations (`git restore`, `git push*`, `git checkout`,
|
||||
`git reset`). If Tier 2 can't make progress (3 red-phase failures, 3
|
||||
green-phase failures, or 30 minutes with no commit/green), it stops
|
||||
early, writes a failure report, and notifies you. You review the
|
||||
feature branch with Tier 1 in the main repo, then merge.
|
||||
|
||||
**Scope:** 7 new files in main repo (mostly config + scripts + 1 small
|
||||
Python module), 4 new test files, 1 PowerShell wrapper, 1 bootstrap
|
||||
script, 1 user guide. ~600 lines of new code.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
### 1.1 The State Before This Track (as of `88e44d1c`)
|
||||
|
||||
The current OpenCode configuration has these properties:
|
||||
|
||||
- **One repo, two modes via agent profile.** `opencode.json:11` sets
|
||||
`default_agent: "tier2-tech-lead"`. Tier 1 and Tier 2 are
|
||||
distinguished by which agent profile the user selects in the OpenCode
|
||||
session, not by which directory they're in.
|
||||
- **Permission bottleneck on Tier 2.** `.opencode/agents/tier2-tech-lead.md:6-9`
|
||||
sets `permission: { edit: "ask", bash: "ask", 'manual-slop_*': allow }`.
|
||||
Every `edit` and every `bash` call from Tier 2 prompts the user for
|
||||
approval. For well-regularized tracks (TDD red/green/refactor with
|
||||
atomic per-task commits, e.g., the upcoming `result_migration_*`
|
||||
tracks), this is **noise** — the user has already pre-approved the
|
||||
track plan, and the per-task approval doesn't add safety, it just
|
||||
adds 50+ clicks per track.
|
||||
- **No filesystem boundary enforcement.** Tier 2 has the same
|
||||
filesystem access as the user. There is nothing preventing Tier 2 (or
|
||||
a delegated Tier 3 worker) from reading `C:\Users\Ed\.aws\credentials`
|
||||
or writing to a different project entirely.
|
||||
- **No git ban enforcement.** Nothing prevents Tier 2 from running
|
||||
`git restore`, `git push origin`, `git checkout -- <file>`, or
|
||||
`git reset --hard`. These are the four operations the user has
|
||||
called out as "destructive to its progress or affects the origin
|
||||
server" in the original ask.
|
||||
- **No failure threshold / give-up mechanism.** A stuck Tier 2 runs
|
||||
until the user notices or the agent self-terminates. There is no
|
||||
"3 red-phase attempts without progress → stop and write a report"
|
||||
guardrail.
|
||||
- **One OpenCode session at a time.** The main repo's OpenCode session
|
||||
is the only execution environment. Tier 2 cannot run in parallel with
|
||||
Tier 1 review.
|
||||
|
||||
### 1.2 The Goal
|
||||
|
||||
Add a **second execution mode** for Tier 2 that is:
|
||||
|
||||
- **Autonomous** — no `permission: ask` prompts for `edit` or `bash`
|
||||
- **Sandboxed** — file access is restricted to the Tier 2 clone + an
|
||||
app-data temp dir, enforced at 3 independent layers (OpenCode
|
||||
permission system, Windows restricted token + ACLs, git hooks)
|
||||
- **Bounded** — a one-shot run with a failure threshold; stuck runs
|
||||
stop early and write a report
|
||||
- **Reviewable** — the run produces a feature branch in the clone;
|
||||
the user fetches it back to main and reviews with Tier 1
|
||||
- **Opt-in to the app's test suite** — the sandbox / bootstrap / smoke
|
||||
tests are env-var-gated so the default `uv run pytest` run stays
|
||||
app-focused and fast
|
||||
|
||||
The main repo (the Tier 1 control plane) is **not modified** —
|
||||
`opencode.json` stays the same (Tier 1 still has `permission: ask`),
|
||||
and the existing MMA agents stay the same.
|
||||
|
||||
### 1.3 What the User Experiences
|
||||
|
||||
**One-time bootstrap (the user runs once):**
|
||||
```powershell
|
||||
cd C:\projects\manual_slop
|
||||
pwsh scripts/tier2/setup_tier2_clone.ps1
|
||||
```
|
||||
|
||||
**Per-track invocation (the user's normal flow from now on):**
|
||||
1. `cd C:\projects\manual_slop_tier2`
|
||||
2. Open OpenCode in that directory (the "Tier 2 Sandboxed" desktop
|
||||
shortcut the bootstrap created)
|
||||
3. In the OpenCode session, type:
|
||||
```
|
||||
/tier-2-auto-execute result_migration_review_pass
|
||||
```
|
||||
4. Tier 2 fetches the spec, creates `tier2/result_migration_review_pass`
|
||||
branch, runs the plan, commits per task
|
||||
5. On success: prints a summary. On give-up: writes a failure report
|
||||
and prints its path.
|
||||
6. `cd C:\projects\manual_slop` (back to main)
|
||||
7. `git fetch C:/projects/manual_slop_tier2 tier2/result_migration_review_pass`
|
||||
8. Review the diff with Tier 1 (interactive)
|
||||
9. `git merge --no-ff tier2/result_migration_review_pass` to main
|
||||
|
||||
**No `permission: ask` prompts in step 4.** If a Tier 2 tool call
|
||||
attempts a banned operation, the OpenCode permission system denies it;
|
||||
if a delegated Tier 3 worker tries to escape via a Python subprocess,
|
||||
the Windows ACLs deny it; if a `git push` somehow slips through, the
|
||||
pre-push hook blocks it. **Three independent layers, all enforcing the
|
||||
same ban list.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Current State Audit (as of `88e44d1c`)
|
||||
|
||||
### 2.1 Already Implemented (DO NOT re-implement)
|
||||
|
||||
- **OpenCode agent profile scaffolding** —
|
||||
`.opencode/agents/tier{1,2,3,4}-*.md:1-200` and the
|
||||
`opencode.json:1-50` config file. The `tier2-autonomous` agent
|
||||
profile this track adds follows the same pattern.
|
||||
- **Slash command pattern** — `.opencode/commands/conductor-implement.md:1-100`
|
||||
is the existing pattern for slash commands. The
|
||||
`tier-2-auto-execute.md` command follows the same structure (front
|
||||
matter `agent:` and `description:`, markdown body with protocol).
|
||||
- **Conductor track convention** — `conductor/tracks/<id>/{spec,plan}.md`
|
||||
and `metadata.json` per `conductor/workflow.md` "State.toml
|
||||
Template" + "Track Dependencies and Execution Order" sections. This
|
||||
track's artifacts follow that pattern.
|
||||
- **Project-level test opt-in convention** — the `live_gui` fixture
|
||||
in `tests/conftest.py` and the existing env-var-gated tests (e.g.,
|
||||
the `RUN_LIVE_GUI=1` pattern in `tests/test_live_*.py`). The
|
||||
`TIER2_SANDBOX_TESTS=1` opt-in gate for this track's sandbox tests
|
||||
follows the same shape.
|
||||
- **PowerShell-based tooling** — `scripts/` already contains
|
||||
PowerShell-adjacent Python scripts. The new wrapper is a pure
|
||||
PowerShell script, consistent with `pywin32`-based operations on
|
||||
Windows.
|
||||
- **`scripts/audit_*.py` pattern** — the 4 existing audit scripts
|
||||
(`audit_exception_handling.py`, `audit_weak_types.py`,
|
||||
`audit_main_thread_imports.py`, `audit_no_models_config_io.py`) are
|
||||
the project's enforcement mechanism. This track does not introduce
|
||||
a new audit (the failcount thresholds are TOML-config, not
|
||||
statically checkable), but follows the `scripts/audit_<name>.py`
|
||||
naming for any future addition.
|
||||
|
||||
### 2.2 Gaps to Fill (This Track's Scope)
|
||||
|
||||
**Gap 1: A second clone as the Tier 2 execution environment.**
|
||||
|
||||
The main repo (`C:\projects\manual_slop\`) currently doubles as both
|
||||
the Tier 1 control plane and the Tier 2 execution environment. The
|
||||
fix is a sibling clone at `C:\projects\manual_slop_tier2\` with
|
||||
`origin` set to the main repo's local path (no remote). The clone is
|
||||
where the feature branch lives; the user fetches the branch back into
|
||||
main for review.
|
||||
|
||||
**Gap 2: A `tier2-autonomous` agent profile with deny rules.**
|
||||
|
||||
The existing `tier2-tech-lead` agent has `permission: ask` for `edit`
|
||||
and `bash`. The fix is a new `tier2-autonomous` agent profile (in the
|
||||
Tier 2 clone's `opencode.json`) with:
|
||||
- `permission.edit: allow`
|
||||
- `permission.bash: { "*": "allow", "git push*": "deny",
|
||||
"git checkout*": "deny", "git restore*": "deny", "git reset*": "deny" }`
|
||||
- `permission.read` / `permission.write` restricted to the Tier 2
|
||||
clone + `C:\Users\Ed\AppData\Local\manual_slop\tier2\`
|
||||
|
||||
**Gap 3: A sandboxed launcher (Windows restricted token + ACLs).**
|
||||
|
||||
OpenCode's permission system is process-level. A determined Tier 3
|
||||
worker calling `os.system("...")` from a delegated Python script
|
||||
could in principle bypass OpenCode. The fix is a PowerShell wrapper
|
||||
that:
|
||||
- Acquires a Windows restricted token (drops `SeBackupPrivilege`,
|
||||
`SeRestorePrivilege`, `SeTakeOwnershipPrivilege`, `SeDebugPrivilege`,
|
||||
`SeLoadDriverPrivilege`)
|
||||
- Sets explicit ACLs on the Tier 2 clone + app-data temp dir (allow
|
||||
the restricted token, deny everything else)
|
||||
- Wraps the process tree in a Job Object (no breakaway)
|
||||
- Launches OpenCode + the MCP server under the restricted token via
|
||||
`CreateProcessWithTokenW`
|
||||
|
||||
**Gap 4: A `tier-2-auto-execute` slash command.**
|
||||
|
||||
The existing slash commands are conductor-style ("start
|
||||
implementation", "create track"). The new slash command takes a
|
||||
`<track-name>` argument, fetches the spec from `origin/main`, creates
|
||||
a `tier2/<track-name>` branch via `git switch -c` (NOT `git checkout`),
|
||||
runs the plan via Tier 2, monitors the failcount, and reports back.
|
||||
|
||||
**Gap 5: A failure threshold + give-up mechanism (`failcount.py`).**
|
||||
|
||||
The current Tier 2 has no built-in "I can't make progress" detection.
|
||||
A stuck agent burns tokens until the user notices. The fix is a pure
|
||||
Python module that tracks three orthogonal signals:
|
||||
- `red_phase_failures` (3 = give up)
|
||||
- `green_phase_failures` (3 = give up)
|
||||
- `no_progress_minutes` (30 = give up)
|
||||
|
||||
Whichever signal hits its threshold first triggers give-up. The
|
||||
module is pure logic, fully unit-testable, with a TOML config for
|
||||
threshold overrides.
|
||||
|
||||
**Gap 6: A failure report writer + flag file + notification.**
|
||||
|
||||
When give-up fires, the system needs to:
|
||||
- Write a markdown report to
|
||||
`C:\Users\Ed\AppData\Local\manual_slop\tier2_failures\<track>_<utc-timestamp>.md`
|
||||
with: header, tasks completed, current task state, last 3 failures,
|
||||
failcount state, git log, recommendation
|
||||
- Create a `.STOPPED` flag file alongside the report
|
||||
- Print a clear "TRACK ABORTED" banner in the OpenCode session with
|
||||
the report path
|
||||
- Optionally: Windows toast notification (opt-in via `--toast` flag)
|
||||
|
||||
**Gap 7: Git hooks as defense-in-depth (Layer 3).**
|
||||
|
||||
The OpenCode permission system is the primary enforcement for git bans.
|
||||
A pre-push hook (`pre-push` in the clone's `.git/hooks/`) is the
|
||||
backup that catches `git push origin*` even if the OpenCode deny rule
|
||||
is somehow misconfigured. A `post-checkout` hook logs any checkout of
|
||||
tracked files to a detection log.
|
||||
|
||||
**Gap 8: A user guide for bootstrap + invocation + manual verification.**
|
||||
|
||||
The user needs to know:
|
||||
- How to run the bootstrap once
|
||||
- How to invoke the slash command
|
||||
- What the failure report looks like
|
||||
- How to review and merge the feature branch
|
||||
- How to manually verify the sandbox blocks the banned operations
|
||||
|
||||
---
|
||||
|
||||
## 3. Goals
|
||||
|
||||
- **Eliminate the `permission: ask` bottleneck** for well-regularized
|
||||
tracks. The user clicks zero times during a normal Tier 2 run
|
||||
(excluding the "did Tier 2 give up?" check at the end).
|
||||
- **Enforce the 4 hard git bans** (`git restore`, `git push*`,
|
||||
`git checkout`, `git reset`) at 3 independent layers (OpenCode,
|
||||
Windows OS, git hooks). A bypass of one layer is caught by another.
|
||||
- **Enforce the filesystem boundary** (Tier 2 clone + app-data temp
|
||||
only) at 2 independent layers (OpenCode path allowlist, Windows
|
||||
ACLs). Even a delegated Python subprocess can't read outside the
|
||||
allowlist.
|
||||
- **Bound the blast radius** with a failure threshold. A stuck Tier 2
|
||||
stops within ~30 minutes and writes a report, instead of running
|
||||
indefinitely.
|
||||
- **Keep the default test run app-focused.** All sandbox/bootstrap/
|
||||
smoke tests are env-var-gated; `uv run pytest` with no env vars
|
||||
stays fast and never touches the Windows ACL subsystem.
|
||||
- **Keep Tier 1 unchanged.** The main repo's `opencode.json` is not
|
||||
modified. Tier 1 retains its `permission: ask` workflow.
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### 4.1 Bootstrap (one-time, user-driven)
|
||||
|
||||
**FR1.1:** `scripts/tier2/setup_tier2_clone.ps1` (new) clones the
|
||||
main repo to `C:\projects\manual_slop_tier2\`, sets
|
||||
`origin = C:\projects\manual_slop`, copies the agent/command/
|
||||
opencode.json templates to the clone, installs the git hooks into
|
||||
the clone's `.git/hooks/`, creates the app-data temp dir
|
||||
`C:\Users\Ed\AppData\Local\manual_slop\tier2\` with restricted ACLs,
|
||||
and creates a "Tier 2 (Sandboxed)" desktop shortcut.
|
||||
|
||||
**FR1.2:** The bootstrap is idempotent — re-running it does not
|
||||
destroy an existing clone's feature branches (it `git fetch origin`
|
||||
and pulls the latest templates, but does not `git reset` the clone).
|
||||
|
||||
**FR1.3:** The bootstrap dry-run mode (`-WhatIf`) shows what would
|
||||
happen without making changes. Required for safety.
|
||||
|
||||
### 4.2 The tier2-autonomous agent profile
|
||||
|
||||
**FR2.1:** `.opencode/agents/tier2-autonomous.md` (template) in main
|
||||
repo; copied to Tier 2 clone during bootstrap. Defines the
|
||||
autonomous-mode agent with the deny rules in §2.2 Gap 2.
|
||||
|
||||
**FR2.2:** The agent's `temperature: 0.4` (matches Tier 2 Tech Lead).
|
||||
The agent uses `git switch -c <branch>` for new branches and
|
||||
`git switch <branch>` for switching — `git checkout` is banned
|
||||
project-wide.
|
||||
|
||||
**FR2.3:** The agent prompt includes the failcount monitoring
|
||||
contract: "After each task commit, check
|
||||
`<app-data>/tier2/<track>/state.json` via the failcount module. If
|
||||
`should_give_up` returns true, write the failure report and stop."
|
||||
|
||||
### 4.3 The sandboxed launcher
|
||||
|
||||
**FR3.1:** `scripts/tier2/run_tier2_sandboxed.ps1` (new) is the
|
||||
entry point that opens OpenCode in the Tier 2 clone under a
|
||||
restricted token.
|
||||
|
||||
**FR3.2:** The wrapper acquires a restricted token via .NET
|
||||
(`CreateRestrictedToken`), sets ACLs on the Tier 2 clone + app-data
|
||||
dir to grant the restricted token read/write, wraps the process
|
||||
tree in a Job Object, and launches OpenCode + the MCP server under
|
||||
the restricted token via `CreateProcessWithTokenW`.
|
||||
|
||||
**FR3.3:** The wrapper is the target of the "Tier 2 (Sandboxed)"
|
||||
desktop shortcut created during bootstrap. Right-click → Properties
|
||||
shows the command: `pwsh -File C:\projects\manual_slop\scripts\tier2\run_tier2_sandboxed.ps1`.
|
||||
|
||||
### 4.4 The slash command
|
||||
|
||||
**FR4.1:** `.opencode/commands/tier-2-auto-execute.md` (template) in
|
||||
main repo; copied to Tier 2 clone during bootstrap. Takes a
|
||||
required `<track-name>` argument.
|
||||
|
||||
**FR4.2:** The slash command:
|
||||
1. Reads `conductor/tracks/<track-name>/spec.md` + `plan.md` from
|
||||
the current branch (after a `git fetch origin main`)
|
||||
2. Creates a `tier2/<track-name>` branch via
|
||||
`git switch -c tier2/<track-name> origin/main`
|
||||
3. Initializes the failcount state file at
|
||||
`<app-data>/tier2/<track-name>/state.json`
|
||||
4. Delegates the plan to the tier2-autonomous agent
|
||||
5. After each task commit, checks failcount; on give-up, writes the
|
||||
report and stops
|
||||
6. On success, prints a summary (branch name, N commits, M tasks)
|
||||
|
||||
**FR4.3:** The slash command's protocol is duplicated in a CLI
|
||||
entry point (`scripts/tier2/run_track.py`) so the smoke e2e test
|
||||
can invoke the same logic without spinning up an OpenCode session.
|
||||
|
||||
**FR4.4:** The slash command supports `--resume` to continue a
|
||||
previously-give-up track from the last completed task (state is in
|
||||
the state.json file). Default behavior: refuse to resume, ask for
|
||||
explicit confirmation.
|
||||
|
||||
### 4.5 The failcount module
|
||||
|
||||
**FR5.1:** `scripts/tier2/failcount.py` (new) is a pure-Python module
|
||||
with no external deps. Exposes:
|
||||
- `class FailcountState` — the signal state dataclass
|
||||
- `class FailcountConfig` — threshold loader (from TOML or defaults)
|
||||
- `def should_give_up(state: FailcountState, config: FailcountConfig,
|
||||
now: datetime) -> Result[bool, ErrorInfo]`
|
||||
- `def record_red_failure(state: FailcountState) -> FailcountState`
|
||||
- `def record_green_failure(state: FailcountState) -> FailcountState`
|
||||
- `def record_green_success(state: FailcountState,
|
||||
now: datetime) -> FailcountState` (resets no_progress)
|
||||
- `def record_commit(state: FailcountState,
|
||||
now: datetime) -> FailcountState` (resets no_progress)
|
||||
- `def to_dict(state) -> dict`, `def from_dict(d) -> FailcountState`
|
||||
- `def load_state(track_name: str) -> Result[FailcountState, ErrorInfo]`
|
||||
- `def save_state(track_name: str, state: FailcountState) -> Result[None, ErrorInfo]`
|
||||
|
||||
**FR5.2:** Default thresholds (override via `failcount.toml`):
|
||||
- `red_phase_threshold: 3`
|
||||
- `green_phase_threshold: 3`
|
||||
- `no_progress_minutes: 30`
|
||||
|
||||
**FR5.3:** `should_give_up` returns `True` if ANY signal hits its
|
||||
threshold. The `now` parameter is injectable for testing.
|
||||
|
||||
**FR5.4:** `record_green_success` and `record_commit` reset the
|
||||
`no_progress_minutes` timer. They do NOT reset the red/green
|
||||
failure counters (those only reset on the next progress signal of
|
||||
the same type — e.g., a red failure is reset by a green test that
|
||||
eventually passes).
|
||||
|
||||
### 4.6 The failure report writer
|
||||
|
||||
**FR6.1:** `scripts/tier2/write_report.py` (new) takes a track name,
|
||||
branch name, state, and a list of `TaskResult` records, and writes
|
||||
the markdown report to
|
||||
`C:\Users\Ed\AppData\Local\manual_slop\tier2_failures\<track>_<utc-timestamp>.md`.
|
||||
|
||||
**FR6.2:** The report contains the 7 sections in order:
|
||||
1. Header (track, branch, started-at, stopped-at, duration, give-up signal)
|
||||
2. Tasks completed (list with task IDs, commit SHAs, summaries)
|
||||
3. Current task state (where it stopped: task ID, phase, worker output, test failure)
|
||||
4. Last 3 failures (truncated to 50 lines, full output in `..._full.log`)
|
||||
5. Failcount state at give-up
|
||||
6. Git state (`git log --oneline tier2/<track> ^origin/main`)
|
||||
7. Recommendation (heuristic-based: "track too complex", "spec needs clearer plan", "external dependency missing", "review carefully")
|
||||
|
||||
**FR6.3:** A `.STOPPED` flag file is created at
|
||||
`C:\Users\Ed\AppData\Local\manual_slop\tier2_failures\<track>.STOPPED`.
|
||||
|
||||
**FR6.4:** The report writer returns the report path on success
|
||||
(via `Result[str, ErrorInfo]`).
|
||||
|
||||
### 4.7 The git hooks (Layer 3)
|
||||
|
||||
**FR7.1:** `conductor/tier2/githooks/pre-push` (template) is a
|
||||
shell/PowerShell script that refuses `git push` invocations to any
|
||||
remote. The script returns exit code 1 with the message
|
||||
"Tier 2 autonomous mode: `git push` is disabled. Push the branch
|
||||
manually from the main repo after review."
|
||||
|
||||
**FR7.2:** `conductor/tier2/githooks/post-checkout` (template) is a
|
||||
detection-only hook that logs any checkout of tracked files to
|
||||
`C:\Users\Ed\AppData\Local\manual_slop\tier2\tier2_checkout_log.txt`
|
||||
with a timestamp, the commit hash, and the affected paths.
|
||||
|
||||
**FR7.3:** The bootstrap script copies both hooks to the Tier 2
|
||||
clone's `.git/hooks/` and `chmod +x` (on Linux/WSL) or sets the
|
||||
executable bit via `icacls` (on Windows).
|
||||
|
||||
### 4.8 The user guide
|
||||
|
||||
**FR8.1:** `docs/guide_tier2_autonomous.md` (new) covers:
|
||||
- Why this exists (the `permission: ask` bottleneck)
|
||||
- One-time bootstrap procedure (with `-WhatIf` instructions)
|
||||
- Per-track invocation procedure
|
||||
- The slash command arguments (`<track-name>`, `--resume`, `--toast`)
|
||||
- The failure report layout (with screenshot/example)
|
||||
- How to review and merge the feature branch
|
||||
- The "Verify the sandbox" checklist (manual verification)
|
||||
- Troubleshooting (common errors: origin not set, hooks not
|
||||
executable, failcount.toml missing)
|
||||
|
||||
**FR8.2:** The guide includes a "Verify the sandbox" section that
|
||||
walks the user through attempting each banned operation manually
|
||||
and confirming the denial. This is the user-driven checklist from
|
||||
the design.
|
||||
|
||||
### 4.9 The test suite (opt-in)
|
||||
|
||||
**FR9.1:** `tests/test_failcount.py` (new) — **default-on**. Unit
|
||||
tests for the failure threshold module. The full test inventory:
|
||||
- `test_initial_state_zero`
|
||||
- `test_red_phase_failure_increments`
|
||||
- `test_green_success_resets_red_counter`
|
||||
- `test_green_phase_failure_increments`
|
||||
- `test_no_progress_advances`
|
||||
- `test_no_progress_resets_on_commit`
|
||||
- `test_no_progress_resets_on_green`
|
||||
- `test_threshold_fires_at_three`
|
||||
- `test_threshold_does_not_fire_at_two`
|
||||
- `test_multi_signal_independence`
|
||||
- `test_any_signal_triggers`
|
||||
- `test_state_persistence_round_trip`
|
||||
- `test_configurable_thresholds`
|
||||
|
||||
Target: 100% line + branch coverage on `failcount.py`.
|
||||
|
||||
**FR9.2:** `tests/test_tier2_slash_command_spec.py` (new) — **default-on**.
|
||||
Loads the slash command markdown, verifies its protocol contract
|
||||
(argument parsing, git commands, failcount check, report writing).
|
||||
|
||||
**FR9.3:** `tests/test_tier2_setup_bootstrap.py` (new) — **opt-in**
|
||||
(`TIER2_SANDBOX_TESTS=1`). Runs `setup_tier2_clone.ps1` against a
|
||||
fixture workspace, verifies the side effects (clone exists, origin
|
||||
set, templates copied, hooks installed, app-data dir created with
|
||||
ACLs).
|
||||
|
||||
**FR9.4:** `tests/test_tier2_sandbox_enforcement.py` (new) —
|
||||
**opt-in** (`TIER2_SANDBOX_TESTS=1`). The critical test: spawns the
|
||||
wrapper in a subprocess, inside the sandboxed context attempts
|
||||
each banned operation, verifies each is denied.
|
||||
|
||||
**FR9.5:** `tests/test_tier2_report_writer.py` (new) — **opt-in**
|
||||
(`TIER2_SANDBOX_TESTS=1`). Invokes failcount until give-up,
|
||||
verifies the report file is created at the right path with the
|
||||
right 7 sections.
|
||||
|
||||
**FR9.6:** `tests/test_tier2_smoke_e2e.py` (new) — **opt-in**
|
||||
(`TIER2_SANDBOX_TESTS=1 TIER2_SMOKE=1`). Runs the full pipeline
|
||||
against a fixture workspace: bootstrap → invoke the CLI entry
|
||||
point → verify the feature branch exists with 1 commit → verify
|
||||
the report file is NOT created (success path).
|
||||
|
||||
## 5. Non-Functional Requirements
|
||||
|
||||
**NFR1. Performance:** the failcount module adds <1ms per check.
|
||||
The slash command's protocol adds <500ms to a typical Tier 2 task
|
||||
(spec fetch + branch creation + state init).
|
||||
|
||||
**NFR2. Reliability:** the failcount state is persisted after every
|
||||
commit. A killed run can be resumed (or refused to resume) on the
|
||||
next invocation. The state file uses atomic write (write to
|
||||
`state.json.tmp` + `os.replace`) to survive crashes mid-write.
|
||||
|
||||
**NFR3. Security:**
|
||||
- The 4 git bans are enforced at 3 independent layers (OpenCode
|
||||
permission system, Windows OS-level via restricted token, git
|
||||
hooks). A bypass of one layer is caught by another.
|
||||
- The filesystem boundary is enforced at 2 independent layers
|
||||
(OpenCode path allowlist, Windows ACLs).
|
||||
- The Tier 2 process tree is wrapped in a Job Object that
|
||||
prevents child process escape.
|
||||
|
||||
**NFR4. Testability:**
|
||||
- The failcount module is pure logic, 100% unit-testable without
|
||||
any infrastructure.
|
||||
- The slash command's protocol is duplicated in
|
||||
`scripts/tier2/run_track.py` (CLI entry point) so the smoke e2e
|
||||
test runs without an OpenCode session.
|
||||
- All sandbox / bootstrap / smoke tests are env-var-gated
|
||||
(`TIER2_SANDBOX_TESTS=1`, `TIER2_SMOKE=1`).
|
||||
|
||||
**NFR5. Auditability:** every Tier 2 run writes to
|
||||
`C:\Users\Ed\AppData\Local\manual_slop\tier2\<track>\state.json`
|
||||
and (on give-up) `C:\Users\Ed\AppData\Local\manual_slop\tier2_failures\<track>_<timestamp>.md`.
|
||||
The user can inspect the state at any time.
|
||||
|
||||
**NFR6. UX:** the user clicks zero times during a normal Tier 2
|
||||
run. The "did Tier 2 give up?" check is passive (an OpenCode
|
||||
banner, an optional Windows toast, and a flag file the user can
|
||||
check on next Tier 1 session start).
|
||||
|
||||
**NFR7. Backward compatibility:** the main repo's `opencode.json`
|
||||
is not modified. Tier 1 retains its `permission: ask` workflow.
|
||||
The new agent profile (`tier2-autonomous`) is in the Tier 2 clone
|
||||
only. The new slash command is in the Tier 2 clone only.
|
||||
|
||||
## 6. Architecture Reference
|
||||
|
||||
**This track's design follows these existing patterns:**
|
||||
|
||||
- **`docs/guide_architecture.md`** §"Threading model" — the
|
||||
Tier 2 process tree runs in its own Job Object, isolated from
|
||||
the user's main session.
|
||||
- **`docs/guide_mma.md`** §"Tier 2/3/4 lifecycles" — the Tier 2
|
||||
Tech Lead's existing delegation patterns (Task tool to
|
||||
`@tier3-worker`, `@tier4-qa`) are preserved in the autonomous
|
||||
mode.
|
||||
- **`docs/guide_meta_boundary.md`** — this track is squarely in
|
||||
the "Meta-Tooling" environment (it builds execution infrastructure
|
||||
for the agents), not the "Application" environment. No changes
|
||||
to `src/*.py`.
|
||||
- **`docs/guide_testing.md`** §"Authoring robust live_gui tests"
|
||||
+ the `live_gui` session-scoped pattern — the smoke e2e test
|
||||
follows the same opt-in env-var-gated pattern.
|
||||
- **`conductor/code_styleguides/python.md`** — 1-space indentation,
|
||||
CRLF line endings, no comments, strict type hints. All new Python
|
||||
code in this track follows this styleguide.
|
||||
- **`conductor/code_styleguides/error_handling.md`** — the
|
||||
failcount module uses `Result[T, ErrorInfo]` per the convention
|
||||
(the 3 refactored baseline files use it; the convention is being
|
||||
rolled out across the codebase per
|
||||
`data_oriented_error_handling_20260606` + the upcoming
|
||||
`result_migration_20260616` sub-tracks).
|
||||
|
||||
**This track's NEW patterns (the contribution to the codebase):**
|
||||
|
||||
- **Sibling clone as execution mode switch** — opening OpenCode in
|
||||
a different directory IS the mode switch (no `mode:` flag in
|
||||
`opencode.json`, no env var, just a directory).
|
||||
- **3-layer enforcement stack** — OpenCode permission system +
|
||||
Windows restricted token + git hooks. Documented in
|
||||
`docs/guide_tier2_autonomous.md` (this track's new guide).
|
||||
- **Bounded autonomous run with fail-loud** — the failcount module
|
||||
is a general-purpose "I'm stuck" detector, applicable to any
|
||||
future autonomous run (not just Tier 2). The pattern is
|
||||
reusable for any sub-agent that has a contract to follow.
|
||||
|
||||
## 7. Out of Scope
|
||||
|
||||
- **No changes to the Manual Slop app (`src/*.py`).** This is
|
||||
meta-tooling, not the app. The 4 audit scripts
|
||||
(`audit_exception_handling.py`, `audit_weak_types.py`,
|
||||
`audit_main_thread_imports.py`, `audit_no_models_config_io.py`)
|
||||
are not modified.
|
||||
- **No changes to the main repo's `opencode.json` or MMA agent
|
||||
profiles.** The new `tier2-autonomous` profile lives in the
|
||||
Tier 2 clone only.
|
||||
- **No new top-level `src/<thing>.py` files.** Per the file-naming
|
||||
convention (`AGENTS.md` §"File Size and Naming Convention"), the
|
||||
new code is in `scripts/tier2/`, `conductor/tier2/`, and `tests/`
|
||||
(all namespace-isolated by directory).
|
||||
- **No changes to existing tracks or in-flight work.** The
|
||||
`result_migration_20260616` umbrella track, the
|
||||
`data_oriented_error_handling_20260606` track, and the
|
||||
`exception_handling_audit_20260616` track are not affected.
|
||||
- **No new audit script.** The failcount thresholds are TOML config,
|
||||
not statically checkable. If a future track adds a checkable
|
||||
convention (e.g., "all CLI entry points must use Result[T]"),
|
||||
the new audit script should follow the
|
||||
`scripts/audit_<name>.py` pattern from the existing 4.
|
||||
- **No WSL2 / Docker / Windows Sandbox variants.** The user
|
||||
approved Approach 1 (OpenCode + Windows restricted token + git
|
||||
hooks, all native Windows). WSL2 was considered and deferred;
|
||||
the failure to run Dear PyGui/ImGui tests in WSL2 was the
|
||||
deciding factor.
|
||||
- **No parallel Tier 2 runs.** The Tier 2 clone is a single
|
||||
workspace. Two parallel Tier 2 runs would conflict on the
|
||||
feature branch. If parallel runs become a need, that's a
|
||||
follow-up track.
|
||||
- **No `git push` to non-origin remotes.** Even though the deny
|
||||
rule is `git push*` (any push), the practical use case is
|
||||
"Tier 2 doesn't push at all; the user pushes after review."
|
||||
Adding a "push to a tier2-remote bare dir" workflow is a
|
||||
follow-up if needed.
|
||||
- **No automated review of the feature branch.** Tier 1 reviewing
|
||||
Tier 2's branch is a future track (out of scope here).
|
||||
|
||||
---
|
||||
|
||||
**Spec ends.** The implementation plan (`plan.md` + `metadata.json`)
|
||||
will be written by the `writing-plans` skill in the next phase, after
|
||||
the user reviews this spec.
|
||||
@@ -1,119 +0,0 @@
|
||||
# Track state for tier2_autonomous_sandbox_20260616
|
||||
# Updated by Tier 2 Tech Lead as tasks complete
|
||||
|
||||
[meta]
|
||||
track_id = "tier2_autonomous_sandbox_20260616"
|
||||
name = "Tier 2 Autonomous Sandbox (unattended track execution with bounded blast radius)"
|
||||
status = "completed"
|
||||
current_phase = "complete"
|
||||
last_updated = "2026-06-16"
|
||||
|
||||
[blocked_by]
|
||||
# None - independent track (per spec §1.1)
|
||||
|
||||
[blocks]
|
||||
# None - this is a meta-tooling track; no follow-ups planned in this spec
|
||||
|
||||
[phases]
|
||||
phase_1 = { status = "completed", checkpointsha = "2dbfaeb6", name = "failcount Module + Tests (TDD red/green)" }
|
||||
phase_2 = { status = "completed", checkpointsha = "73ab2778", name = "Failure Report Writer" }
|
||||
phase_3 = { status = "completed", checkpointsha = "9964ad3b", name = "Slash Command + Agent Profile + Spec Test" }
|
||||
phase_4 = { status = "completed", checkpointsha = "796da0de", name = "CLI Entry Point (run_track.py)" }
|
||||
phase_5 = { status = "completed", checkpointsha = "a9be60ae", name = "PowerShell Bootstrap (setup_tier2_clone.ps1)" }
|
||||
phase_6 = { status = "completed", checkpointsha = "cba5457b", name = "PowerShell Sandbox Launcher (run_tier2_sandboxed.ps1)" }
|
||||
phase_7 = { status = "completed", checkpointsha = "e487d34b", name = "Git Hooks" }
|
||||
phase_8 = { status = "completed", checkpointsha = "3e17aa6c", name = "Opt-in Tests (Sandbox Enforcement + Smoke E2E)" }
|
||||
phase_9 = { status = "completed", checkpointsha = "eedbfa11", name = "User Guide + Final Verification" }
|
||||
|
||||
[tasks]
|
||||
# Phase 1: failcount Module + Tests
|
||||
t1_1 = { status = "completed", commit_sha = "9f2ff29c", description = "Create the scripts/tier2/ package directory" }
|
||||
t1_2 = { status = "completed", commit_sha = "e646067a", description = "Write test_initial_state_zero (red)" }
|
||||
t1_3 = { status = "completed", commit_sha = "fc92e1aa", description = "Implement FailcountState + FailcountConfig dataclasses (green)" }
|
||||
t1_4 = { status = "completed", commit_sha = "190766fe", description = "Create the default failcount.toml" }
|
||||
t1_5 = { status = "completed", commit_sha = "2dbfaeb6", description = "Write + implement remaining 17 tests; 100% coverage" }
|
||||
t1_16 = { status = "completed", commit_sha = "2dbfaeb6", description = "Verify 100% coverage on failcount.py" }
|
||||
|
||||
# Phase 2: Failure Report Writer
|
||||
t2_1 = { status = "completed", commit_sha = "5ca8444f", description = "Write test_report_path_is_correct (red)" }
|
||||
t2_2 = { status = "completed", commit_sha = "73ab2778", description = "Implement compute_report_path, compute_stopped_flag_path, TaskResult (green)" }
|
||||
t2_3 = { status = "completed", commit_sha = "73ab2778", description = "Write + implement test_report_has_7_sections" }
|
||||
t2_4 = { status = "completed", commit_sha = "73ab2778", description = "Implement write_failure_report with 7 sections + flag" }
|
||||
|
||||
# Phase 3: Slash Command + Agent Profile + Spec Test
|
||||
t3_1 = { status = "completed", commit_sha = "7380e23b", description = "Create the tier-2-auto-execute.md slash command template" }
|
||||
t3_2 = { status = "completed", commit_sha = "016381c4", description = "Create the tier2-autonomous.md agent template" }
|
||||
t3_3 = { status = "completed", commit_sha = "154a3707", description = "Create the opencode.json.fragment config template" }
|
||||
t3_4 = { status = "completed", commit_sha = "9964ad3b", description = "Write test_tier2_slash_command_spec.py (12 contract assertions)" }
|
||||
t3_5 = { status = "completed", commit_sha = "9964ad3b", description = "User Manual Verification (Phase 3)" }
|
||||
|
||||
# Phase 4: CLI Entry Point (run_track.py)
|
||||
t4_1 = { status = "completed", commit_sha = "796da0de", description = "Create run_track.py skeleton with argparse" }
|
||||
t4_2 = { status = "completed", commit_sha = "796da0de", description = "Wire in git fetch + branch creation" }
|
||||
t4_3 = { status = "completed", commit_sha = "796da0de", description = "User Manual Verification (Phase 4)" }
|
||||
|
||||
# Phase 5: PowerShell Bootstrap (setup_tier2_clone.ps1)
|
||||
t5_1 = { status = "completed", commit_sha = "a9be60ae", description = "Create the bootstrap script skeleton with -WhatIf" }
|
||||
t5_2 = { status = "completed", commit_sha = "a9be60ae", description = "User Manual Verification (Phase 5)" }
|
||||
|
||||
# Phase 6: PowerShell Sandbox Launcher (run_tier2_sandboxed.ps1)
|
||||
t6_1 = { status = "completed", commit_sha = "cba5457b", description = "Create the launcher skeleton (restricted token, Job Object)" }
|
||||
t6_2 = { status = "completed", commit_sha = "cba5457b", description = "User Manual Verification (Phase 6)" }
|
||||
|
||||
# Phase 7: Git Hooks
|
||||
t7_1 = { status = "completed", commit_sha = "01be3923", description = "Create pre-push hook (refuses all pushes)" }
|
||||
t7_2 = { status = "completed", commit_sha = "e487d34b", description = "Create post-checkout hook (detection only)" }
|
||||
|
||||
# Phase 8: Opt-in Tests (Sandbox Enforcement + Smoke E2E)
|
||||
t8_1 = { status = "completed", commit_sha = "cb7c8200", description = "Add tier2_sandbox and tier2_smoke markers to pyproject.toml" }
|
||||
t8_2 = { status = "completed", commit_sha = "37eafc00", description = "Create the trivial smoke track (spec + plan)" }
|
||||
t8_3 = { status = "completed", commit_sha = "5d150dc6", description = "Create test_tier2_setup_bootstrap.py (opt-in, -WhatIf)" }
|
||||
t8_4 = { status = "completed", commit_sha = "5b6e7db1", description = "Create test_tier2_sandbox_enforcement.py (opt-in, pre-push hook)" }
|
||||
t8_5 = { status = "completed", commit_sha = "3e17aa6c", description = "Create test_tier2_smoke_e2e.py (opt-in, double gate)" }
|
||||
t8_6 = { status = "completed", commit_sha = "3e17aa6c", description = "User Manual Verification (Phase 8)" }
|
||||
|
||||
# Phase 9: User Guide + Final Verification
|
||||
t9_1 = { status = "completed", commit_sha = "8bf7cd17", description = "Create the user guide (docs/guide_tier2_autonomous.md)" }
|
||||
t9_2 = { status = "completed", commit_sha = "2f79f199", description = "Update conductor/tracks.md with the new track" }
|
||||
t9_3 = { status = "completed", commit_sha = "eedbfa11", description = "Update metadata.json to status=shipped" }
|
||||
t9_4 = { status = "completed", commit_sha = "eedbfa11", description = "Final User Manual Verification (full track)" }
|
||||
|
||||
[verification]
|
||||
phase_1_failcount_tests_pass = true
|
||||
phase_2_report_writer_tests_pass = true
|
||||
phase_3_slash_command_spec_pass = true
|
||||
phase_4_cli_entry_point_runs = true
|
||||
phase_5_bootstrap_whatif_works = true
|
||||
phase_6_sandbox_launcher_runs = true
|
||||
phase_7_git_hooks_installed = true
|
||||
phase_8_optin_tests_pass = true
|
||||
phase_9_user_guide_complete = true
|
||||
default_pytest_app_focused = true
|
||||
optin_sandbox_tests_under_env_var = true
|
||||
optin_smoke_tests_under_double_env_var = true
|
||||
metadata_json_valid = true
|
||||
|
||||
[test_progress]
|
||||
failcount_unit_tests_target = 19
|
||||
failcount_unit_tests_passing = 19
|
||||
slash_command_spec_tests_target = 12
|
||||
slash_command_spec_tests_passing = 12
|
||||
report_writer_tests_target = 8
|
||||
report_writer_tests_passing = 8
|
||||
bootstrap_tests_target = 1
|
||||
bootstrap_tests_passing = 1
|
||||
sandbox_enforcement_tests_target = 1
|
||||
sandbox_enforcement_tests_passing = 1
|
||||
smoke_e2e_tests_target = 1
|
||||
smoke_e2e_tests_passing = 1
|
||||
|
||||
[enforcement_stack]
|
||||
git_push_ban_enforced = true
|
||||
git_checkout_ban_enforced = true
|
||||
git_restore_ban_enforced = true
|
||||
git_reset_ban_enforced = true
|
||||
filesystem_boundary_enforced = true
|
||||
pre_push_hook_installed = true
|
||||
post_checkout_hook_installed = true
|
||||
opencode_deny_rules_in_clone = true
|
||||
windows_restricted_token_acquired = true
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"id": "tier2_no_appdata_20260618",
|
||||
"name": "Tier 2 Sandbox - Move State/Failures Off AppData",
|
||||
"date": "2026-06-18",
|
||||
"type": "fix",
|
||||
"priority": "A",
|
||||
"spec": "conductor/tracks/tier2_no_appdata_20260618/spec.md",
|
||||
"plan": "conductor/tracks/tier2_no_appdata_20260618/plan.md",
|
||||
"status": "active",
|
||||
"blocked_by": {},
|
||||
"blocks": {},
|
||||
"scope": {
|
||||
"new_files": [],
|
||||
"modified_files": [
|
||||
"scripts/tier2/failcount.py",
|
||||
"scripts/tier2/write_report.py",
|
||||
"scripts/tier2/run_track.py",
|
||||
"scripts/tier2/setup_tier2_clone.ps1",
|
||||
"scripts/tier2/run_tier2_sandboxed.ps1",
|
||||
"scripts/tier2/write_track_completion_report.py",
|
||||
"conductor/tier2/opencode.json.fragment",
|
||||
"conductor/tier2/agents/tier2-autonomous.md",
|
||||
"conductor/tier2/commands/tier-2-auto-execute.md",
|
||||
"docs/guide_tier2_autonomous.md",
|
||||
"conductor/workflow.md",
|
||||
".gitignore",
|
||||
"tests/test_tier2_slash_command_spec.py",
|
||||
"tests/test_no_temp_writes.py"
|
||||
],
|
||||
"deleted_files": []
|
||||
},
|
||||
"verification_criteria": [
|
||||
"scripts/tier2/failcount.py default state dir is scripts/tier2/state/<track>/ (Path.cwd()-relative)",
|
||||
"scripts/tier2/write_report.py default failures dir is scripts/tier2/failures/ (Path.cwd()-relative)",
|
||||
"scripts/tier2/run_track.py chdirs to repo_path before state/report calls",
|
||||
"conductor/tier2/opencode.json.fragment has NO AppData allow rules in read/write",
|
||||
"conductor/tier2/opencode.json.fragment has *AppData\\* bash deny rule (in addition to *AppData\\Local\\Temp\\*)",
|
||||
"conductor/tier2/agents/tier2-autonomous.md contains 'NEVER USE APPDATA' or equivalent phrasing; no AppData path strings",
|
||||
"conductor/tier2/commands/tier-2-auto-execute.md contains no AppData path strings",
|
||||
"scripts/tier2/setup_tier2_clone.ps1 has no AppData variable declarations or New-Item/Set-Acl calls",
|
||||
"scripts/tier2/run_tier2_sandboxed.ps1 has no AppData variable declarations",
|
||||
"docs/guide_tier2_autonomous.md has no AppData path strings",
|
||||
"conductor/workflow.md hard-bans table row says 'File access outside Tier 2 clone (AppData denied)'",
|
||||
".gitignore has scripts/tier2/state/ and scripts/tier2/failures/",
|
||||
"tests/test_tier2_slash_command_spec.py asserts NO AppData refs in agent prompt and command",
|
||||
"uv run python scripts/run_tests_batched.py passes for test_failcount.py + test_tier2_report_writer.py + test_tier2_slash_command_spec.py + test_no_temp_writes.py",
|
||||
"uv run python scripts/audit_no_temp_writes.py --strict exits 0"
|
||||
],
|
||||
"regressions_and_pre_existing_failures": [],
|
||||
"pre_existing_failures_remaining": [],
|
||||
"deferred_to_followup_tracks": [
|
||||
{
|
||||
"title": "Re-bootstrap the live Tier 2 clone",
|
||||
"description": "The user re-runs pwsh -File scripts/tier2/setup_tier2_clone.ps1 after this track merges so the clone picks up the new inside-clone conventions and the AppData-denied permissions.",
|
||||
"track_status": "manual user action"
|
||||
}
|
||||
],
|
||||
"estimated_effort": {
|
||||
"method": "scope (per workflow.md §Tier 1 Track Initialization Rules). NO day estimates.",
|
||||
"scope": "11 source files + 3 test files + 1 doc + 1 workflow.md section + 1 .gitignore; ~15 atomic commits across 6 phases."
|
||||
},
|
||||
"risk_register": [
|
||||
{
|
||||
"risk": "An existing Tier 2 run is using the old AppData config and its state cannot be migrated automatically",
|
||||
"likelihood": "high",
|
||||
"mitigation": "Document in the spec that the user's existing live_gui_test_fixes_20260618 run is unaffected by this change until re-bootstrap. State on AppData is discarded on next bootstrap."
|
||||
},
|
||||
{
|
||||
"risk": "The AppData path strings are hard-coded in a downstream script we missed",
|
||||
"likelihood": "medium",
|
||||
"mitigation": "Run scripts/audit_no_temp_writes.py --strict after the changes. Run a grep for 'AppData' across scripts/ and conductor/ and docs/ as the final verification."
|
||||
},
|
||||
{
|
||||
"risk": "The TIER2_STATE_DIR / TIER2_FAILURES_DIR env-var escape hatch is removed by mistake",
|
||||
"likelihood": "low",
|
||||
"mitigation": "The existing tests (tests/test_failcount.py:176,190,198 and tests/test_tier2_report_writer.py:25,33,40,71) monkeypatch the env var. They must still pass after the change."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
# Track Plan: Tier 2 Sandbox - Move State/Failures Off AppData
|
||||
|
||||
**Goal:** move failcount state and failure-report locations inside the Tier 2 clone; remove all AppData references from Tier 2 conventions, permissions, scripts, docs, and tests.
|
||||
**Scope:** 11 source files + 3 test files + 1 doc + 1 workflow.md section + 1 .gitignore.
|
||||
**Convention:** 1-space Python indentation. CRLF where the file is already CRLF (do not normalize).
|
||||
|
||||
## Phase 1: Move the default state and failure-report paths
|
||||
|
||||
Focus: change the Python defaults so load/save use `scripts/tier2/state/...` and `scripts/tier2/failures/...` when no env-var override is set.
|
||||
|
||||
### Task 1.1: Update `scripts/tier2/failcount.py:_state_dir` default
|
||||
- **WHERE:** `scripts/tier2/failcount.py:117-123` (the `_state_dir(track_name)` function).
|
||||
- **WHAT:** change the default `base` from `r"C:\Users\Ed\AppData\Local\manual_slop\tier2"` to `Path.cwd() / "scripts" / "tier2" / "state"` (computed when the function is called; `Path` import already present at line 11).
|
||||
- **HOW:** rewrite the function as:
|
||||
```python
|
||||
def _state_dir(track_name: str) -> Path:
|
||||
base_str = os.environ.get("TIER2_STATE_DIR")
|
||||
if base_str:
|
||||
return Path(base_str) / track_name
|
||||
return Path.cwd() / "scripts" / "tier2" / "state" / track_name
|
||||
```
|
||||
- **SAFETY:** preserve the env-var escape hatch (`TIER2_STATE_DIR`); preserve the `Path` return type. The function has no other callers.
|
||||
- **COMMIT:** `fix(tier2): move failcount state default inside Tier 2 clone (scripts/tier2/state/)`
|
||||
|
||||
### Task 1.2: Update `scripts/tier2/write_report.py:_failures_dir` default
|
||||
- **WHERE:** `scripts/tier2/write_report.py:20-23` (the `_failures_dir()` function).
|
||||
- **WHAT:** change the default from `r"C:\Users\Ed\AppData\Local\manual_slop\tier2_failures"` to `Path.cwd() / "scripts" / "tier2" / "failures"`.
|
||||
- **HOW:** rewrite the function as:
|
||||
```python
|
||||
def _failures_dir() -> Path:
|
||||
base_str = os.environ.get("TIER2_FAILURES_DIR")
|
||||
if base_str:
|
||||
return Path(base_str)
|
||||
return Path.cwd() / "scripts" / "tier2" / "failures"
|
||||
```
|
||||
- **SAFETY:** preserve `TIER2_FAILURES_DIR` env-var override; preserve the `Path` return type. Callers are `compute_report_path`, `compute_stopped_flag_path`, and `write_failure_report` (all in the same file).
|
||||
- **COMMIT:** `fix(tier2): move failure-report default inside Tier 2 clone (scripts/tier2/failures/)`
|
||||
|
||||
### Task 1.3: `scripts/tier2/run_track.py` chdir before state calls
|
||||
- **WHERE:** `scripts/tier2/run_track.py:run_init` (around line 78, before `save_state`) and `run_track.py:run_report` (around line 100, before `write_failure_report`).
|
||||
- **WHAT:** add `os.chdir(repo_path)` so `Path.cwd()` in `_state_dir` / `_failures_dir` resolves to the repo root.
|
||||
- **HOW:** add `import os` at the top (the file already imports `argparse`, `subprocess`, `sys`, `datetime`, `pathlib`); add `os.chdir(repo_path)` as the first line of `run_init` and `run_report`.
|
||||
- **SAFETY:** `os.chdir` is process-global; this is acceptable because `run_track.py` is the CLI entry point, not a library. The chdir is idempotent within a single invocation.
|
||||
- **COMMIT:** `fix(tier2): chdir to repo_path in run_track before state/report calls`
|
||||
|
||||
### Task 1.4: Add `scripts/tier2/state/` and `scripts/tier2/failures/` to .gitignore
|
||||
- **WHERE:** `.gitignore` (top-level). Currently excludes `scripts/generated` on line 11.
|
||||
- **WHAT:** add `scripts/tier2/state/` and `scripts/tier2/failures/` after the `scripts/generated` line.
|
||||
- **HOW:** edit the file in place.
|
||||
- **SAFETY:** these are track-isolated scratch dirs; committing them would pollute the tree.
|
||||
- **COMMIT:** `chore(tier2): gitignore scripts/tier2/state/ and scripts/tier2/failures/`
|
||||
|
||||
## Phase 2: Update OpenCode permissions and agent/command prompts
|
||||
|
||||
Focus: remove AppData allow rules from the OpenCode JSON fragment; update the agent prompt and slash command to say "NEVER USE APPDATA".
|
||||
|
||||
### Task 2.1: `conductor/tier2/opencode.json.fragment` — remove AppData allow rules
|
||||
- **WHERE:** lines 10-11, 16-17, 62-63, 68-69 (the `permission.read` and `permission.write` blocks at top level and at the `tier2-autonomous` agent level).
|
||||
- **WHAT:** delete the two `C:\\Users\\Ed\\AppData\\Local\\manual_slop\\tier2\\**` and `C:\\Users\\Ed\\AppData\\Local\\manual_slop\\tier2_failures\\**` allow rules. The remaining allow rule (the Tier 2 clone path) is unchanged.
|
||||
- **HOW:** four targeted `edit_file` calls (one per `read`/`write` block × top-level/agent).
|
||||
- **SAFETY:** keep the existing `*AppData\\Local\\Temp\\*` bash deny rule. **Do NOT** modify the bash rules in this task — that's Task 2.2.
|
||||
- **COMMIT:** `fix(tier2): remove AppData allow rules from OpenCode permission JSON`
|
||||
|
||||
### Task 2.2: `conductor/tier2/opencode.json.fragment` — add `*AppData\\*` bash deny
|
||||
- **WHERE:** the `permission.bash` block at top level (line 46) and at the `tier2-autonomous` agent level (line 73).
|
||||
- **WHAT:** add `"*AppData\\*": "deny"` after the existing `"*AppData\\Local\\Temp\\*": "deny"` rule. The broader pattern catches `Local`, `LocalLow`, `Roaming`, and any other subdir.
|
||||
- **HOW:** two targeted edits.
|
||||
- **SAFETY:** the rule denies any bash command containing `AppData\`. Legitimate Tier 2 work does not write there. Combined with Task 2.1 (no allow rules), this is belt-and-suspenders.
|
||||
- **COMMIT:** `fix(tier2): add *AppData\\* bash deny rule (broader than just Temp)`
|
||||
|
||||
### Task 2.3: `conductor/tier2/agents/tier2-autonomous.md` — replace AppData convention
|
||||
- **WHERE:** line 47 (the "Temp files" bullet under "Conventions (MUST follow - added 2026-06-17)").
|
||||
- **WHAT:** replace the entire bullet. The new bullet says: "All scratch, state, audit-output, and intermediate files MUST live inside the Tier 2 clone (the OpenCode `*` deny rule blocks everything else). Default locations: `scripts/tier2/state/<track>/state.json` for failcount state, `scripts/tier2/failures/` for failure reports, `scripts/tier2/artifacts/<track>/` for throwaway scripts. **The `C:\Users\Ed\AppData\...` tree is OFF-LIMITS** for any read, write, or shell command. The OpenCode `*AppData\\*` bash deny rule enforces this."
|
||||
- **HOW:** edit_file on the bullet's full text.
|
||||
- **SAFETY:** preserve the env-var escape-hatch language (TIER2_STATE_DIR / TIER2_FAILURES_DIR are honored if set).
|
||||
- **COMMIT:** `docs(tier2): agent prompt - replace AppData convention with inside-clone convention`
|
||||
|
||||
### Task 2.4: `conductor/tier2/commands/tier-2-auto-execute.md` — replace AppData convention
|
||||
- **WHERE:** line 46 (the "Temp files" bullet under "Conventions (MUST follow - added 2026-06-17)").
|
||||
- **WHAT:** identical change to Task 2.3, applied to the slash command prompt. Also update line 19 ("Check for a previous run" — the path is `<app-data>/tier2/<track-name>/state.json`) and line 25 (step 3 in Protocol — "Initialize failcount state at `<app-data>/tier2/<track-name>/state.json`") to reference `scripts/tier2/state/<track-name>/state.json`.
|
||||
- **HOW:** three edit_file calls.
|
||||
- **SAFETY:** the slash command prompt is what the Tier 2 agent reads; if it still says `<app-data>`, the agent will continue trying to use AppData.
|
||||
- **COMMIT:** `docs(tier2): slash command - replace AppData paths with inside-clone paths`
|
||||
|
||||
## Phase 3: Update bootstrap scripts
|
||||
|
||||
Focus: `setup_tier2_clone.ps1` and `run_tier2_sandboxed.ps1` stop creating/referencing AppData dirs.
|
||||
|
||||
### Task 3.1: `scripts/tier2/setup_tier2_clone.ps1` — remove AppData dir creation
|
||||
- **WHERE:** lines 23 (`$AppDataDir`), 30 (`$AppDataFailuresDir`), 122-133 (the `New-Item` / `Get-Acl` / `Set-Acl` block).
|
||||
- **WHAT:** delete the `$AppDataDir` and `$AppDataFailuresDir` parameter / variable declarations and the entire "Create app-data dir with restricted ACLs" step block. Update the docstring (lines 6-9) to remove the "creates the app-data temp dir with restricted ACLs" sentence.
|
||||
- **HOW:** three edit_file calls.
|
||||
- **SAFETY:** the script must still create the Tier 2 clone, copy templates, install git hooks, and create the desktop shortcut. The deleted step is purely about AppData dirs.
|
||||
- **COMMIT:** `fix(tier2): setup_tier2_clone.ps1 - stop creating AppData dirs`
|
||||
|
||||
### Task 3.2: `scripts/tier2/run_tier2_sandboxed.ps1` — remove AppData dir references
|
||||
- **WHERE:** lines 20-21 (`$AppDataDir`, `$AppDataFailuresDir`), line 7 (docstring), line 77 (the "Set explicit ACLs on the Tier 2 clone + app-data dir" comment).
|
||||
- **WHAT:** delete the `$AppDataDir` / `$AppDataFailuresDir` variable declarations and any ACL-set logic that references them. Update the docstring (line 7) to remove "app-data dir" from the list.
|
||||
- **HOW:** four edit_file calls.
|
||||
- **SAFETY:** the restricted-token + Job-Object + launch logic must stay intact.
|
||||
- **COMMIT:** `fix(tier2): run_tier2_sandboxed.ps1 - remove AppData dir references`
|
||||
|
||||
## Phase 4: Update tests
|
||||
|
||||
Focus: flip the slash-command-spec tests so they assert "no AppData refs" instead of "AppData refs required"; update `test_no_temp_writes.py` docstring and fix-message.
|
||||
|
||||
### Task 4.1: `tests/test_tier2_slash_command_spec.py:test_agent_denies_temp_writes`
|
||||
- **WHERE:** lines 82-91 (the entire `test_agent_denies_temp_writes` function).
|
||||
- **WHAT:** flip the assertions. Replace:
|
||||
```python
|
||||
assert 'AppData\\Local\\Temp' in content, "agent prompt must include Temp deny rule in frontmatter bash"
|
||||
assert 'AppData\\Local\\manual_slop\\tier2' in content or 'app-data' in content.lower(), "agent prompt must point agent at the app-data dir for temp files"
|
||||
```
|
||||
with:
|
||||
```python
|
||||
assert 'AppData\\Local\\Temp' in content, "agent prompt must include Temp deny rule in frontmatter bash"
|
||||
assert "*AppData\\\\*" in content or "AppData\\\\*" in content, "agent prompt must include the broader AppData deny rule"
|
||||
assert "scripts/tier2/state" in content, "agent prompt must point agent at scripts/tier2/state for failcount state"
|
||||
assert "scripts/tier2/failures" in content, "agent prompt must point agent at scripts/tier2/failures for failure reports"
|
||||
assert "AppData\\Local\\manual_slop\\tier2" not in content, "agent prompt must NOT reference the AppData tier2 dir (2026-06-18 hard ban)"
|
||||
```
|
||||
Update the docstring to mention the 2026-06-18 reversal.
|
||||
- **HOW:** edit_file on the function body and docstring.
|
||||
- **SAFETY:** the `*AppData\\*` substring check matches the literal JSON bash key `"*AppData\\*"`. Be careful with Python string-escape semantics — use a raw string or a literal substring that survives the JSON double-escape.
|
||||
- **COMMIT:** `test(tier2): slash_command_spec - assert no AppData refs, point at inside-clone`
|
||||
|
||||
### Task 4.2: `tests/test_tier2_slash_command_spec.py:test_command_denies_temp_writes` (or the equivalent for the command file)
|
||||
- **WHERE:** the parallel test for the slash command prompt (likely also in `tests/test_tier2_slash_command_spec.py`).
|
||||
- **WHAT:** apply the same flip as Task 4.1 to the command prompt content.
|
||||
- **HOW:** edit_file.
|
||||
- **SAFETY:** keep the Temp deny assertion; add the new inside-clone-pointing assertions; remove the AppData-required assertion.
|
||||
- **COMMIT:** `test(tier2): slash_command_spec - command prompt assert no AppData refs`
|
||||
|
||||
### Task 4.3: `tests/test_no_temp_writes.py` docstring + fix message
|
||||
- **WHERE:** lines 1-15 (the docstring) and line 33 (the fix-message string).
|
||||
- **WHAT:** replace the AppData paths in the docstring (lines 6-7) with `scripts/tier2/state/` and `scripts/tier2/failures/`. Replace the fix-message suggestion on line 33 (`C:\\Users\\Ed\\AppData\\Local\\manual_slop\\tier2\\ instead of %TEMP%.`) with `scripts/tier2/state/ or scripts/tier2/failures/ instead of %TEMP%.`.
|
||||
- **HOW:** edit_file.
|
||||
- **SAFETY:** the audit script's behavior is unchanged; only the human-facing strings change.
|
||||
- **COMMIT:** `test(tier2): no_temp_writes - replace AppData refs in docstring + fix message`
|
||||
|
||||
## Phase 5: Update user-facing docs and workflow
|
||||
|
||||
Focus: `docs/guide_tier2_autonomous.md` and `conductor/workflow.md` stop referencing AppData.
|
||||
|
||||
### Task 5.1: `docs/guide_tier2_autonomous.md` — replace AppData refs
|
||||
- **WHERE:** line 24 (bootstrap step 5), line 59 (the "4 hard bans" table row), line 72 (failure report location), lines 119-129 (Troubleshooting section).
|
||||
- **WHAT:** replace each `C:\Users\Ed\AppData\Local\manual_slop\tier2...` reference with the new `scripts/tier2/state/...` / `scripts/tier2/failures/...` paths.
|
||||
- **HOW:** multiple edit_file calls (one per paragraph that contains an AppData path).
|
||||
- **SAFETY:** the guide's structure and other content stay intact; only path strings change.
|
||||
- **COMMIT:** `docs(tier2): guide_tier2_autonomous - replace AppData paths with inside-clone paths`
|
||||
|
||||
### Task 5.2: `conductor/workflow.md` — update hard bans table
|
||||
- **WHERE:** line 386 (the row "File access outside Tier 2 clone + app-data dir").
|
||||
- **WHAT:** replace with "File access outside Tier 2 clone (AppData, Temp, Documents, etc. all denied at the OpenCode `*` level + targeted `*AppData\\*` deny)."
|
||||
- **HOW:** edit_file.
|
||||
- **SAFETY:** the surrounding 3-layer-enforcement table structure stays.
|
||||
- **COMMIT:** `docs(tier2): workflow.md hard bans - AppData denied (no exception)`
|
||||
|
||||
### Task 5.3: `scripts/tier2/write_track_completion_report.py` — update report output
|
||||
- **WHERE:** lines 262, 264 (the "Filesystem boundary" and "Failcount monitored" rows in the generated report).
|
||||
- **WHAT:** replace the AppData path strings with `scripts/tier2/state/...` / `scripts/tier2/failures/...`.
|
||||
- **HOW:** two edit_file calls.
|
||||
- **SAFETY:** the generated report's structure stays; only path strings change. The report's downstream consumers (the user reading it after a Tier 2 run) need to see the actual paths the next run will use.
|
||||
- **COMMIT:** `fix(tier2): write_track_completion_report - use inside-clone paths in output`
|
||||
|
||||
## Phase 6: Conductor verification
|
||||
|
||||
Focus: ensure the test suite still passes after the changes; register the track in `conductor/tracks.md`.
|
||||
|
||||
### Task 6.1: Run targeted test batches
|
||||
- **COMMAND:** `uv run python scripts/run_tests_batched.py --tier tier-1-unit-core tests/test_failcount.py tests/test_tier2_report_writer.py tests/test_tier2_slash_command_spec.py tests/test_no_temp_writes.py`
|
||||
- **EXPECTED:** all 4 test files pass. The `test_failcount` and `test_tier2_report_writer` env-var tests pass because they monkeypatch the env var (FR7's backward-compat requirement). The `test_tier2_slash_command_spec` tests pass because the new assertions match the updated agent prompt and slash command. The `test_no_temp_writes` test passes because the audit script's behavior didn't change.
|
||||
- **COMMIT:** no commit (this is a verification step).
|
||||
|
||||
### Task 6.2: Run the static analyzer batch
|
||||
- **COMMAND:** `uv run python scripts/audit_no_temp_writes.py --strict`
|
||||
- **EXPECTED:** `CLEAN: no script under ./scripts/ emits to %TEMP%` and exit code 0. The audit's exclusion list (`scripts/tier2/artifacts`) covers the throwaway scripts that may still have AppData path strings.
|
||||
- **COMMIT:** no commit.
|
||||
|
||||
### Task 6.3: Register the track in `conductor/tracks.md`
|
||||
- **WHERE:** append a new entry block following the precedent set by `tier2_autonomous_sandbox_20260616`.
|
||||
- **WHAT:** add the link, spec, plan, metadata, status, and a one-line summary.
|
||||
- **COMMIT:** `conductor(tracks): register tier2_no_appdata_20260618 (shipped)` (after Phase 1-5 commit SHAs are recorded).
|
||||
|
||||
---
|
||||
|
||||
## End-of-Track Report (added 2026-06-17 convention)
|
||||
|
||||
On Phase 6 completion, write `docs/reports/TRACK_COMPLETION_tier2_no_appdata_20260618.md` following the precedent set by `docs/reports/TRACK_COMPLETION_tier2_autonomous_sandbox_20260616.md`. Update `conductor/tracks/tier2_no_appdata_20260618/state.toml` to `status = "completed"`.
|
||||
@@ -1,117 +0,0 @@
|
||||
# Track Specification: Tier 2 Sandbox - Move State/Failures Off AppData
|
||||
|
||||
**Track ID:** `tier2_no_appdata_20260618`
|
||||
**Date:** 2026-06-18
|
||||
**Priority:** A (the in-flight Tier 2 run for `live_gui_test_fixes_20260618` is blocked by the AppData path assumption; a future Tier 2 clone will inherit the broken config unless this ships)
|
||||
**Type:** fix (convention + infrastructure; no behavior change in product code)
|
||||
|
||||
## Overview
|
||||
|
||||
The Tier 2 autonomous sandbox currently persists its failcount state to `C:\Users\Ed\AppData\Local\manual_slop\tier2\<track>\state.json` and writes failure reports to `C:\Users\Ed\AppData\Local\manual_slop\tier2_failures\`. The OpenCode permission JSON allowlists both. The user has explicitly directed: **"NEVER USE APPDATA"** — meaning the whole `C:\Users\Ed\AppData\...` tree should be off-limits to the Tier 2 sandbox.
|
||||
|
||||
This track moves both the state and the failure-report directories **inside the Tier 2 clone** (`C:\projects\manual_slop_tier2\`) and removes every AppData reference from the conventions, the agent prompt, the slash command, the OpenCode JSON fragment, the bootstrap scripts, the user guide, and the tests. After this track, `C:\Users\Ed\AppData\...` is never referenced by the Tier 2 sandbox in any form.
|
||||
|
||||
## Current State Audit (as of 2026-06-18, commit 02aed999)
|
||||
|
||||
### Already Implemented (DO NOT re-implement)
|
||||
|
||||
- **Tier 2 sandbox enforcement (3-layer):** OpenCode `permission.bash` deny rules + Windows restricted token + git hooks. Shipped in `tier2_autonomous_sandbox_20260616` (commit `00c6922c`).
|
||||
- **`*AppData\Local\Temp\*` deny rule:** already blocks the global Temp dir (the 2026-06-17 regression fix). The bash deny keys are present in both the top-level and the `tier2-autonomous` agent's `permission.bash`.
|
||||
- **`scripts/audit_no_temp_writes.py`:** scans `./scripts/**` for any `%TEMP%` / `tempfile.` / `$env:TEMP` usage. Default-on regression test `tests/test_no_temp_writes.py` invokes it with `--strict`.
|
||||
- **TIER2_STATE_DIR / TIER2_FAILURES_DIR env-var overrides:** `scripts/tier2/failcount.py` and `scripts/tier2/write_report.py` already accept env-var overrides; the AppData paths are just the *defaults*.
|
||||
|
||||
### Gaps to Fill (This Track's Scope)
|
||||
|
||||
The AppData paths are still the **defaults** for failcount state and failure reports, and the conventions/permissions/tests all reinforce them:
|
||||
|
||||
1. **`scripts/tier2/failcount.py:117-123`** — `_state_dir(track_name)` defaults to `r"C:\Users\Ed\AppData\Local\manual_slop\tier2"` when `TIER2_STATE_DIR` is unset.
|
||||
2. **`scripts/tier2/write_report.py:20-23`** — `_failures_dir()` defaults to `r"C:\Users\Ed\AppData\Local\manual_slop\tier2_failures"` when `TIER2_FAILURES_DIR` is unset.
|
||||
3. **`conductor/tier2/opencode.json.fragment`** — `permission.read` and `permission.write` allowlist `C:\Users\Ed\AppData\Local\manual_slop\tier2\**` and `C:\Users\Ed\AppData\Local\manual_slop\tier2_failures\**` at both the top level and the `tier2-autonomous` agent level. These allow rules *keep the door open* — even if the agent is told not to use AppData, the permission system *would* allow it.
|
||||
4. **`conductor/tier2/agents/tier2-autonomous.md`** — explicitly tells the agent "Use `C:\Users\Ed\AppData\Local\manual_slop\tier2\` for all scratch / audit-output / temp files." (Line 47)
|
||||
5. **`conductor/tier2/commands/tier-2-auto-execute.md`** — same instruction at line 46.
|
||||
6. **`scripts/tier2/setup_tier2_clone.ps1:122-133`** — creates `C:\Users\Ed\AppData\Local\manual_slop\tier2\` and `C:\Users\Ed\AppData\Local\manual_slop\tier2_failures\` with restricted ACLs on bootstrap.
|
||||
7. **`scripts/tier2/run_tier2_sandboxed.ps1:20-21,77`** — references the AppData dirs and sets ACLs on them.
|
||||
8. **`docs/guide_tier2_autonomous.md`** — 4 explicit AppData references (lines 24, 72, 119, 128).
|
||||
9. **`conductor/workflow.md:386`** — hard bans table says "File access outside Tier 2 clone + app-data dir."
|
||||
10. **`scripts/tier2/write_track_completion_report.py:262,264`** — writes the AppData paths into the generated completion report.
|
||||
11. **`tests/test_tier2_slash_command_spec.py:91`** — asserts `'AppData\\Local\\manual_slop\\tier2' in content` (the test *requires* the agent prompt to reference AppData; this is the regression we are now reversing).
|
||||
12. **`tests/test_no_temp_writes.py:33`** — the failure-message string still suggests `C:\Users\Ed\AppData\Local\manual_slop\tier2\` as the fix target.
|
||||
|
||||
### Root Cause
|
||||
|
||||
The `tier2_autonomous_sandbox_20260616` track (shipped 2026-06-16) chose AppData because (a) it's outside the project tree so it doesn't pollute git, and (b) Windows restricted tokens can have explicit ACLs applied to AppData subdirs while keeping the rest of the user profile accessible. The trade-off was never questioned because Tier 2 was working.
|
||||
|
||||
On 2026-06-17, the agent attempted to write an audit JSON to `C:\Users\Ed\AppData\Local\Temp\` (the wrong AppData path — the system Temp, not the manual_slop one). The OpenCode permission system denied it because `*AppData\Local\Temp\*` was in the bash deny list, but the agent was confused because the *prompt* said "use AppData" and the *allowlist* said "AppData/Local/manual_slop/tier2/ is OK." The 2026-06-17 fix added the Temp deny rule and the AppData instruction to the prompt — but the underlying assumption (AppData is fine) was still baked in.
|
||||
|
||||
On 2026-06-18, the user issued the directive: **"NEVER USE APPDATA."** This is a stronger rule than the 2026-06-17 fix. The Tier 2 sandbox must stop treating AppData as a scratch space, period.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Zero AppData references in Tier 2 conventions.** The agent prompt, slash command, user guide, and OpenCode JSON must never say "use C:\Users\Ed\AppData\..." for any purpose.
|
||||
2. **Default state location = inside the clone.** `scripts/tier2/state/<track>/state.json` (relative to the clone root, computed via `Path.cwd()` when the agent runs).
|
||||
3. **Default failure-report location = inside the clone.** `scripts/tier2/failures/<track>_<utc-ts>.md` and `scripts/tier2/failures/<track>.STOPPED`.
|
||||
4. **Permission system refuses AppData.** OpenCode JSON `read`/`write` must not allowlist any `C:\Users\Ed\AppData\...` path. The deny rule for `*AppData\Local\Temp\*` stays; we add `*AppData\*` deny rules as a belt-and-suspenders.
|
||||
5. **Bootstrap does not create AppData dirs.** `setup_tier2_clone.ps1` and `run_tier2_sandboxed.ps1` no longer reference AppData.
|
||||
6. **Tests assert the new behavior.** `tests/test_tier2_slash_command_spec.py` and `tests/test_no_temp_writes.py` are updated to assert no AppData references in the agent prompt / fix messages.
|
||||
7. **Backward-compatible env-var escape hatch.** The existing `TIER2_STATE_DIR` / `TIER2_FAILURES_DIR` env-var overrides are preserved (still honored if set), but the *default* moves inside the clone.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
**FR1. State location moves inside the clone.**
|
||||
- `scripts/tier2/failcount.py:_state_dir` returns `Path.cwd() / "scripts" / "tier2" / "state" / track_name` by default.
|
||||
- `TIER2_STATE_DIR` env-var override is preserved.
|
||||
- `run_track.py:run_init` does `os.chdir(repo_path)` before calling `save_state` so `Path.cwd()` resolves to the clone root.
|
||||
|
||||
**FR2. Failure-report location moves inside the clone.**
|
||||
- `scripts/tier2/write_report.py:_failures_dir` returns `Path.cwd() / "scripts" / "tier2" / "failures"` by default.
|
||||
- `TIER2_FAILURES_DIR` env-var override is preserved.
|
||||
- `run_track.py:run_report` does `os.chdir(repo_path)` before calling `write_failure_report`.
|
||||
|
||||
**FR3. OpenCode permission JSON removes AppData allow rules.**
|
||||
- `conductor/tier2/opencode.json.fragment`: top-level and `tier2-autonomous` agent — `read`/`write` allow rules for `C:\Users\Ed\AppData\Local\manual_slop\tier2\**` and `C:\Users\Ed\AppData\Local\manual_slop\tier2_failures\**` are removed.
|
||||
- The existing `*AppData\Local\Temp\*` bash deny rule stays.
|
||||
- A new `*AppData\*` bash deny rule is added (belt-and-suspenders — the OpenCode `*` deny already blocks AppData reads, but a shell command like `> C:\Users\Ed\AppData\Local\foo.txt` was previously allowed because the bash `*` was set to `allow` at the agent level; tightening to `*` deny is too restrictive, so the targeted deny on `*AppData\*` is the surgical fix).
|
||||
|
||||
**FR4. Agent prompt and slash command say "NEVER USE APPDATA".**
|
||||
- `conductor/tier2/agents/tier2-autonomous.md` "Temp files" convention replaced with: "All scratch, state, and audit-output files MUST live inside the Tier 2 clone (`scripts/tier2/state/`, `scripts/tier2/failures/`, `scripts/tier2/artifacts/<track>/`). The `C:\Users\Ed\AppData\...` tree is OFF-LIMITS for any read, write, or shell command. This is enforced by the OpenCode `*AppData\*` deny rule; a violation will halt the run."
|
||||
- `conductor/tier2/commands/tier-2-auto-execute.md` "Conventions" section: same update.
|
||||
|
||||
**FR5. Bootstrap scripts stop creating AppData dirs.**
|
||||
- `scripts/tier2/setup_tier2_clone.ps1`: remove `$AppDataDir` / `$AppDataFailuresDir` variables and the `New-Item` / `Set-Acl` calls.
|
||||
- `scripts/tier2/run_tier2_sandboxed.ps1`: same.
|
||||
|
||||
**FR6. Tests updated.**
|
||||
- `tests/test_tier2_slash_command_spec.py:test_agent_denies_temp_writes` — flipped assertion: the agent prompt must NOT contain `AppData\Local\manual_slop\tier2` and MUST contain `scripts/tier2/state` or `scripts/tier2/failures`.
|
||||
- `tests/test_tier2_slash_command_spec.py:test_command_denies_temp_writes` — same flip (the slash command prompt has the same convention).
|
||||
- `tests/test_no_temp_writes.py` docstring + fix message: replace the AppData suggestion with `scripts/tier2/state/` / `scripts/tier2/failures/`.
|
||||
|
||||
**FR7. User guide updated.**
|
||||
- `docs/guide_tier2_autonomous.md`: 4 AppData references replaced with the new inside-clone locations. The "Verify the sandbox" checklist's `<app-data>` reference is removed.
|
||||
|
||||
**FR8. Hard bans table updated.**
|
||||
- `conductor/workflow.md:386`: "File access outside Tier 2 clone + app-data dir" → "File access outside Tier 2 clone (AppData, Temp, Documents, etc. all denied)."
|
||||
|
||||
**FR9. Completion report writer updated.**
|
||||
- `scripts/tier2/write_track_completion_report.py`: replace the 2 AppData path strings with the new `scripts/tier2/state/...` / `scripts/tier2/failures/...` paths.
|
||||
|
||||
**FR10. .gitignore updated.**
|
||||
- `scripts/tier2/state/` and `scripts/tier2/failures/` added (track-isolated scratch, must not be committed).
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **No regressions:** all existing failcount and report-writer tests pass after the path changes. The existing `TIER2_STATE_DIR` / `TIER2_FAILURES_DIR` env-var tests (`tests/test_failcount.py:176,190,198` and `tests/test_tier2_report_writer.py:25,33,40,71`) continue to pass — they monkeypatch the env var, which overrides the default.
|
||||
- **CLI ergonomics:** `scripts/tier2/run_track.py` continues to take `--repo-path` (default `.`). The `os.chdir(repo_path)` call is silent and idempotent.
|
||||
- **The in-flight Tier 2 run is NOT broken by this change** — the Tier 2 clone at `C:\projects\manual_slop_tier2\` still has the old config until re-bootstrapped. The user's existing run for `live_gui_test_fixes_20260618` continues to use AppData as it was bootstrapped.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
- **`docs/guide_tier2_autonomous.md`** — the user-facing Tier 2 sandbox guide. Sections 1 (bootstrap), 5 (the 4 hard bans), 7 (the failure report), and Troubleshooting are all touched.
|
||||
- **`conductor/workflow.md` §"Tier 2 Autonomous Sandbox" (lines 365-396)** — the convention-level rules and the 3-layer enforcement table. The "Hard bans" row is updated.
|
||||
- **`conductor/code_styleguides/workspace_paths.md`** — the principle "test workspaces live in the project tree under `tests/artifacts/`" extends naturally to "Tier 2 scratch lives in the project tree under `scripts/tier2/state/` and `scripts/tier2/failures/`." We cite this principle in the spec; we don't modify the styleguide (it's about *test* workspaces, not Tier 2 scratch).
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Re-bootstrap of the live Tier 2 clone (`C:\projects\manual_slop_tier2\`). The user re-runs `pwsh -File scripts/tier2/setup_tier2_clone.ps1` after this track merges.
|
||||
- Migration of existing state from `C:\Users\Ed\AppData\Local\manual_slop\tier2\...` into `scripts/tier2/state/...`. Any in-flight run's state is discarded on the next re-bootstrap.
|
||||
- Repo-wide LF normalization (a separate future track).
|
||||
- Tier 2 audit script (`scripts/audit_no_temp_writes.py`) changes — it already correctly scans for `%TEMP%` patterns; the AppData path strings in its docstring are updated as part of FR6 (the test fix-message change).
|
||||
@@ -1,52 +0,0 @@
|
||||
# Track state for tier2_no_appdata_20260618
|
||||
# Updated by Tier 2 Tech Lead as tasks complete
|
||||
|
||||
[meta]
|
||||
track_id = "tier2_no_appdata_20260618"
|
||||
name = "Tier 2 Sandbox - Move State/Failures Off AppData"
|
||||
status = "completed"
|
||||
current_phase = "complete"
|
||||
last_updated = "2026-06-18"
|
||||
|
||||
[blocked_by]
|
||||
# No blockers. The track can start immediately.
|
||||
|
||||
[blocks]
|
||||
# No downstream blocks. The user's re-bootstrap of the live Tier 2 clone is a manual action.
|
||||
|
||||
[phases]
|
||||
phase_1 = { status = "pending", checkpointsha = "", name = "Move the default state and failure-report paths" }
|
||||
phase_2 = { status = "pending", checkpointsha = "", name = "Update OpenCode permissions and agent/command prompts" }
|
||||
phase_3 = { status = "pending", checkpointsha = "", name = "Update bootstrap scripts" }
|
||||
phase_4 = { status = "pending", checkpointsha = "", name = "Update tests" }
|
||||
phase_5 = { status = "pending", checkpointsha = "", name = "Update user-facing docs and workflow" }
|
||||
phase_6 = { status = "pending", checkpointsha = "", name = "Conductor verification" }
|
||||
|
||||
[tasks]
|
||||
t1_1 = { status = "pending", commit_sha = "", description = "Update scripts/tier2/failcount.py:_state_dir default to scripts/tier2/state/<track>/" }
|
||||
t1_2 = { status = "pending", commit_sha = "", description = "Update scripts/tier2/write_report.py:_failures_dir default to scripts/tier2/failures/" }
|
||||
t1_3 = { status = "pending", commit_sha = "", description = "scripts/tier2/run_track.py: chdir to repo_path before state/report calls" }
|
||||
t1_4 = { status = "pending", commit_sha = "", description = "Add scripts/tier2/state/ and scripts/tier2/failures/ to .gitignore" }
|
||||
t2_1 = { status = "pending", commit_sha = "", description = "conductor/tier2/opencode.json.fragment: remove AppData allow rules from read/write" }
|
||||
t2_2 = { status = "pending", commit_sha = "", description = "conductor/tier2/opencode.json.fragment: add *AppData\\* bash deny rule" }
|
||||
t2_3 = { status = "pending", commit_sha = "", description = "conductor/tier2/agents/tier2-autonomous.md: replace AppData convention with inside-clone" }
|
||||
t2_4 = { status = "pending", commit_sha = "", description = "conductor/tier2/commands/tier-2-auto-execute.md: replace AppData paths with inside-clone paths" }
|
||||
t3_1 = { status = "pending", commit_sha = "", description = "scripts/tier2/setup_tier2_clone.ps1: stop creating AppData dirs" }
|
||||
t3_2 = { status = "pending", commit_sha = "", description = "scripts/tier2/run_tier2_sandboxed.ps1: remove AppData dir references" }
|
||||
t4_1 = { status = "pending", commit_sha = "", description = "tests/test_tier2_slash_command_spec.py: assert NO AppData refs in agent prompt" }
|
||||
t4_2 = { status = "pending", commit_sha = "", description = "tests/test_tier2_slash_command_spec.py: assert NO AppData refs in command prompt" }
|
||||
t4_3 = { status = "pending", commit_sha = "", description = "tests/test_no_temp_writes.py: replace AppData refs in docstring + fix message" }
|
||||
t5_1 = { status = "pending", commit_sha = "", description = "docs/guide_tier2_autonomous.md: replace AppData paths with inside-clone paths" }
|
||||
t5_2 = { status = "pending", commit_sha = "", description = "conductor/workflow.md hard bans table: AppData denied (no exception)" }
|
||||
t5_3 = { status = "pending", commit_sha = "", description = "scripts/tier2/write_track_completion_report.py: use inside-clone paths in output" }
|
||||
t6_1 = { status = "pending", commit_sha = "", description = "Run targeted test batches (test_failcount, test_tier2_report_writer, test_tier2_slash_command_spec, test_no_temp_writes)" }
|
||||
t6_2 = { status = "pending", commit_sha = "", description = "Run scripts/audit_no_temp_writes.py --strict" }
|
||||
t6_3 = { status = "pending", commit_sha = "", description = "Register the track in conductor/tracks.md" }
|
||||
|
||||
[verification]
|
||||
phase_1_complete = false
|
||||
phase_2_complete = false
|
||||
phase_3_complete = false
|
||||
phase_4_complete = false
|
||||
phase_5_complete = false
|
||||
phase_6_complete = false
|
||||
Reference in New Issue
Block a user