Compare commits

...

13 Commits

92 changed files with 514 additions and 7670 deletions

View File

@@ -35,24 +35,26 @@ The **MMA (Multi-Model Agent)** system decomposes epics into tracks, tracks into
## Module Map
| File | Lines | Role |
|---|---|---|
| `gui_2.py` | ~3080 | Primary ImGui interface — App class, frame-sync, HITL dialogs |
| `ai_client.py` | ~1800 | Multi-provider LLM abstraction (Gemini, Anthropic, DeepSeek, Gemini CLI) |
| `mcp_client.py` | ~870 | 26 MCP tools with filesystem sandboxing and tool dispatch |
| `api_hooks.py` | ~330 | HookServer — REST API for external automation on `:8999` |
| `api_hook_client.py` | ~245 | Python client for the Hook API (used by tests and external tooling) |
| `multi_agent_conductor.py` | ~250 | ConductorEngine — Tier 2 orchestration loop with DAG execution |
| `conductor_tech_lead.py` | ~100 | Tier 2 ticket generation from track briefs |
| `dag_engine.py` | ~100 | TrackDAG (dependency graph) + ExecutionEngine (tick-based state machine) |
| `models.py` | ~100 | Ticket, Track, WorkerContext dataclasses |
| `events.py` | ~89 | EventEmitter, AsyncEventQueue, UserRequestEvent |
| `project_manager.py` | ~300 | TOML config persistence, discussion management, track state |
| `session_logger.py` | ~200 | JSON-L + markdown audit trails (comms, tools, CLI, hooks) |
| `shell_runner.py` | ~100 | PowerShell execution with timeout, env config, QA callback |
| `file_cache.py` | ~150 | ASTParser (tree-sitter) — skeleton and curated views |
| `summarize.py` | ~120 | Heuristic file summaries (imports, classes, functions) |
| `outline_tool.py` | ~80 | Hierarchical code outline via stdlib `ast` |
Core implementation resides in the `src/` directory.
| File | Role |
|---|---|
| `src/gui_2.py` | Primary ImGui interface — App class, frame-sync, HITL dialogs |
| `src/ai_client.py` | Multi-provider LLM abstraction (Gemini, Anthropic, DeepSeek, Gemini CLI) |
| `src/mcp_client.py` | 26 MCP tools with filesystem sandboxing and tool dispatch |
| `src/api_hooks.py` | HookServer — REST API for external automation on `:8999` |
| `src/api_hook_client.py` | Python client for the Hook API (used by tests and external tooling) |
| `src/multi_agent_conductor.py` | ConductorEngine — Tier 2 orchestration loop with DAG execution |
| `src/conductor_tech_lead.py` | Tier 2 ticket generation from track briefs |
| `src/dag_engine.py` | TrackDAG (dependency graph) + ExecutionEngine (tick-based state machine) |
| `src/models.py` | Ticket, Track, WorkerContext dataclasses |
| `src/events.py` | EventEmitter, AsyncEventQueue, UserRequestEvent |
| `src/project_manager.py` | TOML config persistence, discussion management, track state |
| `src/session_logger.py` | JSON-L + markdown audit trails (comms, tools, CLI, hooks) |
| `src/shell_runner.py` | PowerShell execution with timeout, env config, QA callback |
| `src/file_cache.py` | ASTParser (tree-sitter) — skeleton and curated views |
| `src/summarize.py` | Heuristic file summaries (imports, classes, functions) |
| `src/outline_tool.py` | Hierarchical code outline via stdlib `ast` |
---
@@ -89,8 +91,8 @@ api_key = "YOUR_KEY"
### Running
```powershell
uv run gui_2.py # Normal mode
uv run gui_2.py --enable-test-hooks # With Hook API on :8999
uv run sloppy.py # Normal mode
uv run sloppy.py --enable-test-hooks # With Hook API on :8999
```
### Running Tests

View File

