feat(types): Complete strict static analysis and typing track

This commit is contained in:
2026-03-04 09:46:02 -05:00
parent c6c2a1b40c
commit fe2114a2e0
46 changed files with 606 additions and 795 deletions

View File

@@ -16,7 +16,7 @@ import tomllib
import re
import glob
from pathlib import Path, PureWindowsPath
from typing import Any
from typing import Any, cast
import summarize
import project_manager
from file_cache import ASTParser
@@ -67,9 +67,11 @@ def build_files_section(base_dir: Path, files: list[str | dict[str, Any]]) -> st
sections = []
for entry_raw in files:
if isinstance(entry_raw, dict):
entry = entry_raw.get("path")
entry = cast(str, entry_raw.get("path", ""))
else:
entry = entry_raw
if not entry or not isinstance(entry, str):
continue
paths = resolve_paths(base_dir, entry)
if not paths:
sections.append(f"### `{entry}`\n\n```text\nERROR: no files matched: {entry}\n```")
@@ -90,6 +92,8 @@ def build_files_section(base_dir: Path, files: list[str | dict[str, Any]]) -> st
def build_screenshots_section(base_dir: Path, screenshots: list[str]) -> str:
sections = []
for entry in screenshots:
if not entry or not isinstance(entry, str):
continue
paths = resolve_paths(base_dir, entry)
if not paths:
sections.append(f"### `{entry}`\n\n_ERROR: no files matched: {entry}_")
@@ -115,14 +119,16 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
mtime : float (last modification time, for skip-if-unchanged optimization)
tier : int | None (optional tier for context management)
"""
items = []
items: list[dict[str, Any]] = []
for entry_raw in files:
if isinstance(entry_raw, dict):
entry = entry_raw.get("path")
entry = cast(str, entry_raw.get("path", ""))
tier = entry_raw.get("tier")
else:
entry = entry_raw
tier = None
if not entry or not isinstance(entry, str):
continue
paths = resolve_paths(base_dir, entry)
if not paths:
items.append({"path": None, "entry": entry, "content": f"ERROR: no files matched: {entry}", "error": True, "mtime": 0.0, "tier": tier})
@@ -156,14 +162,15 @@ def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str:
sections = []
for item in file_items:
path = item.get("path")
entry = item.get("entry", "unknown")
content = item.get("content", "")
entry = cast(str, item.get("entry", "unknown"))
content = cast(str, item.get("content", ""))
if path is None:
sections.append(f"### `{entry}`\n\n```text\n{content}\n```")
continue
suffix = path.suffix.lstrip(".") if hasattr(path, "suffix") else "text"
p = cast(Path, path)
suffix = p.suffix.lstrip(".") if hasattr(p, "suffix") else "text"
lang = suffix if suffix else "text"
original = entry if "*" not in entry else str(path)
original = entry if "*" not in entry else str(p)
sections.append(f"### `{original}`\n\n```{lang}\n{content}\n```")
return "\n\n---\n\n".join(sections)
@@ -205,15 +212,16 @@ def build_tier1_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
sections = []
for item in file_items:
path = item.get("path")
name = path.name if path else ""
name = path.name if path and isinstance(path, Path) else ""
if name in core_files or item.get("tier") == 1:
# Include in full
sections.append("### `" + (item.get("entry") or str(path)) + "`\n\n" +
f"```{path.suffix.lstrip('.') if path.suffix else 'text'}\n{item.get('content', '')}\n```")
sections.append("### `" + (cast(str, item.get("entry")) or str(path)) + "`\n\n" +
f"```{path.suffix.lstrip('.') if path and isinstance(path, Path) and path.suffix else 'text'}\n{item.get('content', '')}\n```")
else:
# Summarize
sections.append("### `" + (item.get("entry") or str(path)) + "`\n\n" +
summarize.summarise_file(path, item.get("content", "")))
if path and isinstance(path, Path):
sections.append("### `" + (cast(str, item.get("entry")) or str(path)) + "`\n\n" +
summarize.summarise_file(path, cast(str, item.get("content", ""))))
parts.append("## Files (Tier 1 - Mixed)\n\n" + "\n\n---\n\n".join(sections))
if screenshots:
parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots))
@@ -237,20 +245,20 @@ def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
if file_items:
sections = []
for item in file_items:
path = item.get("path")
entry = item.get("entry", "")
path = cast(Path, item.get("path"))
entry = cast(str, item.get("entry", ""))
path_str = str(path) if path else ""
# Check if this file is in focus_files (by name or path)
is_focus = False
for focus in focus_files:
if focus == entry or (path and focus == path.name) or focus in path_str:
if focus == entry or (path and focus == path.name) or (path_str and focus in path_str):
is_focus = True
break
if is_focus or item.get("tier") == 3:
sections.append("### `" + (entry or path_str) + "`\n\n" +
f"```{path.suffix.lstrip('.') if path and path.suffix else 'text'}\n{item.get('content', '')}\n```")
else:
content = item.get("content", "")
content = cast(str, item.get("content", ""))
if path and path.suffix == ".py" and not item.get("error"):
try:
parser = ASTParser("python")
@@ -260,7 +268,8 @@ def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
# Fallback to summary if AST parsing fails
sections.append(f"### `{entry or path_str}`\n\n" + summarize.summarise_file(path, content))
else:
sections.append(f"### `{entry or path_str}`\n\n" + summarize.summarise_file(path, content))
if path:
sections.append(f"### `{entry or path_str}`\n\n" + summarize.summarise_file(path, content))
parts.append("## Files (Tier 3 - Focused)\n\n" + "\n\n---\n\n".join(sections))
if screenshots:
parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots))

File diff suppressed because it is too large Load Diff

View File

@@ -22,20 +22,19 @@
- [x] SAFETY: Preserve JSON serialization compatibility.
- [x] Task: Conductor - User Manual Verification 'Phase 2: Core Library' (Protocol in workflow.md)
## Phase 3: GUI God-Object Typing Resolution
- [ ] Task: Resolve `gui_2.py` Type Errors
- [ ] WHERE: `gui_2.py`
- [ ] WHAT: Type the `App` class state variables, method signatures, and ImGui integration boundaries.
- [ ] HOW: Use `type: ignore[import]` only for ImGui C-bindings if strictly necessary, but type internal state tightly.
- [ ] SAFETY: Ensure `live_gui` tests pass after typing.
- [x] PROGRESS: Initial pass completed, several critical errors resolved, baseline established.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Typing' (Protocol in workflow.md)
## Phase 3: GUI God-Object Typing Resolution [checkpoint: 6ebbf40]
- [x] Task: Resolve `gui_2.py` Type Errors
- [x] WHERE: `gui_2.py`
- [x] WHAT: Type the `App` class state variables, method signatures, and ImGui integration boundaries.
- [x] HOW: Use `type: ignore[import]` only for ImGui C-bindings if strictly necessary, but type internal state tightly.
- [x] SAFETY: Ensure `live_gui` tests pass after typing.
- [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Typing' (Protocol in workflow.md)
## Phase 4: CI Integration & Final Validation
## Phase 4: CI Integration & Final Validation [checkpoint: c6c2a1b]
- [x] Task: Establish Pre-Commit Guardrails
- [x] WHERE: `.git/hooks/pre-commit` or a `scripts/validate_types.ps1`
- [x] WHAT: Create a script that runs ruff and mypy, blocking commits if they fail.
- [x] HOW: Standard shell scripting.
- [x] SAFETY: Ensure it works cross-platform (Windows/Linux).
- [ ] Task: Full Suite Validation & Warning Cleanup
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Validation' (Protocol in workflow.md)
- [x] Task: Full Suite Validation & Warning Cleanup
- [x] Task: Conductor - User Manual Verification 'Phase 4: Validation' (Protocol in workflow.md)

View File

@@ -6,7 +6,7 @@ This file is kept so that any stale imports do not break.
"""
from pathlib import Path
from typing import Optional
from typing import Optional, Any, List, Tuple, Dict
import tree_sitter
import tree_sitter_python
@@ -33,15 +33,15 @@ class ASTParser:
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
"""
tree = self.parse(code)
edits = []
edits: List[Tuple[int, int, str]] = []
def is_docstring(node):
def is_docstring(node: tree_sitter.Node) -> bool:
if node.type == "expression_statement" and node.child_count > 0:
if node.children[0].type == "string":
return True
return False
def walk(node):
def walk(node: tree_sitter.Node) -> None:
if node.type == "function_definition":
body = node.child_by_field_name("body")
if body and body.type == "block":
@@ -77,15 +77,15 @@ class ASTParser:
Otherwise strips bodies but preserves docstrings.
"""
tree = self.parse(code)
edits = []
edits: List[Tuple[int, int, str]] = []
def is_docstring(node):
def is_docstring(node: tree_sitter.Node) -> bool:
if node.type == "expression_statement" and node.child_count > 0:
if node.children[0].type == "string":
return True
return False
def has_core_logic_decorator(node):
def has_core_logic_decorator(node: tree_sitter.Node) -> bool:
# Check if parent is decorated_definition
parent = node.parent
if parent and parent.type == "decorated_definition":
@@ -96,7 +96,7 @@ class ASTParser:
return True
return False
def has_hot_comment(func_node):
def has_hot_comment(func_node: tree_sitter.Node) -> bool:
# Check all descendants of the function_definition for a [HOT] comment
stack = [func_node]
while stack:
@@ -109,7 +109,7 @@ class ASTParser:
stack.append(child)
return False
def walk(node):
def walk(node: tree_sitter.Node) -> None:
if node.type == "function_definition":
body = node.child_by_field_name("body")
if body and body.type == "block":
@@ -153,5 +153,6 @@ def get_file_id(path: Path) -> Optional[str]:
def evict(path: Path) -> None:
pass
def list_cached() -> list[dict]:
def list_cached() -> List[Dict[str, Any]]:
return []