@@ -44,7 +44,7 @@ For deep implementation details when planning or implementing tracks, consult `d
- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows.
- **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode.
- **Structured Log Taxonomy:** Automated session-based log organization into `logs/sessions/`, `logs/agents/`, and `logs/errors/`. Includes a dedicated GUI panel for monitoring and manual whitelisting. Features an intelligent heuristic-based pruner that automatically cleans up insignificant logs older than 24 hours while preserving valuable sessions.
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
- **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis.
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
- **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid).

View File

@@ -37,7 +37,7 @@
- **psutil:** For system and process monitoring (CPU/Memory telemetry).
- **uv:** An extremely fast Python package and project manager.
- **pytest:** For unit and integration testing, leveraging custom fixtures for live GUI verification.
- **Taxonomy & Artifacts:** Enforces a clean root by redirecting session logs to `logs/sessions/`, sub-agent logs to `logs/agents/`, and error logs to `logs/errors/`. Temporary test data and test logs are siloed in `tests/artifacts/` and `tests/logs/`.
- **Taxonomy & Artifacts:** Enforces a clean root by organizing core implementation into a `src/` directory, and redirecting session logs to `logs/sessions/`, sub-agent logs to `logs/agents/`, and error logs to `logs/errors/`. Temporary test data and test logs are siloed in `tests/artifacts/` and `tests/logs/`.
- **ApiHookClient:** A dedicated IPC client for automated GUI interaction and state inspection.
- **mma-exec / mma.ps1:** Python-based execution engine and PowerShell wrapper for managing the 4-Tier MMA hierarchy and automated documentation mapping.
- **dag_engine.py:** A native Python utility implementing `TrackDAG` and `ExecutionEngine` for dependency resolution, cycle detection, transitive blocking propagation, and programmable task execution loops.

View File

@@ -8,10 +8,10 @@ This file tracks all major tracks for the project. Each track has its own detail
*The following tracks MUST be executed in this exact order to safely resolve tech debt before feature development.*
1. [ ] **Track: Codebase Migration to `src` & Cleanup**
1. [x] **Track: Codebase Migration to `src` & Cleanup**
*Link: [./tracks/codebase_migration_20260302/](./tracks/codebase_migration_20260302/)*
2. [ ] **Track: GUI Decoupling & Controller Architecture**
2. [~] **Track: GUI Decoupling & Controller Architecture**
*Link: [./tracks/gui_decoupling_controller_20260302/](./tracks/gui_decoupling_controller_20260302/)*
3. [ ] **Track: Hook API UI State Verification**

View File

@@ -1,54 +1,22 @@
# Implementation Plan: Codebase Migration to `src` & Cleanup (codebase_migration_20260302)
## Phase 1: Unused File Identification & Removal
- [ ] Task: Initialize MMA Environment `activate_skill mma-orchestrator`
- [ ] Task: Audit Codebase for Dead Files
- [ ] WHERE: Project root
- [ ] WHAT: Run `py_find_usages` or grep on suspected unused files to verify they are not referenced by `gui_2.py`, `tests/`, `simulation/`, or core config files.
- [ ] HOW: Gather a list of unused files.
- [ ] SAFETY: Do not delete files referenced in `.toml` files or Github action workflows.
- [ ] Task: Delete Unused Files
- [ ] WHERE: Project root
- [ ] WHAT: Use `run_powershell` with `Remove-Item` to delete the identified unused files.
- [ ] HOW: Explicitly list and delete them.
- [ ] SAFETY: Stage deletions to Git carefully.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Unused File Identification & Removal' (Protocol in workflow.md)
- [x] Task: Initialize MMA Environment `activate_skill mma-orchestrator`
- [x] Task: Audit Codebase for Dead Files (1eb9d29)
- [x] Task: Delete Unused Files (1eb9d29)
- [-] Task: Conductor - User Manual Verification 'Phase 1: Unused File Identification & Removal' (SKIPPED)
## Phase 2: Directory Restructuring & Migration
- [ ] Task: Create `src/` Directory
- [ ] WHERE: Project root
- [ ] WHAT: Create the `src/` directory. Add an empty `__init__.py` to make it a package.
- [ ] HOW: `New-Item -ItemType Directory src; New-Item src/__init__.py`.
- [ ] SAFETY: None.
- [ ] Task: Move Application Files to `src/`
- [ ] WHERE: Project root
- [ ] WHAT: Move core `.py` files (`gui_2.py`, `ai_client.py`, `mcp_client.py`, `shell_runner.py`, `project_manager.py`, `events.py`, etc.) into `src/`.
- [ ] HOW: Use `git mv` via `run_powershell` or standard `Move-Item`.
- [ ] SAFETY: Preserve git history of these files.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Directory Restructuring & Migration' (Protocol in workflow.md)
- [x] Task: Create `src/` Directory
- [x] Task: Move Application Files to `src/`
- [x] Task: Conductor - User Manual Verification 'Phase 2: Directory Restructuring & Migration' (Checkpoint: 24f385e)
## Phase 3: Entry Point & Import Resolution
- [ ] Task: Create `sloppy.py` Entry Point
- [ ] WHERE: Project root (`sloppy.py`)
- [ ] WHAT: Create the script to act as the primary launch point. It should import `App` from `src.gui_2` and pass CLI args.
- [ ] HOW: Write a standard Python script wrapper.
- [ ] SAFETY: Ensure it correctly propagates `sys.argv`.
- [ ] Task: Resolve Absolute and Relative Imports
- [ ] WHERE: `src/*.py`, `tests/*.py`, `simulation/*.py`
- [ ] WHAT: Update import statements. E.g., `import gui_2` becomes `from src import gui_2` or adjust `sys.path.append` in tests.
- [ ] HOW: Surgical string replacements. Ensure `pytest` can still find fixtures and test modules.
- [ ] SAFETY: Run `uv run pytest` to aggressively check for `ModuleNotFoundError`s.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Entry Point & Import Resolution' (Protocol in workflow.md)
- [x] Task: Create `sloppy.py` Entry Point (c102392)
- [x] Task: Resolve Absolute and Relative Imports (c102392)
- [x] Task: Conductor - User Manual Verification 'Phase 3: Entry Point & Import Resolution' (Checkpoint: 24f385e)
## Phase 4: Final Validation & Documentation
- [ ] Task: Full Test Suite Validation
- [ ] WHERE: Project root
- [ ] WHAT: Run `uv run pytest`. Fix any remaining path resolution issues for logs, artifacts, and configs.
- [ ] HOW: Verify 100% pass rate.
- [ ] SAFETY: Artifacts must still be written to `tests/artifacts/`.
- [ ] Task: Update Core Documentation
- [ ] WHERE: `Readme.md`, `docs/`, `conductor/tech-stack.md`
- [ ] WHAT: Document `sloppy.py` as the new entry point. Document the `src/` directory layout.
- [ ] HOW: Surgical text replacement.
- [ ] SAFETY: Accurate representation of new structure.
- [x] Task: Full Test Suite Validation (ea5bb4e)
- [x] Task: Update Core Documentation (ea5bb4e)
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Validation & Documentation' (Protocol in workflow.md)

View File

@@ -1,17 +1,9 @@
# Implementation Plan: GUI Decoupling & Controller Architecture (gui_decoupling_controller_20260302)
## Phase 1: Controller Skeleton & State Migration
- [ ] Task: Initialize MMA Environment `activate_skill mma-orchestrator`
- [ ] Task: Create `app_controller.py` Skeleton
- [ ] WHERE: `app_controller.py` (New file)
- [ ] WHAT: Create the `AppController` class. Initialize basic state structures (logs, metrics, flags).
- [ ] HOW: Standard class definition.
- [ ] SAFETY: Do not break existing GUI yet.
- [ ] Task: Migrate Data State from GUI
- [ ] WHERE: `gui_2.py:__init__` and `app_controller.py`
- [ ] WHAT: Move variables like `_comms_log`, `_tool_log`, `mma_streams`, `active_tickets` to the controller.
- [ ] HOW: Update GUI to reference `self.controller.mma_streams` instead of `self.mma_streams`.
- [ ] SAFETY: Search and replace carefully; use `py_check_syntax`.
- [x] Task: Initialize MMA Environment `activate_skill mma-orchestrator` [d0009bb]
- [x] Task: Create `app_controller.py` Skeleton [d0009bb]
- [x] Task: Migrate Data State from GUI [d0009bb]
- [ ] Task: Conductor - User Manual Verification 'Phase 1: State Migration' (Protocol in workflow.md)
## Phase 2: Logic & Background Thread Migration

View File

@@ -8,6 +8,28 @@
Manual Slop solves a single tension: **AI reasoning is high-latency and non-deterministic; GUI interaction must be low-latency and responsive.** The engine enforces strict decoupling between three thread domains so that multi-second LLM calls never block the render loop, and every AI-generated payload passes through a human-auditable gate before execution.
## Project Structure
The codebase is organized into a `src/` layout to separate implementation from configuration and artifacts.
```
manual_slop/
├── conductor/ # Conductor tracks, specs, and plans
├── docs/ # Deep-dive architectural documentation
├── logs/ # Session logs, agent traces, and errors
├── scripts/ # Build, migration, and IPC bridge scripts
├── src/ # Core Python implementation
│ ├── ai_client.py # LLM provider abstraction
│ ├── gui_2.py # Main ImGui application
│ ├── mcp_client.py # MCP tool implementation
│ └── ... # Other core modules
├── tests/ # Pytest suite and simulation fixtures
├── simulation/ # Workflow and agent simulation logic
├── sloppy.py # Primary application entry point
├── config.toml # Global application settings
└── manual_slop.toml # Project-specific configuration
```
---
## Thread Domains

View File

@@ -1,17 +0,0 @@
role = "tier3-worker"
prompt = """FIX DeepSeek implementation in ai_client.py.
CONTEXT:
Several tests in @tests/test_deepseek_provider.py are failing (returning '(No text returned by the model)') because the current implementation of '_send_deepseek' in @ai_client.py forces 'stream=True' and expects SSE format, but the test mocks provide standard JSON responses.
TASK:
1. Modify '_send_deepseek' in @ai_client.py to handle the response correctly whether it is a stream or a standard JSON response.
- You should probably determine this based on the 'stream' value in the payload (which is currently hardcoded to True, but the implementation should be flexible).
- If 'stream' is True, use the iter_lines() logic to aggregate chunks.
- If 'stream' is False, use resp.json() to get the content.
2. Fix the 'NameError: name 'data' is not defined' and ensure 'usage' is correctly extracted.
3. Ensure 'full_content', 'full_reasoning' (thinking tags), and 'tool_calls' are correctly captured and added to the conversation history in both modes.
4. Ensure all tests in @tests/test_deepseek_provider.py pass.
OUTPUT: Provide the raw Python code for the modified '_send_deepseek' function."""
docs = ["ai_client.py", "tests/test_deepseek_provider.py"]

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +0,0 @@
# gemini.py
from __future__ import annotations
import tomllib
from typing import Any
from google import genai
_client: genai.Client | None = None
_chat: Any = None
def _load_key() -> str:
with open("credentials.toml", "rb") as f:
return tomllib.load(f)["gemini"]["api_key"]
def _ensure_client() -> None:
global _client
if _client is None:
_client = genai.Client(api_key=_load_key())
def _ensure_chat() -> None:
global _chat
if _chat is None:
_ensure_client()
_chat = _client.chats.create(model="gemini-2.0-flash")
def send(md_content: str, user_message: str) -> str:
global _chat
_ensure_chat()
full_message = f"<context>\n{md_content}\n</context>\n\n{user_message}"
response = _chat.send_message(full_message)
return response.text
def reset_session() -> None:
global _client, _chat
_client = None
_chat = None

View File

@@ -1,2 +0,0 @@
@echo off
uv run python scripts/tool_call.py get_file_summary

Binary file not shown.

Binary file not shown.

View File

@@ -8,5 +8,5 @@ active = "main"
[discussions.main]
git_commit = ""
last_updated = "2026-03-04T09:44:10"
last_updated = "2026-03-04T10:09:06"
history = []

View File

@@ -1,10 +0,0 @@
role = "tier3-worker"
prompt = """Implement strict type hints for ALL functions and methods in @gui_2.py.
1. Use specific types (e.g., dict[str, Any], list[str], Union[str, Path], etc.) for arguments and returns.
2. Maintain the 'AI-Optimized' style: 1-space indentation, NO blank lines within function bodies, and maximum 1 blank line between definitions.
3. Since this file is very large, you MUST use surgical tools (discovered_tool_py_update_definition, discovered_tool_py_set_signature, discovered_tool_py_set_var_declaration) to apply changes. Do NOT try to overwrite the entire file at once.
4. Do NOT change any logic.
5. Use discovered_tool_py_check_syntax after each major change to verify syntax.
6. Ensure 'from typing import Any, dict, list, Union, Optional, Callable' etc. are present.
7. Focus on completing the task efficiently without hitting timeouts."""
docs = ["gui_2.py", "conductor/workflow.md"]

View File

@@ -1,21 +0,0 @@
import subprocess
import sys
def test_type_hints() -> None:
files = ["project_manager.py", "session_logger.py"]
all_missing = []
for f in files:
print(f"Scanning {f}...")
result = subprocess.run(["uv", "run", "python", "scripts/type_hint_scanner.py", f], capture_output=True, text=True)
if result.stdout.strip():
print(f"Missing hints in {f}:\n{result.stdout}")
all_missing.append(f)
if all_missing:
print(f"FAILURE: Missing type hints in: {', '.join(all_missing)}")
sys.exit(1)
else:
print("SUCCESS: All functions have type hints.")
sys.exit(0)
if __name__ == "__main__":
test_type_hints()

View File

@@ -1,3 +0,0 @@
role = "tier3-worker"
prompt = "Read @ai_client.py and describe the current placeholder implementation of _send_deepseek. Just a one-sentence summary."
docs = ["ai_client.py"]

View File

@@ -1,31 +0,0 @@
Files with untyped items: 25
File NoRet Params Vars Total
-------------------------------------------------------------------------------------
./debug_ast.py 1 2 4 7
./tests/visual_mma_verification.py 0 0 4 4
./debug_ast_2.py 0 0 3 3
./scripts/cli_tool_bridge.py 1 0 1 2
./scripts/mcp_server.py 0 0 2 2
./tests/test_gui_diagnostics.py 0 0 2 2
./tests/test_gui_updates.py 0 0 2 2
./tests/test_layout_reorganization.py 0 0 2 2
./scripts/check_hints.py 0 0 1 1
./scripts/check_hints_v2.py 0 0 1 1
./scripts/claude_tool_bridge.py 0 0 1 1
./scripts/type_hint_scanner.py 1 0 0 1
./tests/mock_alias_tool.py 0 0 1 1
./tests/test_gemini_cli_adapter_parity.py 0 0 1 1
./tests/test_gui2_parity.py 0 0 1 1
./tests/test_gui2_performance.py 0 0 1 1
./tests/test_gui_performance_requirements.py 0 1 0 1
./tests/test_gui_stress_performance.py 0 1 0 1
./tests/test_hooks.py 0 1 0 1
./tests/test_live_workflow.py 0 1 0 1
./tests/test_track_state_persistence.py 0 1 0 1
./tests/verify_mma_gui_robust.py 0 0 1 1
./tests/visual_diag.py 0 0 1 1
./tests/visual_orchestration_verification.py 0 1 0 1
./tests/visual_sim_mma_v2.py 0 1 0 1
-------------------------------------------------------------------------------------
TOTAL 41

View File

@@ -3,10 +3,13 @@ import json
import logging
import os
# Add project root to sys.path so we can import api_hook_client
# Add project root and src/ to sys.path so we can import api_hook_client
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if project_root not in sys.path:
sys.path.append(project_root)
src_path = os.path.join(project_root, "src")
if src_path not in sys.path:
sys.path.append(src_path)
try:
from api_hook_client import ApiHookClient

View File

@@ -3,11 +3,14 @@ import json
import logging
import os
# Add project root to sys.path so we can import api_hook_client
# Add project root and src/ to sys.path so we can import api_hook_client
# This helps in cases where the script is run from different directories
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if project_root not in sys.path:
sys.path.append(project_root)
src_path = os.path.join(project_root, "src")
if src_path not in sys.path:
sys.path.append(src_path)
try:
from api_hook_client import ApiHookClient

View File

@@ -13,8 +13,10 @@ import asyncio
import os
import sys
# Add project root to sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# Add project root and src/ to sys.path
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, project_root)
sys.path.insert(0, os.path.join(project_root, "src"))
import mcp_client
import shell_runner

View File

@@ -7,8 +7,10 @@ import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# Add project root and src/ to sys.path
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(project_root)
sys.path.append(os.path.join(project_root, "src"))
try:
import mcp_client

View File

@@ -2,8 +2,10 @@ import json
import sys
import os
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# Add project root and src/ to sys.path
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(project_root)
sys.path.append(os.path.join(project_root, "src"))
try:
import mcp_client

20
scripts/update_paths.py Normal file
View File

@@ -0,0 +1,20 @@
import os
import glob
pattern = 'sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))'
replacement = pattern + '\nsys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))'
# Files to update
files = glob.glob("tests/*.py") + glob.glob("simulation/*.py") + glob.glob("scripts/*.py")
for file_path in files:
if not os.path.isfile(file_path):
continue
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if pattern in content and replacement not in content:
print(f"Updating {file_path}")
new_content = content.replace(pattern, replacement)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)

View File

@@ -4,6 +4,7 @@ import time
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient
from simulation.user_agent import UserSimAgent

View File

@@ -5,8 +5,10 @@ from typing import Any, Optional
from api_hook_client import ApiHookClient
from simulation.workflow_sim import WorkflowSimulator
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# Ensure project root and src/ are in path
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(project_root)
sys.path.append(os.path.join(project_root, "src"))
class BaseSimulation:
def __init__(self, client: ApiHookClient = None) -> None:

12
sloppy.py Normal file
View File

@@ -0,0 +1,12 @@
import sys
import os
# Add src to sys.path so we can import from it easily
project_root = os.path.dirname(os.path.abspath(__file__))
src_path = os.path.join(project_root, "src")
sys.path.insert(0, src_path)
from gui_2 import main
if __name__ == "__main__":
main()

276
src/app_controller.py Normal file
View File

@@ -0,0 +1,276 @@
import asyncio
import threading
import time
import sys
import os
from typing import Any, List, Dict, Optional, Tuple
from pathlib import Path
from src import events
from src import session_logger
from src import project_manager
from src.performance_monitor import PerformanceMonitor
from src.models import Track, Ticket, load_config, parse_history_entries, DISC_ROLES, AGENT_TOOL_NAMES
class AppController:
"""
The headless controller for the Manual Slop application.
Owns the application state and manages background services.
"""
def __init__(self):
# Initialize locks first to avoid initialization order issues
self._send_thread_lock: threading.Lock = threading.Lock()
self._disc_entries_lock: threading.Lock = threading.Lock()
self._pending_comms_lock: threading.Lock = threading.Lock()
self._pending_tool_calls_lock: threading.Lock = threading.Lock()
self._pending_history_adds_lock: threading.Lock = threading.Lock()
self._pending_gui_tasks_lock: threading.Lock = threading.Lock()
self._pending_dialog_lock: threading.Lock = threading.Lock()
self._api_event_queue_lock: threading.Lock = threading.Lock()
self.config: Dict[str, Any] = {}
self.project: Dict[str, Any] = {}
self.active_project_path: str = ""
self.project_paths: List[str] = []
self.active_discussion: str = "main"
self.disc_entries: List[Dict[str, Any]] = []
self.disc_roles: List[str] = []
self.files: List[str] = []
self.screenshots: List[str] = []
self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue()
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._loop_thread: Optional[threading.Thread] = None
self.tracks: List[Dict[str, Any]] = []
self.active_track: Optional[Track] = None
self.active_tickets: List[Dict[str, Any]] = []
self.mma_streams: Dict[str, str] = {}
self.mma_status: str = "idle"
self._tool_log: List[Dict[str, Any]] = []
self._comms_log: List[Dict[str, Any]] = []
self.session_usage: Dict[str, Any] = {
"input_tokens": 0,
"output_tokens": 0,
"cache_read_input_tokens": 0,
"cache_creation_input_tokens": 0,
"last_latency": 0.0
}
self.mma_tier_usage: Dict[str, Dict[str, Any]] = {
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
}
self.perf_monitor: PerformanceMonitor = PerformanceMonitor()
self._pending_gui_tasks: List[Dict[str, Any]] = []
# AI settings state
self._current_provider: str = "gemini"
self._current_model: str = "gemini-2.5-flash-lite"
self.temperature: float = 0.0
self.max_tokens: int = 8192
self.history_trunc_limit: int = 8000
# UI-related state moved to controller
self.ui_ai_input: str = ""
self.ui_disc_new_name_input: str = ""
self.ui_disc_new_role_input: str = ""
self.ui_epic_input: str = ""
self.ui_new_track_name: str = ""
self.ui_new_track_desc: str = ""
self.ui_new_track_type: str = "feature"
self.ui_conductor_setup_summary: str = ""
self.ui_last_script_text: str = ""
self.ui_last_script_output: str = ""
self.ui_new_ticket_id: str = ""
self.ui_new_ticket_desc: str = ""
self.ui_new_ticket_target: str = ""
self.ui_new_ticket_deps: str = ""
self.ui_output_dir: str = ""
self.ui_files_base_dir: str = ""
self.ui_shots_base_dir: str = ""
self.ui_project_git_dir: str = ""
self.ui_project_main_context: str = ""
self.ui_project_system_prompt: str = ""
self.ui_gemini_cli_path: str = "gemini"
self.ui_word_wrap: bool = True
self.ui_summary_only: bool = False
self.ui_auto_add_history: bool = False
self.ui_global_system_prompt: str = ""
self.ui_agent_tools: Dict[str, bool] = {}
self.available_models: List[str] = []
self.proposed_tracks: List[Dict[str, Any]] = []
self._show_track_proposal_modal: bool = False
self.ai_status: str = 'idle'
self.ai_response: str = ''
self.last_md: str = ''
self.last_md_path: Optional[Path] = None
self.last_file_items: List[Any] = []
self.send_thread: Optional[threading.Thread] = None
self.models_thread: Optional[threading.Thread] = None
self.show_windows: Dict[str, bool] = {}
self.show_script_output: bool = False
self.show_text_viewer: bool = False
self.text_viewer_title: str = ''
self.text_viewer_content: str = ''
self._pending_comms: List[Dict[str, Any]] = []
self._pending_tool_calls: List[Dict[str, Any]] = []
self._pending_history_adds: List[Dict[str, Any]] = []
self.perf_history: Dict[str, List[float]] = {'frame_time': [0.0]*100, 'fps': [0.0]*100, 'cpu': [0.0]*100, 'input_lag': [0.0]*100}
self._perf_last_update: float = 0.0
self._autosave_interval: float = 60.0
self._last_autosave: float = time.time()
# More state moved from App
self._ask_dialog_open: bool = False
self._ask_request_id: Optional[str] = None
self._ask_tool_data: Optional[Dict[str, Any]] = None
self.mma_step_mode: bool = False
self.active_tier: Optional[str] = None
self.ui_focus_agent: Optional[str] = None
self._pending_mma_approval: Optional[Dict[str, Any]] = None
self._mma_approval_open: bool = False
self._mma_approval_edit_mode: bool = False
self._mma_approval_payload: str = ""
self._pending_mma_spawn: Optional[Dict[str, Any]] = None
self._mma_spawn_open: bool = False
self._mma_spawn_edit_mode: bool = False
self._mma_spawn_prompt: str = ''
self._mma_spawn_context: str = ''
self._trigger_blink: bool = False
self._is_blinking: bool = False
self._blink_start_time: float = 0.0
self._trigger_script_blink: bool = False
self._is_script_blinking: bool = False
self._script_blink_start_time: float = 0.0
self._scroll_disc_to_bottom: bool = False
self._scroll_comms_to_bottom: bool = False
self._scroll_tool_calls_to_bottom: bool = False
self._gemini_cache_text: str = ""
self._last_stable_md: str = ''
self._token_stats: Dict[str, Any] = {}
self._token_stats_dirty: bool = False
self.ui_disc_truncate_pairs: int = 2
self.ui_auto_scroll_comms: bool = True
self.ui_auto_scroll_tool_calls: bool = True
self._show_add_ticket_form: bool = False
self._track_discussion_active: bool = False
self._tier_stream_last_len: Dict[str, int] = {}
self.is_viewing_prior_session: bool = False
self.prior_session_entries: List[Dict[str, Any]] = []
self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
self.ui_manual_approve: bool = False
def init_state(self):
"""Initializes the application state from configurations."""
self.config = load_config()
ai_cfg = self.config.get("ai", {})
self._current_provider = ai_cfg.get("provider", "gemini")
self._current_model = ai_cfg.get("model", "gemini-2.5-flash-lite")
self.temperature = ai_cfg.get("temperature", 0.0)
self.max_tokens = ai_cfg.get("max_tokens", 8192)
self.history_trunc_limit = ai_cfg.get("history_trunc_limit", 8000)
projects_cfg = self.config.get("projects", {})
self.project_paths = list(projects_cfg.get("paths", []))
self.active_project_path = projects_cfg.get("active", "")
self._load_active_project()
self.files = list(self.project.get("files", {}).get("paths", []))
self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
disc_sec = self.project.get("discussion", {})
self.disc_roles = list(disc_sec.get("roles", list(DISC_ROLES)))
self.active_discussion = disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
with self._disc_entries_lock:
self.disc_entries = parse_history_entries(disc_data.get("history", []), self.disc_roles)
# UI state
self.ui_output_dir = self.project.get("output", {}).get("output_dir", "./md_gen")
self.ui_files_base_dir = self.project.get("files", {}).get("base_dir", ".")
self.ui_shots_base_dir = self.project.get("screenshots", {}).get("base_dir", ".")
proj_meta = self.project.get("project", {})
self.ui_project_git_dir = proj_meta.get("git_dir", "")
self.ui_project_main_context = proj_meta.get("main_context", "")
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
self.ui_word_wrap = proj_meta.get("word_wrap", True)
self.ui_summary_only = proj_meta.get("summary_only", False)
self.ui_auto_add_history = disc_sec.get("auto_add", False)
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
_default_windows = {
"Context Hub": True,
"Files & Media": True,
"AI Settings": True,
"MMA Dashboard": True,
"Tier 1: Strategy": True,
"Tier 2: Tech Lead": True,
"Tier 3: Workers": True,
"Tier 4: QA": True,
"Discussion Hub": True,
"Operations Hub": True,
"Theme": True,
"Log Management": False,
"Diagnostics": False,
}
saved = self.config.get("gui", {}).get("show_windows", {})
self.show_windows = {k: saved.get(k, v) for k, v in _default_windows.items()}
agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
label = self.project.get("project", {}).get("name", "")
session_logger.open_session(label=label)
def _load_active_project(self) -> None:
"""Loads the active project configuration, with fallbacks."""
if self.active_project_path and Path(self.active_project_path).exists():
try:
self.project = project_manager.load_project(self.active_project_path)
return
except Exception as e:
print(f"Failed to load project {self.active_project_path}: {e}")
for pp in self.project_paths:
if Path(pp).exists():
try:
self.project = project_manager.load_project(pp)
self.active_project_path = pp
return
except Exception:
continue
self.project = project_manager.migrate_from_legacy_config(self.config)
name = self.project.get("project", {}).get("name", "project")
fallback_path = f"{name}.toml"
project_manager.save_project(self.project, fallback_path)
self.active_project_path = fallback_path
if fallback_path not in self.project_paths:
self.project_paths.append(fallback_path)
def start_services(self):
"""Starts background threads and async event loop."""
self._loop = asyncio.new_event_loop()
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
self._loop_thread.start()
def _run_event_loop(self):
"""Internal loop runner."""
asyncio.set_event_loop(self._loop)
self._loop.run_forever()
def stop_services(self):
"""Stops background threads and async event loop."""
if self._loop:
self._loop.call_soon_threadsafe(self._loop.stop)
if self._loop_thread:
self._loop_thread.join(timeout=2.0)

View File

@@ -32,7 +32,8 @@ from log_registry import LogRegistry
from log_pruner import LogPruner
import conductor_tech_lead
import multi_agent_conductor
from models import Track, Ticket
from models import Track, Ticket, DISC_ROLES, AGENT_TOOL_NAMES, CONFIG_PATH, load_config, parse_history_entries
from app_controller import AppController
from file_cache import ASTParser
from fastapi import FastAPI, Depends, HTTPException
@@ -40,14 +41,9 @@ from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel
from imgui_bundle import imgui, hello_imgui, immapp
CONFIG_PATH: Path = Path("config.toml")
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek"]
COMMS_CLAMP_CHARS: int = 300
def load_config() -> dict[str, Any]:
with open(CONFIG_PATH, "rb") as f:
return tomllib.load(f)
def save_config(config: dict[str, Any]) -> None:
with open(CONFIG_PATH, "wb") as f:
tomli_w.dump(config, f)
@@ -78,17 +74,6 @@ DIR_COLORS: dict[str, imgui.ImVec4] = {"OUT": C_OUT, "IN": C_IN}
KIND_COLORS: dict[str, imgui.ImVec4] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS}
HEAVY_KEYS: set[str] = {"message", "text", "script", "output", "content"}
DISC_ROLES: list[str] = ["User", "AI", "Vendor API", "System"]
AGENT_TOOL_NAMES: list[str] = [
"run_powershell", "read_file", "list_directory", "search_files", "get_file_summary",
"web_search", "fetch_url", "py_get_skeleton", "py_get_code_outline", "get_file_slice",
"py_get_definition", "py_get_signature", "py_get_class_summary", "py_get_var_declaration",
"get_git_diff", "py_find_usages", "py_get_imports", "py_check_syntax", "py_get_hierarchy",
"py_get_docstring", "get_tree", "get_ui_performance",
# Mutating tools — disabled by default
"set_file_slice", "py_update_definition", "py_set_signature", "py_set_var_declaration",
]
def truncate_entries(entries: list[dict[str, Any]], max_pairs: int) -> list[dict[str, Any]]:
if max_pairs <= 0:
return []
@@ -102,14 +87,6 @@ def truncate_entries(entries: list[dict[str, Any]], max_pairs: int) -> list[dict
return entries[i:]
return entries
def _parse_history_entries(history: list[str], roles: list[str] | None = None) -> list[dict[str, Any]]:
known = roles if roles is not None else DISC_ROLES
entries = []
for raw in history:
entry = project_manager.str_to_entry(raw, known)
entries.append(entry)
return entries
class ConfirmDialog:
def __init__(self, script: str, base_dir: str) -> None:
self._uid = str(uuid.uuid4())
@@ -172,170 +149,41 @@ class App:
"""The main ImGui interface orchestrator for Manual Slop."""
def __init__(self) -> None:
# Initialize locks first to avoid initialization order issues
self._send_thread_lock: threading.Lock = threading.Lock()
self._disc_entries_lock: threading.Lock = threading.Lock()
self._pending_comms_lock: threading.Lock = threading.Lock()
self._pending_tool_calls_lock: threading.Lock = threading.Lock()
self._pending_history_adds_lock: threading.Lock = threading.Lock()
self._pending_gui_tasks_lock: threading.Lock = threading.Lock()
self._pending_dialog_lock: threading.Lock = threading.Lock()
self._api_event_queue_lock: threading.Lock = threading.Lock()
# Initialize controller and delegate state
self.controller = AppController()
self.controller.init_state()
# Aliases for controller-owned locks
self._send_thread_lock = self.controller._send_thread_lock
self._disc_entries_lock = self.controller._disc_entries_lock
self._pending_comms_lock = self.controller._pending_comms_lock
self._pending_tool_calls_lock = self.controller._pending_tool_calls_lock
self._pending_history_adds_lock = self.controller._pending_history_adds_lock
self._pending_gui_tasks_lock = self.controller._pending_gui_tasks_lock
self._pending_dialog_lock = self.controller._pending_dialog_lock
self._api_event_queue_lock = self.controller._api_event_queue_lock
self.config: dict[str, Any] = load_config()
self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue()
self._loop: asyncio.AbstractEventLoop = asyncio.new_event_loop()
self._loop_thread: threading.Thread = threading.Thread(target=self._run_event_loop, daemon=True)
self._loop_thread.start()
ai_cfg = self.config.get("ai", {})
self._current_provider: str = ai_cfg.get("provider", "gemini")
self._current_model: str = ai_cfg.get("model", "gemini-2.5-flash-lite")
self.available_models: list[str] = []
self.temperature: float = ai_cfg.get("temperature", 0.0)
self.max_tokens: int = ai_cfg.get("max_tokens", 8192)
self.history_trunc_limit: int = ai_cfg.get("history_trunc_limit", 8000)
projects_cfg = self.config.get("projects", {})
self.project_paths: list[str] = list(projects_cfg.get("paths", []))
self.active_project_path: str = projects_cfg.get("active", "")
self.project: dict[str, Any] = {}
self.active_discussion: str = "main"
self._load_active_project()
self.files: list[str] = list(self.project.get("files", {}).get("paths", []))
self.screenshots: list[str] = list(self.project.get("screenshots", {}).get("paths", []))
disc_sec = self.project.get("discussion", {})
self.disc_roles: list[str] = list(disc_sec.get("roles", list(DISC_ROLES)))
self.active_discussion = disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
with self._disc_entries_lock:
self.disc_entries: list[dict[str, Any]] = _parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.ui_output_dir: str = self.project.get("output", {}).get("output_dir", "./md_gen")
self.ui_files_base_dir: str = self.project.get("files", {}).get("base_dir", ".")
self.ui_shots_base_dir: str = self.project.get("screenshots", {}).get("base_dir", ".")
proj_meta = self.project.get("project", {})
self.ui_project_git_dir: str = proj_meta.get("git_dir", "")
self.ui_project_main_context: str = proj_meta.get("main_context", "")
self.ui_project_system_prompt: str = proj_meta.get("system_prompt", "")
self.ui_gemini_cli_path: str = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
self.ui_word_wrap: bool = proj_meta.get("word_wrap", True)
self.ui_summary_only: bool = proj_meta.get("summary_only", False)
self.ui_auto_add_history: bool = disc_sec.get("auto_add", False)
self.ui_global_system_prompt: str = self.config.get("ai", {}).get("system_prompt", "")
self.ui_ai_input: str = ""
self.ui_disc_new_name_input: str = ""
self.ui_disc_new_role_input: str = ""
self.ui_epic_input: str = ""
self.proposed_tracks: list[dict[str, Any]] = []
self._show_track_proposal_modal: bool = False
self.ui_new_track_name: str = ""
self.ui_new_track_desc: str = ""
self.ui_new_track_type: str = "feature"
self.ui_conductor_setup_summary: str = ""
self.ui_last_script_text: str = ""
self.ui_last_script_output: str = ""
self.ai_status: str = "idle"
self.ai_response: str = ""
self.last_md: str = ""
self.last_md_path: Path | None = None
self.last_file_items: list[Any] = []
self.send_thread: threading.Thread | None = None
self.models_thread: threading.Thread | None = None
_default_windows = {
"Context Hub": True,
"Files & Media": True,
"AI Settings": True,
"MMA Dashboard": True,
"Tier 1: Strategy": True,
"Tier 2: Tech Lead": True,
"Tier 3: Workers": True,
"Tier 4: QA": True,
"Discussion Hub": True,
"Operations Hub": True,
"Theme": True,
"Log Management": False,
"Diagnostics": False,
}
saved = self.config.get("gui", {}).get("show_windows", {})
self.show_windows: dict[str, bool] = {k: saved.get(k, v) for k, v in _default_windows.items()}
self.show_script_output: bool = False
self.show_text_viewer: bool = False
self.text_viewer_title: str = ""
self.text_viewer_content: str = ""
self._pending_dialog: ConfirmDialog | None = None
self._pending_dialog_open: bool = False
self._pending_actions: dict[str, ConfirmDialog] = {}
self._pending_ask_dialog: bool = False
self._ask_dialog_open: bool = False
self._ask_request_id: str | None = None
self._ask_tool_data: dict[str, Any] | None = None
self.mma_step_mode: bool = False
self.active_track: Track | None = None
self.active_tickets: list[dict[str, Any]] = []
self.active_tier: str | None = None
self.ui_focus_agent: str | None = None
self.mma_status: str = "idle"
self._pending_mma_approval: dict[str, Any] | None = None
self._mma_approval_open: bool = False
self._mma_approval_edit_mode: bool = False
self._mma_approval_payload: str = ""
self._pending_mma_spawn: dict[str, Any] | None = None
self._mma_spawn_open: bool = False
self._mma_spawn_edit_mode: bool = False
self._mma_spawn_prompt: str = ''
self._mma_spawn_context: str = ''
self.mma_tier_usage: dict[str, dict[str, Any]] = {
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
}
self._tool_log: list[dict[str, Any]] = []
self._comms_log: list[dict[str, Any]] = []
self._pending_comms: list[dict[str, Any]] = []
self._pending_tool_calls: list[dict[str, Any]] = []
self._pending_history_adds: list[dict[str, Any]] = []
self._trigger_blink: bool = False
self._is_blinking: bool = False
self._blink_start_time: float = 0.0
self._trigger_script_blink: bool = False
self._is_script_blinking: bool = False
self._script_blink_start_time: float = 0.0
self._scroll_disc_to_bottom: bool = False
self._scroll_comms_to_bottom: bool = False
self._scroll_tool_calls_to_bottom: bool = False
self._pending_gui_tasks: list[dict[str, Any]] = []
self.session_usage: dict[str, Any] = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "last_latency": 0.0}
self._gemini_cache_text: str = ""
self._last_stable_md: str = ''
self._token_stats: dict[str, Any] = {}
self._token_stats_dirty: bool = False
self.ui_disc_truncate_pairs: int = 2
self.ui_auto_scroll_comms: bool = True
self.ui_auto_scroll_tool_calls: bool = True
agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
self.ui_agent_tools: dict[str, bool] = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
self.tracks: list[dict[str, Any]] = []
self._show_add_ticket_form: bool = False
self.ui_new_ticket_id: str = ""
self.ui_new_ticket_desc: str = ""
self.ui_new_ticket_target: str = ""
self.ui_new_ticket_deps: str = ""
self._track_discussion_active: bool = False
self.mma_streams: dict[str, str] = {}
self._tier_stream_last_len: dict[str, int] = {}
self.is_viewing_prior_session: bool = False
self.prior_session_entries: list[dict[str, Any]] = []
self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
self.ui_manual_approve: bool = False
self.perf_monitor: PerformanceMonitor = PerformanceMonitor()
self.perf_history: dict[str, list[float]] = {"frame_time": [0.0]*100, "fps": [0.0]*100, "cpu": [0.0]*100, "input_lag": [0.0]*100}
self._perf_last_update: float = 0.0
self._autosave_interval: float = 60.0
self._last_autosave: float = time.time()
label = self.project.get("project", {}).get("name", "")
session_logger.open_session(label=label)
self._prune_old_logs()
self._init_ai_and_hooks()
def __getattr__(self, name: str) -> Any:
if name != 'controller' and hasattr(self, 'controller') and hasattr(self.controller, name):
return getattr(self.controller, name)
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
def __setattr__(self, name: str, value: Any) -> None:
if name == 'controller':
super().__setattr__(name, value)
elif hasattr(self, 'controller') and hasattr(self.controller, name):
setattr(self.controller, name, value)
else:
super().__setattr__(name, value)
def _prune_old_logs(self) -> None:
"""Asynchronously prunes old insignificant logs on startup."""
@@ -638,33 +486,10 @@ class App:
def _cb_disc_create(self) -> None:
nm = self.ui_disc_new_name_input.strip()
if nm:
if nm:
self._create_discussion(nm)
self.ui_disc_new_name_input = ""
def _load_active_project(self) -> None:
if self.active_project_path and Path(self.active_project_path).exists():
try:
self.project = project_manager.load_project(self.active_project_path)
return
except Exception as e:
print(f"Failed to load project {self.active_project_path}: {e}")
for pp in self.project_paths:
if Path(pp).exists():
try:
self.project = project_manager.load_project(pp)
self.active_project_path = pp
return
except Exception:
continue
self.project = project_manager.migrate_from_legacy_config(self.config)
name = self.project.get("project", {}).get("name", "project")
fallback_path = f"{name}.toml"
project_manager.save_project(self.project, fallback_path)
self.active_project_path = fallback_path
if fallback_path not in self.project_paths:
self.project_paths.append(fallback_path)
def _switch_project(self, path: str) -> None:
if not Path(path).exists():
self.ai_status = f"project file not found: {path}"
@@ -690,7 +515,7 @@ class App:
self.active_discussion = disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.disc_entries = parse_history_entries(disc_data.get("history", []), self.disc_roles)
proj = self.project
self.ui_output_dir = proj.get("output", {}).get("output_dir", "./md_gen")
self.ui_files_base_dir = proj.get("files", {}).get("base_dir", ".")
@@ -734,7 +559,7 @@ class App:
track_history = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
if track_history:
with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(track_history, self.disc_roles)
self.disc_entries = parse_history_entries(track_history, self.disc_roles)
def _cb_load_track(self, track_id: str) -> None:
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
@@ -759,7 +584,7 @@ class App:
history = project_manager.load_track_history(track_id, self.ui_files_base_dir)
with self._disc_entries_lock:
if history:
self.disc_entries = _parse_history_entries(history, self.disc_roles)
self.disc_entries = parse_history_entries(history, self.disc_roles)
else:
self.disc_entries = []
self._recalculate_session_usage()
@@ -803,7 +628,7 @@ class App:
self._discussion_names_dirty = True
disc_data = discussions[name]
with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.disc_entries = parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.ai_status = f"discussion: {name}"
def _flush_disc_entries_to_project(self) -> None:
@@ -2568,7 +2393,7 @@ class App:
self._flush_disc_entries_to_project()
history_strings = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(history_strings, self.disc_roles)
self.disc_entries = parse_history_entries(history_strings, self.disc_roles)
self.ai_status = f"track discussion: {self.active_track.id}"
else:
self._flush_disc_entries_to_project()
@@ -3617,6 +3442,22 @@ class App:
def run(self) -> None:
"""Initializes the ImGui runner and starts the main application loop."""
self.controller.start_services()
self._loop = self.controller._loop
self._loop_thread = self.controller._loop_thread
if self._loop:
self._loop.call_soon_threadsafe(lambda: self._loop.create_task(self._process_event_queue()))
async def queue_fallback() -> None:
while True:
try:
self._process_pending_gui_tasks()
self._process_pending_history_adds()
except: pass
await asyncio.sleep(0.1)
self._loop.call_soon_threadsafe(lambda: self._loop.create_task(queue_fallback()))
if "--headless" in sys.argv:
print("Headless mode active")
self._fetch_models(self.current_provider)

View File

@@ -1,6 +1,33 @@
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
from datetime import datetime
from pathlib import Path
import tomllib
from src import project_manager
CONFIG_PATH: Path = Path('config.toml')
DISC_ROLES: list[str] = ['User', 'AI', 'Vendor API', 'System']
AGENT_TOOL_NAMES: list[str] = [
"run_powershell", "read_file", "list_directory", "search_files", "get_file_summary",
"web_search", "fetch_url", "py_get_skeleton", "py_get_code_outline", "get_file_slice",
"py_get_definition", "py_get_signature", "py_get_class_summary", "py_get_var_declaration",
"get_git_diff", "py_find_usages", "py_get_imports", "py_check_syntax", "py_get_hierarchy",
"py_get_docstring", "get_tree", "get_ui_performance",
# Mutating tools — disabled by default
"set_file_slice", "py_update_definition", "py_set_signature", "py_set_var_declaration",
]
def load_config() -> dict[str, Any]:
with open(CONFIG_PATH, "rb") as f:
return tomllib.load(f)
def parse_history_entries(history: list[str], roles: list[str] | None = None) -> list[dict[str, Any]]:
known = roles if roles is not None else DISC_ROLES
entries = []
for raw in history:
entry = project_manager.str_to_entry(raw, known)
entries.append(entry)
return entries
@dataclass
class Ticket:

View File

@@ -1,17 +0,0 @@
role = "tier3-worker"
prompt = """TASK: Implement streaming support for the DeepSeek provider in ai_client.py and add failing tests.
INSTRUCTIONS:
1. In @tests/test_deepseek_provider.py:
- Add a test function 'test_deepseek_streaming' that mocks a streaming API response using 'requests.post(..., stream=True)'.
- Use 'mock_response.iter_lines()' to simulate chunks of data.
- Assert that 'ai_client.send()' correctly aggregates these chunks into a single string.
2. In @ai_client.py:
- Modify the '_send_deepseek' function to use 'requests.post(..., stream=True)'.
- Implement a loop to iterate over the response lines using 'iter_lines()'.
- Aggregate the content from each chunk.
- Ensure the aggregated content is added to the history and returned by the function.
OUTPUT: Provide the raw Python code for the modified sections or the full files. No pleasantries."""
docs = ["conductor/workflow.md", "ai_client.py", "tests/test_deepseek_provider.py", "mcp_client.py"]

View File

@@ -12,8 +12,9 @@ from pathlib import Path
from typing import Generator, Any
from unittest.mock import patch
# Ensure project root is in path for imports
# Ensure project root and src/ are in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
# Import the App class after patching if necessary, but here we just need the type hint
from gui_2 import App
@@ -161,10 +162,10 @@ def app_instance() -> Generator[App, None, None]:
@pytest.fixture(scope="session")
def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
"""
Session-scoped fixture that starts gui_2.py with --enable-test-hooks.
Session-scoped fixture that starts sloppy.py with --enable-test-hooks.
Includes high-signal environment telemetry and workspace isolation.
"""
gui_script = os.path.abspath("gui_2.py")
gui_script = os.path.abspath("sloppy.py")
diag = VerificationLogger("live_gui_startup", "live_gui_diag")
diag.log_state("GUI Script", "N/A", "gui_2.py")

View File

@@ -4,6 +4,7 @@ from unittest.mock import patch, MagicMock
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
import ai_client

View File

@@ -4,6 +4,7 @@ import ai_client
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from ai_client import set_agent_tools, _build_anthropic_tools

View File

@@ -5,6 +5,7 @@ import os
# Ensure project root is in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -5,6 +5,7 @@ from unittest.mock import patch
# Ensure project root is in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -7,6 +7,7 @@ import os
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
# Import after path fix
from scripts.cli_tool_bridge import main

View File

@@ -7,6 +7,7 @@ import os
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
# Import after path fix
from scripts.cli_tool_bridge import main

View File

@@ -5,6 +5,7 @@ from typing import Any
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -5,6 +5,7 @@ import sys
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
import ai_client
import project_manager

View File

@@ -6,6 +6,7 @@ import os
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient
from simulation.sim_context import ContextSimulation

View File

@@ -8,6 +8,7 @@ import os
# Ensure the project root is in sys.path to resolve imports correctly
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from gemini_cli_adapter import GeminiCliAdapter

View File

@@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
# Import the necessary functions from ai_client, including the reset helper
from ai_client import get_gemini_cache_stats, reset_session

View File

@@ -8,6 +8,7 @@ import sys
# Ensure project root is in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient
# Define a temporary file path for callback testing

View File

@@ -5,6 +5,7 @@ import os
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -4,6 +4,7 @@ from typing import Any
# Ensure project root is in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
def test_diagnostics_panel_initialization(app_instance: Any) -> None:

View File

@@ -5,6 +5,7 @@ from gui_2 import App
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
def test_gui_updates_on_event(app_instance: App) -> None:
app_instance.last_md = "mock_md"

View File

@@ -4,6 +4,7 @@ import os
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -4,6 +4,7 @@ import os
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -5,6 +5,7 @@ from typing import Any
# Ensure project root is in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from gui_2 import App

View File

@@ -6,6 +6,7 @@ from pathlib import Path
# Ensure project root is in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
# Import necessary modules from the project
import aggregate

View File

@@ -4,6 +4,7 @@ from unittest.mock import patch
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -5,6 +5,7 @@ import os
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from gui_2 import App

View File

@@ -5,6 +5,7 @@ import os
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -4,6 +4,7 @@ from unittest.mock import patch
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
import mcp_client

View File

@@ -4,6 +4,7 @@ import time
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from performance_monitor import PerformanceMonitor

View File

@@ -4,6 +4,7 @@ import sys
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from simulation.sim_ai_settings import AISettingsSimulation

View File

@@ -4,6 +4,7 @@ import sys
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from simulation.sim_base import BaseSimulation

View File

@@ -4,6 +4,7 @@ import sys
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from simulation.sim_context import ContextSimulation

View File

@@ -4,6 +4,7 @@ import sys
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from simulation.sim_execution import ExecutionSimulation

View File

@@ -4,6 +4,7 @@ import sys
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from simulation.sim_tools import ToolsSimulation

View File

@@ -6,6 +6,7 @@ from types import SimpleNamespace
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
import ai_client

View File

@@ -3,6 +3,7 @@ import os
# Ensure project root is in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from simulation.user_agent import UserSimAgent

View File

@@ -5,6 +5,7 @@ import os
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -5,6 +5,7 @@ import os
import json
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -4,6 +4,7 @@ import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from api_hook_client import ApiHookClient

View File

@@ -4,6 +4,7 @@ from unittest.mock import MagicMock
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
from simulation.workflow_sim import WorkflowSimulator