View File

@@ -12,10 +12,10 @@ class GeminiCliAdapter:
def __init__(self, binary_path: str = "gemini"):
self.binary_path = binary_path
self.session_id: Optional[str] = None
self.last_usage: Optional[dict] = None
self.last_usage: Optional[dict[str, Any]] = None
self.last_latency: float = 0.0
def send(self, message: str, safety_settings: list | None = None, system_instruction: str | None = None,
def send(self, message: str, safety_settings: list[Any] | None = None, system_instruction: str | None = None,
model: str | None = None, stream_callback: Optional[Callable[[str], None]] = None) -> dict[str, Any]:
"""
Sends a message to the Gemini CLI and processes the streaming JSON output.

348
gui_2.py
View File

@@ -9,7 +9,7 @@ import json
import sys
import os
import uuid
import requests
import requests # type: ignore[import-untyped]
from pathlib import Path
from tkinter import filedialog, Tk
from typing import Optional, Callable, Any
@@ -61,21 +61,21 @@ def hide_tk_root() -> Tk:
def vec4(r: float, g: float, b: float, a: float = 1.0) -> imgui.ImVec4: return imgui.ImVec4(r/255, g/255, b/255, a)
C_OUT: tuple[float, ...] = vec4(100, 200, 255)
C_IN: tuple[float, ...] = vec4(140, 255, 160)
C_REQ: tuple[float, ...] = vec4(255, 220, 100)
C_RES: tuple[float, ...] = vec4(180, 255, 180)
C_TC: tuple[float, ...] = vec4(255, 180, 80)
C_TR: tuple[float, ...] = vec4(180, 220, 255)
C_TRS: tuple[float, ...] = vec4(200, 180, 255)
C_LBL: tuple[float, ...] = vec4(180, 180, 180)
C_VAL: tuple[float, ...] = vec4(220, 220, 220)
C_KEY: tuple[float, ...] = vec4(140, 200, 255)
C_NUM: tuple[float, ...] = vec4(180, 255, 180)
C_SUB: tuple[float, ...] = vec4(220, 200, 120)
C_OUT: imgui.ImVec4 = vec4(100, 200, 255)
C_IN: imgui.ImVec4 = vec4(140, 255, 160)
C_REQ: imgui.ImVec4 = vec4(255, 220, 100)
C_RES: imgui.ImVec4 = vec4(180, 255, 180)
C_TC: imgui.ImVec4 = vec4(255, 180, 80)
C_TR: imgui.ImVec4 = vec4(180, 220, 255)
C_TRS: imgui.ImVec4 = vec4(200, 180, 255)
C_LBL: imgui.ImVec4 = vec4(180, 180, 180)
C_VAL: imgui.ImVec4 = vec4(220, 220, 220)
C_KEY: imgui.ImVec4 = vec4(140, 200, 255)
C_NUM: imgui.ImVec4 = vec4(180, 255, 180)
C_SUB: imgui.ImVec4 = vec4(220, 200, 120)
DIR_COLORS: dict[str, tuple[float, ...]] = {"OUT": C_OUT, "IN": C_IN}
KIND_COLORS: dict[str, tuple[float, ...]] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS}
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"]
@@ -173,19 +173,19 @@ class App:
def __init__(self) -> None:
# Initialize locks first to avoid initialization order issues
self._send_thread_lock = threading.Lock()
self._disc_entries_lock = threading.Lock()
self._pending_comms_lock = threading.Lock()
self._pending_tool_calls_lock = threading.Lock()
self._pending_history_adds_lock = threading.Lock()
self._pending_gui_tasks_lock = threading.Lock()
self._pending_dialog_lock = threading.Lock()
self._api_event_queue_lock = threading.Lock()
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 = load_config()
self.event_queue = events.AsyncEventQueue()
self._loop = asyncio.new_event_loop()
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
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")
@@ -208,33 +208,33 @@ class App:
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 = 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", ".")
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 = 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", "")
self.ui_ai_input = ""
self.ui_disc_new_name_input = ""
self.ui_disc_new_role_input = ""
self.ui_epic_input = ""
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 = False
self.ui_new_track_name = ""
self.ui_new_track_desc = ""
self.ui_new_track_type = "feature"
self.ui_conductor_setup_summary = ""
self.ui_last_script_text = ""
self.ui_last_script_output = ""
self.ai_status = "idle"
self.ai_response = ""
self.last_md = ""
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
@@ -255,82 +255,82 @@ class App:
"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()}
self.show_script_output = False
self.show_text_viewer = False
self.text_viewer_title = ""
self.text_viewer_content = ""
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 = False
self._pending_dialog_open: bool = False
self._pending_actions: dict[str, ConfirmDialog] = {}
self._pending_ask_dialog = False
self._ask_dialog_open = False
self._ask_request_id = None
self._ask_tool_data = None
self.mma_step_mode = False
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 = "idle"
self.mma_status: str = "idle"
self._pending_mma_approval: dict[str, Any] | None = None
self._mma_approval_open = False
self._mma_approval_edit_mode = False
self._mma_approval_payload = ""
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 = False
self._mma_spawn_edit_mode = False
self._mma_spawn_prompt = ''
self._mma_spawn_context = ''
self.mma_tier_usage = {
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] = []
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] = []
self._pending_tool_calls: list[dict[str, Any]] = []
self._pending_history_adds: list[dict[str, Any]] = []
self._trigger_blink = False
self._is_blinking = False
self._blink_start_time = 0.0
self._trigger_script_blink = False
self._is_script_blinking = False
self._script_blink_start_time = 0.0
self._scroll_disc_to_bottom = False
self._scroll_comms_to_bottom = False
self._scroll_tool_calls_to_bottom = False
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 = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0}
self._gemini_cache_text = ""
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 = {}
self._token_stats: dict[str, Any] = {}
self._token_stats_dirty: bool = False
self.ui_disc_truncate_pairs: int = 2
self.ui_auto_scroll_comms = True
self.ui_auto_scroll_tool_calls = True
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 = False
self.ui_new_ticket_id = ""
self.ui_new_ticket_desc = ""
self.ui_new_ticket_target = ""
self.ui_new_ticket_deps = ""
self._track_discussion_active = False
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 = False
self.is_viewing_prior_session: bool = False
self.prior_session_entries: list[dict[str, Any]] = []
self.test_hooks_enabled = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
self.ui_manual_approve = False
self.perf_monitor = PerformanceMonitor()
self.perf_history = {"frame_time": [0.0]*100, "fps": [0.0]*100, "cpu": [0.0]*100, "input_lag": [0.0]*100}
self._perf_last_update = 0.0
self._autosave_interval = 60.0
self._last_autosave = time.time()
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()
@@ -856,7 +856,7 @@ class App:
self._switch_discussion(remaining[0])
# ---------------------------------------------------------------- logic
def _on_comms_entry(self, entry: dict) -> None:
def _on_comms_entry(self, entry: dict[str, Any]) -> None:
# sys.stderr.write(f"[DEBUG] _on_comms_entry: {entry.get('kind')} {entry.get('direction')}\n")
session_logger.log_comms(entry)
entry["local_ts"] = time.time()
@@ -898,7 +898,7 @@ class App:
with self._pending_tool_calls_lock:
self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
def _on_api_event(self, *args, **kwargs) -> None:
def _on_api_event(self, *args: Any, **kwargs: Any) -> None:
payload = kwargs.get("payload", {})
with self._pending_gui_tasks_lock:
self._pending_gui_tasks.append({"action": "refresh_api_metrics", "payload": payload})
@@ -992,16 +992,16 @@ class App:
setattr(self, attr_name, value)
if item == "gcli_path":
if not ai_client._gemini_cli_adapter:
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=value)
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=str(value))
else:
ai_client._gemini_cli_adapter.binary_path = value
ai_client._gemini_cli_adapter.binary_path = str(value)
elif action == "click":
item = task.get("item")
user_data = task.get("user_data")
if item == "btn_project_new_automated":
self._cb_new_project_automated(user_data)
elif item == "btn_mma_load_track":
self._cb_load_track(user_data)
self._cb_load_track(str(user_data or ""))
elif item in self._clickable_actions:
# Check if it's a method that accepts user_data
import inspect
@@ -1018,7 +1018,7 @@ class App:
item = task.get("listbox", task.get("item"))
value = task.get("item_value", task.get("value"))
if item == "disc_listbox":
self._switch_discussion(value)
self._switch_discussion(str(value or ""))
elif task.get("type") == "ask":
self._pending_ask_dialog = True
self._ask_request_id = task.get("request_id")
@@ -1037,18 +1037,18 @@ class App:
elif cb in self._predefined_callbacks:
self._predefined_callbacks[cb](*args)
elif action == "mma_step_approval":
dlg = MMAApprovalDialog(task.get("ticket_id"), task.get("payload"))
dlg = MMAApprovalDialog(str(task.get("ticket_id") or ""), str(task.get("payload") or ""))
self._pending_mma_approval = task
if "dialog_container" in task:
task["dialog_container"][0] = dlg
elif action == 'refresh_from_project':
self._refresh_from_project()
elif action == "mma_spawn_approval":
dlg = MMASpawnApprovalDialog(
task.get("ticket_id"),
task.get("role"),
task.get("prompt"),
task.get("context_md")
spawn_dlg = MMASpawnApprovalDialog(
str(task.get("ticket_id") or ""),
str(task.get("role") or ""),
str(task.get("prompt") or ""),
str(task.get("context_md") or "")
)
self._pending_mma_spawn = task
self._mma_spawn_prompt = task.get("prompt", "")
@@ -1056,7 +1056,7 @@ class App:
self._mma_spawn_open = True
self._mma_spawn_edit_mode = False
if "dialog_container" in task:
task["dialog_container"][0] = dlg
task["dialog_container"][0] = spawn_dlg
except Exception as e:
print(f"Error executing GUI task: {e}")
@@ -1143,7 +1143,7 @@ class App:
else:
print("[DEBUG] No pending spawn approval found")
def _handle_mma_respond(self, approved: bool, payload: str = None, abort: bool = False, prompt: str = None, context_md: str = None) -> None:
def _handle_mma_respond(self, approved: bool, payload: str | None = None, abort: bool = False, prompt: str | None = None, context_md: str | None = None) -> None:
if self._pending_mma_approval:
dlg = self._pending_mma_approval.get("dialog_container", [None])[0]
if dlg:
@@ -1155,17 +1155,17 @@ class App:
dlg._condition.notify_all()
self._pending_mma_approval = None
if self._pending_mma_spawn:
dlg = self._pending_mma_spawn.get("dialog_container", [None])[0]
if dlg:
with dlg._condition:
dlg._approved = approved
dlg._abort = abort
spawn_dlg = self._pending_mma_spawn.get("dialog_container", [None])[0]
if spawn_dlg:
with spawn_dlg._condition:
spawn_dlg._approved = approved
spawn_dlg._abort = abort
if prompt is not None:
dlg._prompt = prompt
spawn_dlg._prompt = prompt
if context_md is not None:
dlg._context_md = context_md
dlg._done = True
dlg._condition.notify_all()
spawn_dlg._context_md = context_md
spawn_dlg._done = True
spawn_dlg._condition.notify_all()
self._pending_mma_spawn = None
def _handle_approve_ask(self) -> None:
@@ -1173,7 +1173,7 @@ class App:
if not self._ask_request_id: return
request_id = self._ask_request_id
def do_post():
def do_post() -> None:
try:
requests.post(
"http://127.0.0.1:8999/api/ask/respond",
@@ -1191,7 +1191,7 @@ class App:
if not self._ask_request_id: return
request_id = self._ask_request_id
def do_post():
def do_post() -> None:
try:
requests.post(
"http://127.0.0.1:8999/api/ask/respond",
@@ -1268,7 +1268,7 @@ class App:
self._loop.create_task(self._process_event_queue())
# Fallback: process queues even if GUI thread is idling/stuck
async def queue_fallback():
async def queue_fallback() -> None:
while True:
try:
self._process_pending_gui_tasks()
@@ -1393,7 +1393,7 @@ class App:
usage[k] += u.get(k, 0) or 0
self.session_usage = usage
def _refresh_api_metrics(self, payload: dict, md_content: str | None = None) -> None:
def _refresh_api_metrics(self, payload: dict[str, Any], md_content: str | None = None) -> None:
if "latency" in payload:
self.session_usage["last_latency"] = payload["latency"]
self._recalculate_session_usage()
@@ -1567,7 +1567,7 @@ class App:
self.config["gui"] = {"show_windows": self.show_windows}
theme.save_to_config(self.config)
def _do_generate(self) -> tuple[str, Path, list, str, str]:
def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]:
"""Returns (full_md, output_path, file_items, stable_md, discussion_text)."""
self._flush_to_project()
self._save_active_project()
@@ -1589,7 +1589,7 @@ class App:
def _fetch_models(self, provider: str) -> None:
self.ai_status = "fetching models..."
def do_fetch():
def do_fetch() -> None:
try:
models = ai_client.list_models(provider)
self.available_models = models
@@ -1698,12 +1698,14 @@ class App:
self._tool_log.append(tc)
self._pending_tool_calls.clear()
if self.show_windows.get("Context Hub", False):
exp, self.show_windows["Context Hub"] = imgui.begin("Context Hub", self.show_windows["Context Hub"])
exp, opened = imgui.begin("Context Hub", self.show_windows["Context Hub"])
self.show_windows["Context Hub"] = bool(opened)
if exp:
self._render_projects_panel()
imgui.end()
if self.show_windows.get("Files & Media", False):
exp, self.show_windows["Files & Media"] = imgui.begin("Files & Media", self.show_windows["Files & Media"])
exp, opened = imgui.begin("Files & Media", self.show_windows["Files & Media"])
self.show_windows["Files & Media"] = bool(opened)
if exp:
if imgui.collapsing_header("Files"):
self._render_files_panel()
@@ -1711,7 +1713,8 @@ class App:
self._render_screenshots_panel()
imgui.end()
if self.show_windows.get("AI Settings", False):
exp, self.show_windows["AI Settings"] = imgui.begin("AI Settings", self.show_windows["AI Settings"])
exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"])
self.show_windows["AI Settings"] = bool(opened)
if exp:
if imgui.collapsing_header("Provider & Model"):
self._render_provider_panel()
@@ -1721,34 +1724,40 @@ class App:
self._render_token_budget_panel()
imgui.end()
if self.show_windows.get("MMA Dashboard", False):
exp, self.show_windows["MMA Dashboard"] = imgui.begin("MMA Dashboard", self.show_windows["MMA Dashboard"])
exp, opened = imgui.begin("MMA Dashboard", self.show_windows["MMA Dashboard"])
self.show_windows["MMA Dashboard"] = bool(opened)
if exp:
self._render_mma_dashboard()
imgui.end()
if self.show_windows.get("Tier 1: Strategy", False):
exp, self.show_windows["Tier 1: Strategy"] = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"])
exp, opened = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"])
self.show_windows["Tier 1: Strategy"] = bool(opened)
if exp:
self._render_tier_stream_panel("Tier 1", "Tier 1")
imgui.end()
if self.show_windows.get("Tier 2: Tech Lead", False):
exp, self.show_windows["Tier 2: Tech Lead"] = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"])
exp, opened = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"])
self.show_windows["Tier 2: Tech Lead"] = bool(opened)
if exp:
self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)")
imgui.end()
if self.show_windows.get("Tier 3: Workers", False):
exp, self.show_windows["Tier 3: Workers"] = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"])
exp, opened = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"])
self.show_windows["Tier 3: Workers"] = bool(opened)
if exp:
self._render_tier_stream_panel("Tier 3", None)
imgui.end()
if self.show_windows.get("Tier 4: QA", False):
exp, self.show_windows["Tier 4: QA"] = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"])
exp, opened = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"])
self.show_windows["Tier 4: QA"] = bool(opened)
if exp:
self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)")
imgui.end()
if self.show_windows.get("Theme", False):
self._render_theme_panel()
if self.show_windows.get("Discussion Hub", False):
exp, self.show_windows["Discussion Hub"] = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"])
exp, opened = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"])
self.show_windows["Discussion Hub"] = bool(opened)
if exp:
# Top part for the history
imgui.begin_child("HistoryChild", size=(0, -200))
@@ -1765,7 +1774,8 @@ class App:
imgui.end_tab_bar()
imgui.end()
if self.show_windows.get("Operations Hub", False):
exp, self.show_windows["Operations Hub"] = imgui.begin("Operations Hub", self.show_windows["Operations Hub"])
exp, opened = imgui.begin("Operations Hub", self.show_windows["Operations Hub"])
self.show_windows["Operations Hub"] = bool(opened)
if exp:
imgui.text("Focus Agent:")
imgui.same_line()
@@ -1794,7 +1804,8 @@ class App:
if self.show_windows.get("Log Management", False):
self._render_log_management()
if self.show_windows["Diagnostics"]:
exp, self.show_windows["Diagnostics"] = imgui.begin("Diagnostics", self.show_windows["Diagnostics"])
exp, opened = imgui.begin("Diagnostics", self.show_windows["Diagnostics"])
self.show_windows["Diagnostics"] = bool(opened)
if exp:
now = time.time()
if now - self._perf_last_update >= 0.5:
@@ -1893,7 +1904,7 @@ class App:
else:
self._ask_dialog_open = False
if imgui.begin_popup_modal("Approve Tool Execution", None, imgui.WindowFlags_.always_auto_resize)[0]:
if not self._pending_ask_dialog:
if not self._pending_ask_dialog or self._ask_tool_data is None:
imgui.close_current_popup()
else:
tool_name = self._ask_tool_data.get("tool", "unknown")
@@ -2000,7 +2011,7 @@ class App:
self._is_script_blinking = True
self._script_blink_start_time = time.time()
try:
imgui.set_window_focus("Last Script Output")
imgui.set_window_focus("Last Script Output") # type: ignore[call-arg]
except Exception:
pass
if self._is_script_blinking:
@@ -2013,7 +2024,8 @@ class App:
imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 100, 255, alpha))
imgui.push_style_color(imgui.Col_.child_bg, vec4(0, 100, 255, alpha))
imgui.set_next_window_size(imgui.ImVec2(800, 600), imgui.Cond_.first_use_ever)
expanded, self.show_script_output = imgui.begin("Last Script Output", self.show_script_output)
expanded, opened = imgui.begin("Last Script Output", self.show_script_output)
self.show_script_output = bool(opened)
if expanded:
imgui.text("Script:")
imgui.same_line()
@@ -2043,7 +2055,8 @@ class App:
imgui.end()
if self.show_text_viewer:
imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever)
expanded, self.show_text_viewer = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer)
expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer)
self.show_text_viewer = bool(opened)
if expanded:
if self.ui_word_wrap:
imgui.begin_child("tv_wrap", imgui.ImVec2(-1, -1), False)
@@ -2153,7 +2166,7 @@ class App:
self._cb_plan_epic()
def _cb_plan_epic(self) -> None:
def _bg_task():
def _bg_task() -> None:
try:
self.ai_status = "Planning Epic (Tier 1)..."
history = orchestrator_pm.get_track_history_summary()
@@ -2166,7 +2179,7 @@ class App:
_t1_resp = [e for e in _t1_new if e.get("direction") == "IN" and e.get("kind") == "response"]
_t1_in = sum(e.get("payload", {}).get("usage", {}).get("input_tokens", 0) for e in _t1_resp)
_t1_out = sum(e.get("payload", {}).get("usage", {}).get("output_tokens", 0) for e in _t1_resp)
def _push_t1_usage(i, o):
def _push_t1_usage(i: int, o: int) -> None:
self.mma_tier_usage["Tier 1"]["input"] += i
self.mma_tier_usage["Tier 1"]["output"] += o
with self._pending_gui_tasks_lock:
@@ -2194,7 +2207,7 @@ class App:
def _cb_accept_tracks(self) -> None:
self._show_track_proposal_modal = False
def _bg_task():
def _bg_task() -> None:
# Generate skeletons once
self.ai_status = "Phase 2: Generating skeletons for all tracks..."
parser = ASTParser(language="python")
@@ -2374,7 +2387,8 @@ class App:
imgui.end_popup()
def _render_log_management(self) -> None:
exp, self.show_windows["Log Management"] = imgui.begin("Log Management", self.show_windows["Log Management"])
exp, opened = imgui.begin("Log Management", self.show_windows["Log Management"])
self.show_windows["Log Management"] = bool(opened)
if not exp:
imgui.end()
return
@@ -2413,19 +2427,19 @@ class App:
if imgui.button(f"Unstar##{session_id}"):
registry.update_session_metadata(
session_id,
message_count=metadata.get("message_count"),
errors=metadata.get("errors"),
size_kb=metadata.get("size_kb"),
message_count=int(metadata.get("message_count") or 0),
errors=int(metadata.get("errors") or 0),
size_kb=int(metadata.get("size_kb") or 0),
whitelisted=False,
reason=metadata.get("reason")
reason=str(metadata.get("reason") or "")
)
else:
if imgui.button(f"Star##{session_id}"):
registry.update_session_metadata(
session_id,
message_count=metadata.get("message_count"),
errors=metadata.get("errors"),
size_kb=metadata.get("size_kb"),
message_count=int(metadata.get("message_count") or 0),
errors=int(metadata.get("errors") or 0),
size_kb=int(metadata.get("size_kb") or 0),
whitelisted=True,
reason="Manually whitelisted"
)
@@ -2867,7 +2881,7 @@ class App:
self._is_blinking = True
self._blink_start_time = time.time()
try:
imgui.set_window_focus("Response")
imgui.set_window_focus("Response") # type: ignore[call-arg]
except:
pass
is_blinking = False
@@ -3131,7 +3145,7 @@ class App:
imgui.pop_style_color()
imgui.table_next_column()
if imgui.button(f"Load##{track.get('id')}"):
self._cb_load_track(track.get("id"))
self._cb_load_track(str(track.get("id") or ""))
imgui.end_table()
# 1b. New Track Form
@@ -3248,14 +3262,14 @@ class App:
# 4. Task DAG Visualizer
imgui.text("Task DAG")
if self.active_track:
tickets_by_id = {t.get('id'): t for t in self.active_tickets}
tickets_by_id = {str(t.get('id') or ''): t for t in self.active_tickets}
all_ids = set(tickets_by_id.keys())
# Build children map
children_map = {}
children_map: dict[str, list[str]] = {}
for t in self.active_tickets:
for dep in t.get('depends_on', []):
if dep not in children_map: children_map[dep] = []
children_map[dep].append(t.get('id'))
children_map[dep].append(str(t.get('id') or ''))
# Roots are those whose depends_on elements are NOT in all_ids
roots = []
for t in self.active_tickets:
@@ -3263,7 +3277,7 @@ class App:
has_local_dep = any(d in all_ids for d in deps)
if not has_local_dep:
roots.append(t)
rendered = set()
rendered: set[str] = set()
for root in roots:
self._render_ticket_dag_node(root, tickets_by_id, children_map, rendered)
@@ -3341,7 +3355,7 @@ class App:
pass
imgui.end_child()
def _render_ticket_dag_node(self, ticket: Ticket, tickets_by_id: dict[str, Ticket], children_map: dict[str, list[str]], rendered: set[str]) -> None:
def _render_ticket_dag_node(self, ticket: dict[str, Any], tickets_by_id: dict[str, Any], children_map: dict[str, list[str]], rendered: set[str]) -> None:
tid = ticket.get('id', '??')
is_duplicate = tid in rendered
if not is_duplicate:
@@ -3552,8 +3566,10 @@ class App:
imgui.text("Project System Prompt")
ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100))
def _render_theme_panel(self) -> None:
exp, self.show_windows["Theme"] = imgui.begin("Theme", self.show_windows["Theme"])
def _render_theme_panel(self) -> None:
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
self.show_windows["Theme"] = bool(opened)
if exp:
imgui.text("Palette")
cp = theme.get_current_palette()

BIN
gui_2_mypy.txt Normal file

Binary file not shown.

BIN
gui_2_only_errors.txt Normal file

Binary file not shown.

View File

@@ -3,6 +3,7 @@ import tomli_w
import tomllib
from datetime import datetime
import os
from typing import Any
class LogRegistry:
"""
@@ -18,7 +19,7 @@ class LogRegistry:
registry_path (str): The file path to the TOML registry.
"""
self.registry_path = registry_path
self.data = {}
self.data: dict[str, dict[str, Any]] = {}
self.load_registry()
def load_registry(self) -> None:
@@ -56,16 +57,16 @@ class LogRegistry:
"""
try:
# Convert datetime objects to ISO format strings for TOML serialization
data_to_save = {}
data_to_save: dict[str, Any] = {}
for session_id, session_data in self.data.items():
session_data_copy = {}
session_data_copy: dict[str, Any] = {}
for k, v in session_data.items():
if v is None:
continue
if k == 'start_time' and isinstance(v, datetime):
session_data_copy[k] = v.isoformat()
elif k == 'metadata' and isinstance(v, dict):
metadata_copy = {}
metadata_copy: dict[str, Any] = {}
for mk, mv in v.items():
if mv is None:
continue
@@ -125,11 +126,13 @@ class LogRegistry:
if self.data[session_id].get('metadata') is None:
self.data[session_id]['metadata'] = {}
# Update fields
self.data[session_id]['metadata']['message_count'] = message_count
self.data[session_id]['metadata']['errors'] = errors
self.data[session_id]['metadata']['size_kb'] = size_kb
self.data[session_id]['metadata']['whitelisted'] = whitelisted
self.data[session_id]['metadata']['reason'] = reason
metadata = self.data[session_id].get('metadata')
if isinstance(metadata, dict):
metadata['message_count'] = message_count
metadata['errors'] = errors
metadata['size_kb'] = size_kb
metadata['whitelisted'] = whitelisted
metadata['reason'] = reason
# self.data[session_id]['metadata']['timestamp'] = datetime.utcnow() # Optionally add a timestamp
# Also update the top-level whitelisted flag if provided
if whitelisted is not None:
@@ -150,7 +153,7 @@ class LogRegistry:
if session_data is None:
return False # Non-existent sessions are not whitelisted
# Check the top-level 'whitelisted' flag. If it's not set or False, it's not whitelisted.
return session_data.get('whitelisted', False)
return bool(session_data.get('whitelisted', False))
def update_auto_whitelist_status(self, session_id: str) -> None:
"""
@@ -165,14 +168,14 @@ class LogRegistry:
return
session_data = self.data[session_id]
session_path = session_data.get('path')
if not session_path or not os.path.isdir(session_path):
if not session_path or not os.path.isdir(str(session_path)):
return
total_size_bytes = 0
message_count = 0
found_keywords = []
keywords_to_check = ['ERROR', 'WARNING', 'EXCEPTION']
try:
for entry in os.scandir(session_path):
for entry in os.scandir(str(session_path)):
if entry.is_file():
size = entry.stat().st_size
total_size_bytes += size
@@ -210,7 +213,7 @@ class LogRegistry:
reason=reason
)
def get_old_non_whitelisted_sessions(self, cutoff_datetime: datetime) -> list[dict]:
def get_old_non_whitelisted_sessions(self, cutoff_datetime: datetime) -> list[dict[str, Any]]:
"""
Retrieves a list of sessions that are older than a specific cutoff time
and are not marked as whitelisted.
@@ -240,3 +243,4 @@ class LogRegistry:
'start_time': start_time_raw
})
return old_sessions

View File

@@ -31,8 +31,10 @@ so the AI doesn't wander outside the project workspace.
from __future__ import annotations
from pathlib import Path
from typing import Optional, Callable, Any
from typing import Optional, Callable, Any, cast
import os
import ast
import subprocess
import summarize
import outline_tool
import urllib.request
@@ -151,7 +153,7 @@ def _resolve_and_check(raw_path: str) -> tuple[Path | None, str]:
def read_file(path: str) -> str:
"""Return the UTF-8 content of a file, or an error string."""
p, err = _resolve_and_check(path)
if err:
if err or p is None:
return err
if not p.exists():
return f"ERROR: file not found: {path}"
@@ -165,7 +167,7 @@ def read_file(path: str) -> str:
def list_directory(path: str) -> str:
"""List entries in a directory. Returns a compact text table."""
p, err = _resolve_and_check(path)
if err:
if err or p is None:
return err
if not p.exists():
return f"ERROR: path not found: {path}"
@@ -195,7 +197,7 @@ def search_files(path: str, pattern: str) -> str:
pattern examples: '*.py', '**/*.toml', 'src/**/*.rs'
"""
p, err = _resolve_and_check(path)
if err:
if err or p is None:
return err
if not p.is_dir():
return f"ERROR: not a directory: {path}"
@@ -226,7 +228,7 @@ def get_file_summary(path: str) -> str:
For .toml: table keys. For .md: headings. Others: line count + preview.
"""
p, err = _resolve_and_check(path)
if err:
if err or p is None:
return err
if not p.exists():
return f"ERROR: file not found: {path}"
@@ -245,6 +247,7 @@ def py_get_skeleton(path: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
if not p.is_file() or p.suffix != ".py":
@@ -264,6 +267,7 @@ def py_get_code_outline(path: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
if not p.is_file():
@@ -279,12 +283,13 @@ def get_file_slice(path: str, start_line: int, end_line: int) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
try:
lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
start_idx = int(start_line) - 1
end_idx = int(end_line)
start_idx = start_line - 1
end_idx = end_line
return "".join(lines[start_idx:end_idx])
except Exception as e:
return f"ERROR reading slice from '{path}': {e}"
@@ -294,12 +299,13 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
try:
lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
start_idx = int(start_line) - 1
end_idx = int(end_line)
start_idx = start_line - 1
end_idx = end_line
if new_content and not new_content.endswith("\n"):
new_content += "\n"
new_lines = new_content.splitlines(keepends=True) if new_content else []
@@ -309,12 +315,11 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
except Exception as e:
return f"ERROR updating slice in '{path}': {e}"
def _get_symbol_node(tree: Any, name: str) -> Any:
def _get_symbol_node(tree: ast.AST, name: str) -> Optional[ast.AST]:
"""Helper to find an AST node by name (Class, Function, or Variable). Supports dot notation."""
import ast
parts = name.split(".")
def find_in_scope(scope_node, target_name):
def find_in_scope(scope_node: Any, target_name: str) -> Optional[ast.AST]:
# scope_node could be Module, ClassDef, or FunctionDef
body = getattr(scope_node, "body", [])
for node in body:
@@ -345,6 +350,7 @@ def py_get_definition(path: str, name: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
if not p.is_file():
@@ -352,14 +358,13 @@ def py_get_definition(path: str, name: str) -> str:
if p.suffix != ".py":
return f"ERROR: py_get_definition currently only supports .py files (unsupported: {p.suffix})"
try:
import ast
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
lines = code.splitlines(keepends=True)
tree = ast.parse(code)
node = _get_symbol_node(tree, name)
if node:
start = getattr(node, "lineno") - 1
end = getattr(node, "end_lineno")
start = cast(int, getattr(node, "lineno")) - 1
end = cast(int, getattr(node, "end_lineno"))
return "".join(lines[start:end])
return f"ERROR: could not find definition '{name}' in {path}"
except Exception as e:
@@ -370,17 +375,17 @@ def py_update_definition(path: str, name: str, new_content: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
try:
import ast
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
tree = ast.parse(code)
node = _get_symbol_node(tree, name)
if not node:
return f"ERROR: could not find definition '{name}' in {path}"
start = getattr(node, "lineno")
end = getattr(node, "end_lineno")
start = cast(int, getattr(node, "lineno"))
end = cast(int, getattr(node, "end_lineno"))
return set_file_slice(path, start, end, new_content)
except Exception as e:
return f"ERROR updating definition '{name}' in '{path}': {e}"
@@ -390,17 +395,17 @@ def py_get_signature(path: str, name: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
try:
import ast
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
lines = code.splitlines(keepends=True)
tree = ast.parse(code)
node = _get_symbol_node(tree, name)
if not node or not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
return f"ERROR: could not find function/method '{name}' in {path}"
start = getattr(node, "lineno") - 1
start = node.lineno - 1
body_start = node.body[0].lineno - 1
sig_lines = lines[start:body_start]
sig = "".join(sig_lines).strip()
@@ -420,17 +425,17 @@ def py_set_signature(path: str, name: str, new_signature: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
try:
import ast
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
code.splitlines(keepends=True)
tree = ast.parse(code)
node = _get_symbol_node(tree, name)
if not node or not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
return f"ERROR: could not find function/method '{name}' in {path}"
start = getattr(node, "lineno")
start = node.lineno
body_start_line = node.body[0].lineno
# We replace from start until body_start_line - 1
# But we must be careful about comments/docstrings between sig and body
@@ -445,10 +450,10 @@ def py_get_class_summary(path: str, name: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.exists():
return f"ERROR: file not found: {path}"
try:
import ast
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
tree = ast.parse(code)
node = _get_symbol_node(tree, name)
@@ -461,7 +466,7 @@ def py_get_class_summary(path: str, name: str) -> str:
summary.append(f" Docstring: {doc}")
for body_node in node.body:
if isinstance(body_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
start = getattr(body_node, "lineno") - 1
start = body_node.lineno - 1
body_start = body_node.body[0].lineno - 1
sig = "".join(lines[start:body_start]).strip()
summary.append(f" - {sig}")
@@ -474,18 +479,18 @@ def py_get_var_declaration(path: str, name: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.is_file() or p.suffix != ".py":
return f"ERROR: not a python file: {path}"
try:
import ast
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
lines = code.splitlines(keepends=True)
tree = ast.parse(code)
node = _get_symbol_node(tree, name)
if not node or not isinstance(node, (ast.Assign, ast.AnnAssign)):
return f"ERROR: could not find variable '{name}' in {path}"
start = getattr(node, "lineno") - 1
end = getattr(node, "end_lineno")
start = cast(int, getattr(node, "lineno")) - 1
end = cast(int, getattr(node, "end_lineno"))
return "".join(lines[start:end])
except Exception as e:
return f"ERROR retrieving variable '{name}' from '{path}': {e}"
@@ -495,17 +500,17 @@ def py_set_var_declaration(path: str, name: str, new_declaration: str) -> str:
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
if not p.is_file() or p.suffix != ".py":
return f"ERROR: not a python file: {path}"
try:
import ast
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
tree = ast.parse(code)
node = _get_symbol_node(tree, name)
if not node or not isinstance(node, (ast.Assign, ast.AnnAssign)):
return f"ERROR: could not find variable '{name}' in {path}"
start = getattr(node, "lineno")
end = getattr(node, "end_lineno")
start = cast(int, getattr(node, "lineno"))
end = cast(int, getattr(node, "end_lineno"))
return set_file_slice(path, start, end, new_declaration)
except Exception as e:
return f"ERROR updating variable '{name}' in '{path}': {e}"
@@ -516,10 +521,10 @@ def get_git_diff(path: str, base_rev: str = "HEAD", head_rev: str = "") -> str:
base_rev: The base revision (default: HEAD)
head_rev: The head revision (optional)
"""
import subprocess
p, err = _resolve_and_check(path)
if err:
return err
assert p is not None
cmd = ["git", "diff", base_rev]
if head_rev:
cmd.append(head_rev)
@@ -536,12 +541,13 @@ def py_find_usages(path: str, name: str) -> str:
"""Finds exact string matches of a symbol in a given file or directory."""
p, err = _resolve_and_check(path)
if err: return err
assert p is not None
try:
import re
pattern = re.compile(r"\b" + re.escape(name) + r"\b")
results = []
def _search_file(fp):
def _search_file(fp: Path) -> None:
if fp.name == "history.toml" or fp.name.endswith("_history.toml"): return
if not _is_allowed(fp): return
try:
@@ -573,9 +579,9 @@ def py_get_imports(path: str) -> str:
"""Parses a file's AST and returns a strict list of its dependencies."""
p, err = _resolve_and_check(path)
if err: return err
assert p is not None
if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}"
try:
import ast
code = p.read_text(encoding="utf-8")
tree = ast.parse(code)
imports = []
@@ -596,9 +602,9 @@ def py_check_syntax(path: str) -> str:
"""Runs a quick syntax check on a Python file."""
p, err = _resolve_and_check(path)
if err: return err
assert p is not None
if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}"
try:
import ast
code = p.read_text(encoding="utf-8")
ast.parse(code)
return f"Syntax OK: {path}"
@@ -611,10 +617,10 @@ def py_get_hierarchy(path: str, class_name: str) -> str:
"""Scans the project to find subclasses of a given class."""
p, err = _resolve_and_check(path)
if err: return err
import ast
subclasses = []
assert p is not None
subclasses: list[str] = []
def _search_file(fp):
def _search_file(fp: Path) -> None:
if not _is_allowed(fp): return
try:
code = fp.read_text(encoding="utf-8")
@@ -625,7 +631,8 @@ def py_get_hierarchy(path: str, class_name: str) -> str:
if isinstance(base, ast.Name) and base.id == class_name:
subclasses.append(f"{fp.name}: class {node.name}({class_name})")
elif isinstance(base, ast.Attribute) and base.attr == class_name:
subclasses.append(f"{fp.name}: class {node.name}({base.value.id}.{class_name})")
if isinstance(base.value, ast.Name):
subclasses.append(f"{fp.name}: class {node.name}({base.value.id}.{class_name})")
except Exception:
pass
try:
@@ -647,9 +654,9 @@ def py_get_docstring(path: str, name: str) -> str:
"""Extracts the docstring for a specific module, class, or function."""
p, err = _resolve_and_check(path)
if err: return err
assert p is not None
if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}"
try:
import ast
code = p.read_text(encoding="utf-8")
tree = ast.parse(code)
if not name or name == "module":
@@ -657,8 +664,10 @@ def py_get_docstring(path: str, name: str) -> str:
return doc if doc else "No module docstring found."
node = _get_symbol_node(tree, name)
if not node: return f"ERROR: could not find symbol '{name}' in {path}"
doc = ast.get_docstring(node)
return doc if doc else f"No docstring found for '{name}'."
if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, ast.Module)):
doc = ast.get_docstring(node)
return doc if doc else f"No docstring found for '{name}'."
return f"No docstring found for '{name}'."
except Exception as e:
return f"ERROR getting docstring for '{name}': {e}"
@@ -666,12 +675,13 @@ def get_tree(path: str, max_depth: int = 2) -> str:
"""Returns a directory structure up to a max depth."""
p, err = _resolve_and_check(path)
if err: return err
assert p is not None
if not p.is_dir(): return f"ERROR: not a directory: {path}"
try:
max_depth = int(max_depth)
m_depth = max_depth
def _build_tree(dir_path, current_depth, prefix=""):
if current_depth > max_depth: return []
def _build_tree(dir_path: Path, current_depth: int, prefix: str = "") -> list[str]:
if current_depth > m_depth: return []
lines = []
try:
entries = sorted(dir_path.iterdir(), key=lambda e: (e.is_file(), e.name.lower()))
@@ -706,11 +716,11 @@ class _DDGParser(HTMLParser):
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
attrs_dict = dict(attrs)
if tag == "a" and "result__url" in attrs_dict.get("class", ""):
self.current_link = attrs_dict.get("href", "")
if tag == "a" and "result__snippet" in attrs_dict.get("class", ""):
if tag == "a" and "result__url" in cast(str, attrs_dict.get("class", "")):
self.current_link = cast(str, attrs_dict.get("href", ""))
if tag == "a" and "result__snippet" in cast(str, attrs_dict.get("class", "")):
self.in_snippet = True
if tag == "h2" and "result__title" in attrs_dict.get("class", ""):
if tag == "h2" and "result__title" in cast(str, attrs_dict.get("class", "")):
self.in_title = True
def handle_endtag(self, tag: str) -> None:
@@ -777,7 +787,9 @@ def fetch_url(url: str) -> str:
"""Fetch a URL and return its text content (stripped of HTML tags)."""
# Correct duckduckgo redirect links if passed
if url.startswith("//duckduckgo.com/l/?uddg="):
url = urllib.parse.unquote(url.split("uddg=")[1].split("&")[0])
split_uddg = url.split("uddg=")
if len(split_uddg) > 1:
url = urllib.parse.unquote(split_uddg[1].split("&")[0])
if not url.startswith("http"):
url = "https://" + url
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'})
@@ -819,13 +831,13 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
Dispatch an MCP tool call by name. Returns the result as a string.
"""
# Handle aliases
path = tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", "")))
path = str(tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", ""))))
if tool_name == "read_file":
return read_file(path)
if tool_name == "list_directory":
return list_directory(path)
if tool_name == "search_files":
return search_files(path, tool_input.get("pattern", "*"))
return search_files(path, str(tool_input.get("pattern", "*")))
if tool_name == "get_file_summary":
return get_file_summary(path)
if tool_name == "py_get_skeleton":
@@ -833,47 +845,47 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
if tool_name == "py_get_code_outline":
return py_get_code_outline(path)
if tool_name == "py_get_definition":
return py_get_definition(path, tool_input.get("name", ""))
return py_get_definition(path, str(tool_input.get("name", "")))
if tool_name == "py_update_definition":
return py_update_definition(path, tool_input.get("name", ""), tool_input.get("new_content", ""))
return py_update_definition(path, str(tool_input.get("name", "")), str(tool_input.get("new_content", "")))
if tool_name == "py_get_signature":
return py_get_signature(path, tool_input.get("name", ""))
return py_get_signature(path, str(tool_input.get("name", "")))
if tool_name == "py_set_signature":
return py_set_signature(path, tool_input.get("name", ""), tool_input.get("new_signature", ""))
return py_set_signature(path, str(tool_input.get("name", "")), str(tool_input.get("new_signature", "")))
if tool_name == "py_get_class_summary":
return py_get_class_summary(path, tool_input.get("name", ""))
return py_get_class_summary(path, str(tool_input.get("name", "")))
if tool_name == "py_get_var_declaration":
return py_get_var_declaration(path, tool_input.get("name", ""))
return py_get_var_declaration(path, str(tool_input.get("name", "")))
if tool_name == "py_set_var_declaration":
return py_set_var_declaration(path, tool_input.get("name", ""), tool_input.get("new_declaration", ""))
return py_set_var_declaration(path, str(tool_input.get("name", "")), str(tool_input.get("new_declaration", "")))
if tool_name == "get_file_slice":
return get_file_slice(path, tool_input.get("start_line", 1), tool_input.get("end_line", 1))
return get_file_slice(path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)))
if tool_name == "set_file_slice":
return set_file_slice(path, tool_input.get("start_line", 1), tool_input.get("end_line", 1), tool_input.get("new_content", ""))
return set_file_slice(path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)), str(tool_input.get("new_content", "")))
if tool_name == "get_git_diff":
return get_git_diff(
path,
tool_input.get("base_rev", "HEAD"),
tool_input.get("head_rev", "")
str(tool_input.get("base_rev", "HEAD")),
str(tool_input.get("head_rev", ""))
)
if tool_name == "web_search":
return web_search(tool_input.get("query", ""))
return web_search(str(tool_input.get("query", "")))
if tool_name == "fetch_url":
return fetch_url(tool_input.get("url", ""))
return fetch_url(str(tool_input.get("url", "")))
if tool_name == "get_ui_performance":
return get_ui_performance()
if tool_name == "py_find_usages":
return py_find_usages(path, tool_input.get("name", ""))
return py_find_usages(path, str(tool_input.get("name", "")))
if tool_name == "py_get_imports":
return py_get_imports(path)
if tool_name == "py_check_syntax":
return py_check_syntax(path)
if tool_name == "py_get_hierarchy":
return py_get_hierarchy(path, tool_input.get("class_name", ""))
return py_get_hierarchy(path, str(tool_input.get("class_name", "")))
if tool_name == "py_get_docstring":
return py_get_docstring(path, tool_input.get("name", ""))
return py_get_docstring(path, str(tool_input.get("name", "")))
if tool_name == "get_tree":
return get_tree(path, tool_input.get("max_depth", 2))
return get_tree(path, int(tool_input.get("max_depth", 2)))
return f"ERROR: unknown MCP tool '{tool_name}'"
# ------------------------------------------------------------------ tool schema helpers
# These are imported by ai_client.py to build provider-specific declarations.

View File

@@ -103,9 +103,9 @@ class WorkerContext:
class Metadata:
id: str
name: str
status: str
created_at: datetime
updated_at: datetime
status: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
def to_dict(self) -> Dict[str, Any]:
return {
@@ -121,9 +121,9 @@ class Metadata:
return cls(
id=data["id"],
name=data["name"],
status=data.get("status", "todo"),
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at') else datetime.now(),
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at') else datetime.now(),
status=data.get("status"),
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at') else None,
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at') else None,
)
@dataclass

View File

@@ -13,13 +13,14 @@ class CodeOutliner:
return f"ERROR parsing code: {e}"
output = []
def get_docstring(node):
doc = ast.get_docstring(node)
if doc:
return doc.splitlines()[0]
def get_docstring(node: ast.AST) -> str | None:
if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, ast.Module)):
doc = ast.get_docstring(node)
if doc:
return doc.splitlines()[0]
return None
def walk(node, indent=0):
def walk(node: ast.AST, indent: int = 0) -> None:
if isinstance(node, ast.ClassDef):
start_line = node.lineno
end_line = getattr(node, "end_lineno", start_line)

View File

@@ -2,38 +2,38 @@ from __future__ import annotations
import time
import psutil
import threading
from typing import Any
from typing import Any, Optional, Callable
class PerformanceMonitor:
def __init__(self) -> None:
self._start_time = None
self._last_frame_time = 0.0
self._fps = 0.0
self._last_calculated_fps = 0.0
self._frame_count = 0
self._total_frame_count = 0
self._fps_last_time = time.time()
self._process = psutil.Process()
self._cpu_usage = 0.0
self._cpu_lock = threading.Lock()
self._start_time: Optional[float] = None
self._last_frame_time: float = 0.0
self._fps: float = 0.0
self._last_calculated_fps: float = 0.0
self._frame_count: int = 0
self._total_frame_count: int = 0
self._fps_last_time: float = time.time()
self._process: psutil.Process = psutil.Process()
self._cpu_usage: float = 0.0
self._cpu_lock: threading.Lock = threading.Lock()
# Input lag tracking
self._last_input_time = None
self._input_lag_ms = 0.0
self._last_input_time: Optional[float] = None
self._input_lag_ms: float = 0.0
# Alerts
self.alert_callback = None
self.thresholds = {
self.alert_callback: Optional[Callable[[str], None]] = None
self.thresholds: dict[str, float] = {
'frame_time_ms': 33.3, # < 30 FPS
'cpu_percent': 80.0,
'input_lag_ms': 100.0
}
self._last_alert_time = 0
self._alert_cooldown = 30 # seconds
self._last_alert_time: float = 0.0
self._alert_cooldown: int = 30 # seconds
# Detailed profiling
self._component_timings = {}
self._comp_start = {}
self._component_timings: dict[str, float] = {}
self._comp_start: dict[str, float] = {}
# Start CPU usage monitoring thread
self._stop_event = threading.Event()
self._cpu_thread = threading.Thread(target=self._monitor_cpu, daemon=True)
self._stop_event: threading.Event = threading.Event()
self._cpu_thread: threading.Thread = threading.Thread(target=self._monitor_cpu, daemon=True)
self._cpu_thread.start()
def _monitor_cpu(self) -> None:
@@ -107,15 +107,13 @@ class PerformanceMonitor:
def get_metrics(self) -> dict[str, Any]:
with self._cpu_lock:
cpu_usage = self._cpu_usage
metrics = {
metrics: dict[str, Any] = {
'last_frame_time_ms': self._last_frame_time,
'fps': self._last_calculated_fps,
'cpu_percent': cpu_usage,
'total_frames': self._total_frame_count,
'input_lag_ms': self._last_input_time if self._last_input_time else 0.0 # Wait, this should be the calculated lag
'input_lag_ms': self._input_lag_ms
}
# Oops, fixed the input lag logic in previous turn, let's keep it consistent
metrics['input_lag_ms'] = self._input_lag_ms
# Add detailed timings
for name, elapsed in self._component_timings.items():
metrics[f'time_{name}_ms'] = elapsed

View File

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

View File

@@ -37,4 +37,28 @@ disallow_incomplete_defs = true
ignore_missing_imports = true
explicit_package_bases = true
[tool.ruff]
# Target version
target-version = "py311"
exclude = [
"scripts/ai_style_formatter.py",
"scripts/temp_def.py",
]
[tool.ruff.lint]
# Enable standard rules
select = ["E", "F", "W"]
# Ignore style choices that conflict with project's compact style
ignore = [
"E701", # Multiple statements on one line (colon)
"E702", # Multiple statements on one line (semicolon)
"E402", # Module level import not at top of file
"E722", # Do not use bare `except`
"E501", # Line too long
"W291", # Trailing whitespace
"W293", # Blank line contains whitespace
]

View File

@@ -146,7 +146,8 @@ def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optio
ps1_path: Optional[Path] = _SCRIPTS_DIR / ps1_name
try:
ps1_path.write_text(script, encoding="utf-8")
if ps1_path:
ps1_path.write_text(script, encoding="utf-8")
except Exception as exc:
ps1_path = None
ps1_name = f"(write error: {exc})"

View File

@@ -14,7 +14,7 @@ def main():
if not client.wait_for_server(timeout=5):
print("Hook server not found. Start GUI with --enable-test-hooks")
return
sim_agent = UserSimAgent(client)
UserSimAgent(client)
# 1. Reset session to start clean
print("Resetting session...")
client.click("btn_reset")

View File

@@ -26,7 +26,7 @@ context block that replaces full file contents in the initial <context> send.
import ast
import re
from pathlib import Path
from typing import Callable
from typing import Callable, Any
# ------------------------------------------------------------------ per-type extractors
@@ -160,7 +160,7 @@ def summarise_file(path: Path, content: str) -> str:
except Exception as e:
return f"_Summariser error: {e}_"
def summarise_items(file_items: list[dict]) -> list[dict]:
def summarise_items(file_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
Given a list of file_item dicts (as returned by aggregate.build_file_items),
return a parallel list of dicts with an added `summary` key.
@@ -178,7 +178,7 @@ def summarise_items(file_items: list[dict]) -> list[dict]:
result.append({**item, "summary": summary})
return result
def build_summary_markdown(file_items: list[dict]) -> str:
def build_summary_markdown(file_items: list[dict[str, Any]]) -> str:
"""
Build a compact markdown string of file summaries, suitable for the
initial <context> block instead of full file contents.

View File

@@ -10,7 +10,7 @@ import datetime
import shutil
from pathlib import Path
from typing import Generator, Any
from unittest.mock import patch, MagicMock
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__), "..")))
@@ -41,7 +41,7 @@ class VerificationLogger:
})
def finalize(self, title: str, status: str, result_msg: str) -> None:
elapsed = round(time.time() - self.start_time, 2)
round(time.time() - self.start_time, 2)
log_file = self.logs_dir / f"{self.script_name}.txt"
with open(log_file, "w", encoding="utf-8") as f:
f.write(f"[ Test: {self.test_name} ]\n")

View File

@@ -60,7 +60,6 @@ def main() -> None:
return
# 2. Check for multi-round tool triggers
is_resume_list = is_resume and 'list_directory' in prompt
is_resume_read = is_resume and 'read_file' in prompt
is_resume_powershell = is_resume and 'run_powershell' in prompt

View File

@@ -1,4 +1,3 @@
import pytest
import sys
import os
from unittest.mock import patch, MagicMock

View File

@@ -1,4 +1,3 @@
import pytest
import sys
import os
import ai_client

View File

@@ -1,4 +1,3 @@
import pytest
from typing import Any
from unittest.mock import MagicMock, patch
import ai_client

View File

@@ -1,7 +1,7 @@
import sys
import os
from typing import Any
from unittest.mock import MagicMock, patch
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__), "..")))

View File

@@ -3,7 +3,6 @@ Tests for architecture_boundary_hardening_20260302 — Phase 2.
Tasks 2.1-2.4: MCP tool config exposure + MUTATING_TOOLS + HITL enforcement.
"""
import tomllib
import pytest
from project_manager import default_project
MUTATING_TOOLS = {"set_file_slice", "py_update_definition", "py_set_signature", "py_set_var_declaration"}
@@ -101,9 +100,8 @@ def test_mutating_tools_excludes_read_tools():
def test_mutating_tool_triggers_pre_tool_callback(monkeypatch):
"""When a mutating tool is called and pre_tool_callback is set, it must be invoked."""
import ai_client
import mcp_client
from unittest.mock import MagicMock, patch
from unittest.mock import patch
callback_called = []
def fake_callback(desc, base_dir, qa_cb):
callback_called.append(desc)
@@ -113,12 +111,11 @@ def test_mutating_tool_triggers_pre_tool_callback(monkeypatch):
tool_name = "set_file_slice"
args = {"path": "foo.py", "start_line": 1, "end_line": 2, "new_content": "x"}
# Simulate the logic from all 4 provider dispatch blocks
out = ""
_res = fake_callback(f"# MCP MUTATING TOOL: {tool_name}", ".", None)
if _res is None:
out = "USER REJECTED: tool execution cancelled"
pass
else:
out = mcp_client.dispatch(tool_name, args)
mcp_client.dispatch(tool_name, args)
assert len(callback_called) == 1, "pre_tool_callback must be called for mutating tools"
assert mock_dispatch.called

View File

@@ -1,4 +1,3 @@
import pytest
from models import Ticket
from dag_engine import TrackDAG, ExecutionEngine

View File

@@ -1,4 +1,3 @@
import pytest
from models import Ticket
from dag_engine import TrackDAG, ExecutionEngine

View File

@@ -3,7 +3,6 @@ from unittest.mock import patch, MagicMock
import json
import sys
import os
import subprocess
# Ensure the project root is in sys.path to resolve imports correctly
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

View File

@@ -68,7 +68,7 @@ def test_gemini_cli_parameter_resilience(live_gui: Any) -> None:
bridge_path_str = bridge_path.replace("\\", "/")
else:
bridge_path_str = bridge_path
with open(alias_tool_content := "tests/mock_alias_tool.py", "w") as f:
with open("tests/mock_alias_tool.py", "w") as f:
f.write(f'''import sys, json, os, subprocess
prompt = sys.stdin.read()
if '"role": "tool"' in prompt:

View File

@@ -1,6 +1,3 @@
from typing import Generator
import pytest
from unittest.mock import patch
from gui_2 import App
def test_gui2_hubs_exist_in_show_windows(app_instance: App) -> None:

View File

@@ -1,9 +1,6 @@
import pytest
from unittest.mock import patch, MagicMock
from typing import Generator
from gui_2 import App
import ai_client
from events import EventEmitter
def test_mcp_tool_call_is_dispatched(app_instance: App) -> None:
"""

View File

@@ -1,5 +1,3 @@
import pytest
from unittest.mock import patch, MagicMock
import sys
import os
from typing import Any
@@ -7,7 +5,6 @@ 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__), "..")))
from gui_2 import App
def test_diagnostics_panel_initialization(app_instance: Any) -> None:
assert "Diagnostics" in app_instance.show_windows

View File

@@ -1,16 +1,12 @@
import pytest
import sys
import os
from unittest.mock import patch
from typing import Generator, Any
from gui_2 import App
import ai_client
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
def test_gui_updates_on_event(app_instance: App) -> None:
mock_stats = {"percentage": 50.0, "current": 500, "limit": 1000}
app_instance.last_md = "mock_md"
with patch.object(app_instance, '_refresh_api_metrics') as mock_refresh:
# Simulate event (bypassing events.emit since _init_ai_and_hooks is mocked)

View File

@@ -1,10 +1,8 @@
import os
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import patch
from gui_2 import App
def test_track_proposal_editing(app_instance):
# Setup some proposed tracks

View File

@@ -1,5 +1,4 @@
import pytest
from unittest.mock import patch
from gui_2 import App
@pytest.mark.asyncio

View File

@@ -1,4 +1,3 @@
import pytest
from unittest.mock import patch
import sys
import os
@@ -41,7 +40,7 @@ def test_performance_history_updates(app_instance: Any) -> None:
def test_gui_updates_on_event(app_instance: App) -> None:
mock_stats = {"percentage": 50.0, "current": 500, "limit": 1000}
app_instance.last_md = "mock_md"
with patch('ai_client.get_token_stats', return_value=mock_stats) as mock_get_stats:
with patch('ai_client.get_token_stats', return_value=mock_stats):
app_instance._on_api_event(payload={"text": "test"})
app_instance._process_pending_gui_tasks()
assert app_instance._token_stats["percentage"] == 50.0

View File

@@ -6,7 +6,6 @@ from unittest.mock import patch
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from api_hook_client import ApiHookClient
import gui_2
def test_hooks_enabled_via_cli(mock_app) -> None:
with patch.object(sys, 'argv', ['gui_2.py', '--enable-test-hooks']):

View File

@@ -2,11 +2,8 @@
Tests for mma_agent_focus_ux_20260302 — Phase 1: Tier Tagging at Emission.
These tests are written RED-first: they fail before implementation.
"""
from typing import Generator
import pytest
from unittest.mock import patch
import ai_client
from gui_2 import App
@pytest.fixture(autouse=True)

View File

@@ -1,10 +1,6 @@
"""
Tests for mma_agent_focus_ux_20260302 — Phase 3: Focus Agent UI + Filter Logic.
"""
from typing import Generator
import pytest
from unittest.mock import patch
from gui_2 import App
def test_ui_focus_agent_state_var_exists(app_instance):

View File

@@ -1,4 +1,3 @@
import pytest
import json
from unittest.mock import patch
import time

View File

@@ -1,6 +1,4 @@
from typing import Generator
import pytest
from unittest.mock import patch, MagicMock
from unittest.mock import patch
from gui_2 import App
def test_cb_ticket_retry(app_instance: App) -> None:

View File

@@ -1,4 +1,3 @@
import pytest
import sys
import os
import hashlib

View File

@@ -1,7 +1,4 @@
"""Tests for context & token visualization (Track: context_token_viz_20260301)."""
from typing import Generator
from unittest.mock import patch
import pytest
import ai_client
from ai_client import _add_bleed_derived, get_history_bleed_stats

View File

@@ -1,4 +1,3 @@
import pytest
import time
from api_hook_client import ApiHookClient

View File

@@ -1,4 +1,3 @@
import pytest
def test_vlogger_available(vlogger):
vlogger.log_state("Test", "Before", "After")

View File

@@ -22,13 +22,14 @@ Usage
import dearpygui.dearpygui as dpg
from pathlib import Path
from typing import Any
# ------------------------------------------------------------------ palettes
# Colour key names match the DPG mvThemeCol_* constants (string lookup below).
# Only keys that differ from DPG defaults need to be listed.
_PALETTES: dict[str, dict] = {
_PALETTES: dict[str, dict[str, Any]] = {
"DPG Default": {}, # empty = reset to DPG built-in defaults
"10x Dark": {
# Window / frame chrome
@@ -285,11 +286,11 @@ def get_current_font_size() -> float:
def get_current_scale() -> float:
return _current_scale
def get_palette_colours(name: str) -> dict:
def get_palette_colours(name: str) -> dict[str, Any]:
"""Return a copy of the colour dict for the named palette."""
return dict(_PALETTES.get(name, {}))
def apply(palette_name: str, overrides: dict | None = None) -> None:
def apply(palette_name: str, overrides: dict[str, Any] | None = None) -> None:
"""
Build a global DPG theme from the named palette plus optional per-colour
overrides, and bind it as the default theme.
@@ -368,7 +369,7 @@ def set_scale(factor: float) -> None:
_current_scale = factor
dpg.set_global_font_scale(factor)
def save_to_config(config: dict) -> None:
def save_to_config(config: dict[str, Any]) -> None:
"""Persist theme settings into the config dict under [theme]."""
config.setdefault("theme", {})
config["theme"]["palette"] = _current_palette
@@ -376,7 +377,7 @@ def save_to_config(config: dict) -> None:
config["theme"]["font_size"] = _current_font_size
config["theme"]["scale"] = _current_scale
def load_from_config(config: dict) -> None:
def load_from_config(config: dict[str, Any]) -> None:
"""Read [theme] from config and apply everything."""
t = config.get("theme", {})
palette = t.get("palette", "DPG Default")