From d36632c21ad7ad2192ebddb1909c933e8c202a32 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 28 Feb 2026 09:06:45 -0500 Subject: [PATCH] checkpoint: massive refactor --- aggregate.py | 545 +- ai_client.py | 3212 ++++----- api_hook_client.py | 428 +- api_hooks.py | 619 +- conductor/tests/diag_subagent.py | 35 +- conductor/tests/test_infrastructure.py | 84 +- conductor/tests/test_mma_exec.py | 239 +- conductor/tests/test_mma_skeleton.py | 39 +- conductor/tracks.md | 2 +- .../python_style_refactor_20260227/plan.md | 16 +- .../python_style_refactor_20260227/spec.md | 25 +- .../plan.md | 2 +- conductor_tech_lead.py | 129 +- config.toml | 2 +- dag_engine.py | 200 +- debug_ast.py | 8 +- debug_ast_2.py | 134 +- events.py | 81 +- file_cache.py | 248 +- gemini.py | 34 +- gemini_cli_adapter.py | 225 +- gui_2.py | 6029 ++++++++--------- gui_legacy.py | 4635 ++++++------- inspect_ast.py | 6 +- log_pruner.py | 77 +- log_registry.py | 362 +- mcp_client.py | 1724 +++-- mma_prompts.py | 14 +- models.py | 244 +- multi_agent_conductor.py | 520 +- orchestrator_pm.py | 208 +- outline_tool.py | 98 +- performance_monitor.py | 225 +- project_manager.py | 627 +- refactor_ui_task.toml | 10 + reproduce_issue.py | 53 +- reproduce_missing_hints.py | 21 + run_tests.py | 150 +- scripts/cli_tool_bridge.py | 11 +- scripts/mma_exec.py | 8 +- session_logger.py | 299 +- shell_runner.py | 63 +- simulation/live_walkthrough.py | 123 +- simulation/ping_pong.py | 77 +- simulation/sim_ai_settings.py | 55 +- simulation/sim_base.py | 133 +- simulation/sim_context.py | 135 +- simulation/sim_execution.py | 130 +- simulation/sim_tools.py | 71 +- simulation/user_agent.py | 73 +- simulation/workflow_sim.py | 142 +- summarize.py | 288 +- test_mma_persistence.py | 43 +- tests/conftest.py | 138 +- tests/mock_alias_tool.py | 21 + tests/mock_gemini_cli.py | 186 +- tests/temp_liveaisettingssim.toml | 2 + tests/temp_liveaisettingssim_history.toml | 2 +- tests/temp_livecontextsim.toml | 2 + tests/temp_livecontextsim_history.toml | 6 +- tests/temp_liveexecutionsim.toml | 2 + tests/temp_liveexecutionsim_history.toml | 2 +- tests/temp_livetoolssim.toml | 2 + tests/temp_livetoolssim_history.toml | 2 +- tests/temp_project_history.toml | 6 +- tests/test_agent_capabilities.py | 4 +- tests/test_agent_tools_wiring.py | 18 +- tests/test_ai_client_cli.py | 60 +- tests/test_ai_client_list_models.py | 17 +- tests/test_ai_style_formatter.py | 121 +- tests/test_api_events.py | 193 +- tests/test_api_hook_client.py | 104 +- tests/test_api_hook_extensions.py | 110 +- tests/test_ast_parser.py | 103 +- tests/test_ast_parser_curated.py | 34 +- tests/test_async_events.py | 65 +- tests/test_auto_whitelist.py | 110 +- tests/test_cli_tool_bridge.py | 103 +- tests/test_cli_tool_bridge_mapping.py | 61 +- tests/test_conductor_api_hook_integration.py | 78 +- tests/test_conductor_engine.py | 432 +- tests/test_conductor_tech_lead.py | 194 +- tests/test_dag_engine.py | 116 +- tests/test_deepseek_infra.py | 59 +- tests/test_deepseek_provider.py | 217 +- tests/test_execution_engine.py | 194 +- tests/test_extended_sims.py | 60 +- tests/test_gemini_cli_adapter.py | 184 +- tests/test_gemini_cli_adapter_parity.py | 249 +- tests/test_gemini_cli_edge_cases.py | 223 +- tests/test_gemini_cli_integration.py | 232 +- tests/test_gemini_cli_parity_regression.py | 61 +- tests/test_gemini_metrics.py | 63 +- tests/test_gui2_events.py | 59 +- tests/test_gui2_layout.py | 64 +- tests/test_gui2_mcp.py | 119 +- tests/test_gui2_parity.py | 102 +- tests/test_gui2_performance.py | 126 +- tests/test_gui_async_events.py | 125 +- tests/test_gui_diagnostics.py | 87 +- tests/test_gui_events.py | 84 +- tests/test_gui_performance_requirements.py | 49 +- tests/test_gui_stress_performance.py | 72 +- tests/test_gui_updates.py | 155 +- tests/test_headless_service.py | 292 +- tests/test_headless_verification.py | 212 +- tests/test_history_management.py | 295 +- tests/test_hooks.py | 57 +- tests/test_layout_reorganization.py | 125 +- tests/test_live_gui_integration.py | 198 +- tests/test_live_workflow.py | 137 +- tests/test_log_management_ui.py | 129 +- tests/test_log_pruner.py | 80 +- tests/test_log_registry.py | 304 +- tests/test_logging_e2e.py | 115 +- tests/test_mcp_perf_tool.py | 15 +- tests/test_mma_dashboard_refresh.py | 96 +- tests/test_mma_models.py | 244 +- tests/test_mma_orchestration_gui.py | 237 +- tests/test_mma_prompts.py | 72 +- tests/test_mma_ticket_actions.py | 78 +- tests/test_orchestration_logic.py | 188 +- tests/test_orchestrator_pm.py | 128 +- tests/test_orchestrator_pm_history.py | 113 +- tests/test_performance_monitor.py | 30 +- tests/test_process_pending_gui_tasks.py | 93 +- tests/test_project_manager_tracks.py | 140 +- tests/test_session_logging.py | 92 +- tests/test_sim_ai_settings.py | 55 +- tests/test_sim_base.py | 38 +- tests/test_sim_context.py | 73 +- tests/test_sim_execution.py | 69 +- tests/test_sim_tools.py | 46 +- tests/test_spawn_interception.py | 127 +- tests/test_sync_hooks.py | 95 +- tests/test_tier4_interceptor.py | 337 +- tests/test_tiered_context.py | 217 +- tests/test_token_usage.py | 10 +- tests/test_track_state_persistence.py | 116 +- tests/test_track_state_schema.py | 298 +- tests/test_tree_sitter_setup.py | 39 +- tests/test_user_agent.py | 20 +- tests/test_workflow_sim.py | 54 +- tests/verify_mma_gui_robust.py | 145 +- tests/visual_diag.py | 105 +- tests/visual_mma_verification.py | 226 +- tests/visual_orchestration_verification.py | 129 +- tests/visual_sim_mma_v2.py | 53 +- theme_2.py | 416 +- 149 files changed, 16255 insertions(+), 17722 deletions(-) create mode 100644 refactor_ui_task.toml create mode 100644 reproduce_missing_hints.py create mode 100644 tests/mock_alias_tool.py diff --git a/aggregate.py b/aggregate.py index 538e5a9..3e09fc3 100644 --- a/aggregate.py +++ b/aggregate.py @@ -15,98 +15,94 @@ import tomllib import re import glob from pathlib import Path, PureWindowsPath +from typing import Any import summarize import project_manager from file_cache import ASTParser def find_next_increment(output_dir: Path, namespace: str) -> int: - pattern = re.compile(rf"^{re.escape(namespace)}_(\d+)\.md$") - max_num = 0 - for f in output_dir.iterdir(): - if f.is_file(): - match = pattern.match(f.name) - if match: - max_num = max(max_num, int(match.group(1))) - return max_num + 1 + pattern = re.compile(rf"^{re.escape(namespace)}_(\d+)\.md$") + max_num = 0 + for f in output_dir.iterdir(): + if f.is_file(): + match = pattern.match(f.name) + if match: + max_num = max(max_num, int(match.group(1))) + return max_num + 1 def is_absolute_with_drive(entry: str) -> bool: - try: - p = PureWindowsPath(entry) - return p.drive != "" - except Exception: - return False + try: + p = PureWindowsPath(entry) + return p.drive != "" + except Exception: + return False def resolve_paths(base_dir: Path, entry: str) -> list[Path]: - has_drive = is_absolute_with_drive(entry) - is_wildcard = "*" in entry - - matches = [] - if is_wildcard: - root = Path(entry) if has_drive else base_dir / entry - matches = [Path(p) for p in glob.glob(str(root), recursive=True) if Path(p).is_file()] - else: - p = Path(entry) if has_drive else (base_dir / entry).resolve() - matches = [p] - - # Blacklist filter - filtered = [] - for p in matches: - name = p.name.lower() - if name == "history.toml" or name.endswith("_history.toml"): - continue - filtered.append(p) - - return sorted(filtered) + has_drive = is_absolute_with_drive(entry) + is_wildcard = "*" in entry + matches = [] + if is_wildcard: + root = Path(entry) if has_drive else base_dir / entry + matches = [Path(p) for p in glob.glob(str(root), recursive=True) if Path(p).is_file()] + else: + p = Path(entry) if has_drive else (base_dir / entry).resolve() + matches = [p] + # Blacklist filter + filtered = [] + for p in matches: + name = p.name.lower() + if name == "history.toml" or name.endswith("_history.toml"): + continue + filtered.append(p) + return sorted(filtered) def build_discussion_section(history: list[str]) -> str: - sections = [] - for i, paste in enumerate(history, start=1): - sections.append(f"### Discussion Excerpt {i}\n\n{paste.strip()}") - return "\n\n---\n\n".join(sections) + sections = [] + for i, paste in enumerate(history, start=1): + sections.append(f"### Discussion Excerpt {i}\n\n{paste.strip()}") + return "\n\n---\n\n".join(sections) -def build_files_section(base_dir: Path, files: list[str | dict]) -> str: - sections = [] - for entry_raw in files: - if isinstance(entry_raw, dict): - entry = entry_raw.get("path") - else: - entry = entry_raw - - paths = resolve_paths(base_dir, entry) - if not paths: - sections.append(f"### `{entry}`\n\n```text\nERROR: no files matched: {entry}\n```") - continue - for path in paths: - suffix = path.suffix.lstrip(".") - lang = suffix if suffix else "text" - try: - content = path.read_text(encoding="utf-8") - except FileNotFoundError: - content = f"ERROR: file not found: {path}" - except Exception as e: - content = f"ERROR: {e}" - original = entry if "*" not in entry else str(path) - sections.append(f"### `{original}`\n\n```{lang}\n{content}\n```") - return "\n\n---\n\n".join(sections) +def build_files_section(base_dir: Path, files: list[str | dict[str, Any]]) -> str: + sections = [] + for entry_raw in files: + if isinstance(entry_raw, dict): + entry = entry_raw.get("path") + else: + entry = entry_raw + paths = resolve_paths(base_dir, entry) + if not paths: + sections.append(f"### `{entry}`\n\n```text\nERROR: no files matched: {entry}\n```") + continue + for path in paths: + suffix = path.suffix.lstrip(".") + lang = suffix if suffix else "text" + try: + content = path.read_text(encoding="utf-8") + except FileNotFoundError: + content = f"ERROR: file not found: {path}" + except Exception as e: + content = f"ERROR: {e}" + original = entry if "*" not in entry else str(path) + sections.append(f"### `{original}`\n\n```{lang}\n{content}\n```") + return "\n\n---\n\n".join(sections) def build_screenshots_section(base_dir: Path, screenshots: list[str]) -> str: - sections = [] - for entry in screenshots: - paths = resolve_paths(base_dir, entry) - if not paths: - sections.append(f"### `{entry}`\n\n_ERROR: no files matched: {entry}_") - continue - for path in paths: - original = entry if "*" not in entry else str(path) - if not path.exists(): - sections.append(f"### `{original}`\n\n_ERROR: file not found: {path}_") - continue - sections.append(f"### `{original}`\n\n![{path.name}]({path.as_posix()})") - return "\n\n---\n\n".join(sections) + sections = [] + for entry in screenshots: + paths = resolve_paths(base_dir, entry) + if not paths: + sections.append(f"### `{entry}`\n\n_ERROR: no files matched: {entry}_") + continue + for path in paths: + original = entry if "*" not in entry else str(path) + if not path.exists(): + sections.append(f"### `{original}`\n\n_ERROR: file not found: {path}_") + continue + sections.append(f"### `{original}`\n\n![{path.name}]({path.as_posix()})") + return "\n\n---\n\n".join(sections) - -def build_file_items(base_dir: Path, files: list[str | dict]) -> list[dict]: - """ +def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[dict[str, Any]]: + """ Return a list of dicts describing each file, for use by ai_client when it wants to upload individual files rather than inline everything as markdown. @@ -118,240 +114,213 @@ def build_file_items(base_dir: Path, files: list[str | dict]) -> list[dict]: mtime : float (last modification time, for skip-if-unchanged optimization) tier : int | None (optional tier for context management) """ - items = [] - for entry_raw in files: - if isinstance(entry_raw, dict): - entry = entry_raw.get("path") - tier = entry_raw.get("tier") - else: - entry = entry_raw - tier = None + items = [] + for entry_raw in files: + if isinstance(entry_raw, dict): + entry = entry_raw.get("path") + tier = entry_raw.get("tier") + else: + entry = entry_raw + tier = None + 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}) + continue + for path in paths: + try: + content = path.read_text(encoding="utf-8") + mtime = path.stat().st_mtime + error = False + except FileNotFoundError: + content = f"ERROR: file not found: {path}" + mtime = 0.0 + error = True + except Exception as e: + content = f"ERROR: {e}" + mtime = 0.0 + error = True + items.append({"path": path, "entry": entry, "content": content, "error": error, "mtime": mtime, "tier": tier}) + return items - 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}) - continue - for path in paths: - try: - content = path.read_text(encoding="utf-8") - mtime = path.stat().st_mtime - error = False - except FileNotFoundError: - content = f"ERROR: file not found: {path}" - mtime = 0.0 - error = True - except Exception as e: - content = f"ERROR: {e}" - mtime = 0.0 - error = True - items.append({"path": path, "entry": entry, "content": content, "error": error, "mtime": mtime, "tier": tier}) - return items - -def build_summary_section(base_dir: Path, files: list[str | dict]) -> str: - """ +def build_summary_section(base_dir: Path, files: list[str | dict[str, Any]]) -> str: + """ Build a compact summary section using summarize.py — one short block per file. Used as the initial block instead of full file contents. """ - items = build_file_items(base_dir, files) - return summarize.build_summary_markdown(items) + items = build_file_items(base_dir, files) + return summarize.build_summary_markdown(items) -def _build_files_section_from_items(file_items: list[dict]) -> str: - """Build the files markdown section from pre-read file items (avoids double I/O).""" - sections = [] - for item in file_items: - path = item.get("path") - entry = item.get("entry", "unknown") - content = 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" - lang = suffix if suffix else "text" - original = entry if "*" not in entry else str(path) - sections.append(f"### `{original}`\n\n```{lang}\n{content}\n```") - return "\n\n---\n\n".join(sections) +def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str: + """Build the files markdown section from pre-read file items (avoids double I/O).""" + sections = [] + for item in file_items: + path = item.get("path") + entry = item.get("entry", "unknown") + content = 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" + lang = suffix if suffix else "text" + original = entry if "*" not in entry else str(path) + sections.append(f"### `{original}`\n\n```{lang}\n{content}\n```") + return "\n\n---\n\n".join(sections) +def build_markdown_from_items(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str], summary_only: bool = False) -> str: + """Build markdown from pre-read file items instead of re-reading from disk.""" + parts = [] + # STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits + if file_items: + if summary_only: + parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items)) + else: + parts.append("## Files\n\n" + _build_files_section_from_items(file_items)) + if screenshots: + parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots)) + # DYNAMIC SUFFIX: History changes every turn, must go last + if history: + parts.append("## Discussion History\n\n" + build_discussion_section(history)) + return "\n\n---\n\n".join(parts) -def build_markdown_from_items(file_items: list[dict], screenshot_base_dir: Path, screenshots: list[str], history: list[str], summary_only: bool = False) -> str: - """Build markdown from pre-read file items instead of re-reading from disk.""" - parts = [] - # STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits - if file_items: - if summary_only: - parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items)) - else: - parts.append("## Files\n\n" + _build_files_section_from_items(file_items)) - if screenshots: - parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots)) - # DYNAMIC SUFFIX: History changes every turn, must go last - if history: - parts.append("## Discussion History\n\n" + build_discussion_section(history)) - return "\n\n---\n\n".join(parts) - - -def build_markdown_no_history(file_items: list[dict], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False) -> str: - """Build markdown with only files + screenshots (no history). Used for stable caching.""" - return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history=[], summary_only=summary_only) - +def build_markdown_no_history(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False) -> str: + """Build markdown with only files + screenshots (no history). Used for stable caching.""" + return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history=[], summary_only=summary_only) def build_discussion_text(history: list[str]) -> str: - """Build just the discussion history section text. Returns empty string if no history.""" - if not history: - return "" - return "## Discussion History\n\n" + build_discussion_section(history) + """Build just the discussion history section text. Returns empty string if no history.""" + if not history: + return "" + return "## Discussion History\n\n" + build_discussion_section(history) - -def build_tier1_context(file_items: list[dict], screenshot_base_dir: Path, screenshots: list[str], history: list[str]) -> str: - """ +def build_tier1_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str]) -> str: + """ Tier 1 Context: Strategic/Orchestration. Full content for core conductor files and files with tier=1, summaries for others. """ - core_files = {"product.md", "tech-stack.md", "workflow.md", "tracks.md"} - - parts = [] - - # Files section - if file_items: - sections = [] - for item in file_items: - path = item.get("path") - name = path.name if 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```") - else: - # Summarize - sections.append("### `" + (item.get("entry") or str(path)) + "`\n\n" + - summarize.summarise_file(path, item.get("content", ""))) - - parts.append("## Files (Tier 1 - Mixed)\n\n" + "\n\n---\n\n".join(sections)) + core_files = {"product.md", "tech-stack.md", "workflow.md", "tracks.md"} + parts = [] + # Files section + if file_items: + sections = [] + for item in file_items: + path = item.get("path") + name = path.name if 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```") + else: + # Summarize + sections.append("### `" + (item.get("entry") or str(path)) + "`\n\n" + + summarize.summarise_file(path, 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)) + if history: + parts.append("## Discussion History\n\n" + build_discussion_section(history)) + return "\n\n---\n\n".join(parts) - if screenshots: - parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots)) - - if history: - parts.append("## Discussion History\n\n" + build_discussion_section(history)) - - return "\n\n---\n\n".join(parts) - - -def build_tier2_context(file_items: list[dict], screenshot_base_dir: Path, screenshots: list[str], history: list[str]) -> str: - """ +def build_tier2_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str]) -> str: + """ Tier 2 Context: Architectural/Tech Lead. Full content for all files (standard behavior). """ - return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history, summary_only=False) + return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history, summary_only=False) - -def build_tier3_context(file_items: list[dict], screenshot_base_dir: Path, screenshots: list[str], history: list[str], focus_files: list[str]) -> str: - """ +def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str], focus_files: list[str]) -> str: + """ Tier 3 Context: Execution/Worker. Full content for focus_files and files with tier=3, summaries/skeletons for others. """ - parts = [] - - if file_items: - sections = [] - for item in file_items: - path = item.get("path") - entry = 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: - 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", "") - if path and path.suffix == ".py" and not item.get("error"): - try: - parser = ASTParser("python") - skeleton = parser.get_skeleton(content) - sections.append(f"### `{entry or path_str}` (AST Skeleton)\n\n```python\n{skeleton}\n```") - except Exception as e: - # 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)) - - parts.append("## Files (Tier 3 - Focused)\n\n" + "\n\n---\n\n".join(sections)) + parts = [] + if file_items: + sections = [] + for item in file_items: + path = item.get("path") + entry = 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: + 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", "") + if path and path.suffix == ".py" and not item.get("error"): + try: + parser = ASTParser("python") + skeleton = parser.get_skeleton(content) + sections.append(f"### `{entry or path_str}` (AST Skeleton)\n\n```python\n{skeleton}\n```") + except Exception as e: + # 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)) + 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)) + if history: + parts.append("## Discussion History\n\n" + build_discussion_section(history)) + return "\n\n---\n\n".join(parts) - if screenshots: - parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots)) - - if history: - parts.append("## Discussion History\n\n" + build_discussion_section(history)) - - return "\n\n---\n\n".join(parts) +def build_markdown(base_dir: Path, files: list[str | dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str], summary_only: bool = False) -> str: + parts = [] + # STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits + if files: + if summary_only: + parts.append("## Files (Summary)\n\n" + build_summary_section(base_dir, files)) + else: + parts.append("## Files\n\n" + build_files_section(base_dir, files)) + if screenshots: + parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots)) + # DYNAMIC SUFFIX: History changes every turn, must go last + if history: + parts.append("## Discussion History\n\n" + build_discussion_section(history)) + return "\n\n---\n\n".join(parts) +def run(config: dict[str, Any]) -> tuple[str, Path, list[dict[str, Any]]]: + namespace = config.get("project", {}).get("name") + if not namespace: + namespace = config.get("output", {}).get("namespace", "project") + output_dir = Path(config["output"]["output_dir"]) + base_dir = Path(config["files"]["base_dir"]) + files = config["files"].get("paths", []) + screenshot_base_dir = Path(config.get("screenshots", {}).get("base_dir", ".")) + screenshots = config.get("screenshots", {}).get("paths", []) + history = config.get("discussion", {}).get("history", []) + output_dir.mkdir(parents=True, exist_ok=True) + increment = find_next_increment(output_dir, namespace) + output_file = output_dir / f"{namespace}_{increment:03d}.md" + # Build file items once, then construct markdown from them (avoids double I/O) + file_items = build_file_items(base_dir, files) + summary_only = config.get("project", {}).get("summary_only", False) + markdown = build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history, + summary_only=summary_only) + output_file.write_text(markdown, encoding="utf-8") + return markdown, output_file, file_items -def build_markdown(base_dir: Path, files: list[str | dict], screenshot_base_dir: Path, screenshots: list[str], history: list[str], summary_only: bool = False) -> str: - parts = [] - # STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits - if files: - if summary_only: - parts.append("## Files (Summary)\n\n" + build_summary_section(base_dir, files)) - else: - parts.append("## Files\n\n" + build_files_section(base_dir, files)) - if screenshots: - parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots)) - # DYNAMIC SUFFIX: History changes every turn, must go last - if history: - parts.append("## Discussion History\n\n" + build_discussion_section(history)) - return "\n\n---\n\n".join(parts) - -def run(config: dict) -> tuple[str, Path, list[dict]]: - namespace = config.get("project", {}).get("name") - if not namespace: - namespace = config.get("output", {}).get("namespace", "project") - output_dir = Path(config["output"]["output_dir"]) - base_dir = Path(config["files"]["base_dir"]) - files = config["files"].get("paths", []) - screenshot_base_dir = Path(config.get("screenshots", {}).get("base_dir", ".")) - screenshots = config.get("screenshots", {}).get("paths", []) - history = config.get("discussion", {}).get("history", []) - - output_dir.mkdir(parents=True, exist_ok=True) - increment = find_next_increment(output_dir, namespace) - output_file = output_dir / f"{namespace}_{increment:03d}.md" - # Build file items once, then construct markdown from them (avoids double I/O) - file_items = build_file_items(base_dir, files) - summary_only = config.get("project", {}).get("summary_only", False) - markdown = build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history, - summary_only=summary_only) - output_file.write_text(markdown, encoding="utf-8") - return markdown, output_file, file_items - -def main(): - # Load global config to find active project - config_path = Path("config.toml") - if not config_path.exists(): - print("config.toml not found.") - return - - with open(config_path, "rb") as f: - global_cfg = tomllib.load(f) - - active_path = global_cfg.get("projects", {}).get("active") - if not active_path: - print("No active project found in config.toml.") - return - - # Use project_manager to load project (handles history segregation) - proj = project_manager.load_project(active_path) - # Use flat_config to make it compatible with aggregate.run() - config = project_manager.flat_config(proj) - - markdown, output_file, _ = run(config) - print(f"Written: {output_file}") +def main() -> None: +# Load global config to find active project + config_path = Path("config.toml") + if not config_path.exists(): + print("config.toml not found.") + return + with open(config_path, "rb") as f: + global_cfg = tomllib.load(f) + active_path = global_cfg.get("projects", {}).get("active") + if not active_path: + print("No active project found in config.toml.") + return + # Use project_manager to load project (handles history segregation) + proj = project_manager.load_project(active_path) + # Use flat_config to make it compatible with aggregate.run() + config = project_manager.flat_config(proj) + markdown, output_file, _ = run(config) + print(f"Written: {output_file}") if __name__ == "__main__": - main() + main() diff --git a/ai_client.py b/ai_client.py index 4bdd575..5734062 100644 --- a/ai_client.py +++ b/ai_client.py @@ -21,7 +21,7 @@ import difflib import threading import requests from pathlib import Path -from typing import Optional, Callable +from typing import Optional, Callable, Any import os import project_manager import file_cache @@ -42,18 +42,18 @@ _history_trunc_limit: int = 8000 # Global event emitter for API lifecycle events events = EventEmitter() -def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000): - global _temperature, _max_tokens, _history_trunc_limit - _temperature = temp - _max_tokens = max_tok - _history_trunc_limit = trunc_limit +def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000) -> None: + global _temperature, _max_tokens, _history_trunc_limit + _temperature = temp + _max_tokens = max_tok + _history_trunc_limit = trunc_limit def get_history_trunc_limit() -> int: - return _history_trunc_limit + return _history_trunc_limit -def set_history_trunc_limit(val: int): - global _history_trunc_limit - _history_trunc_limit = val +def set_history_trunc_limit(val: int) -> None: + global _history_trunc_limit + _history_trunc_limit = val _gemini_client = None _gemini_chat = None @@ -101,942 +101,838 @@ _MAX_TOOL_OUTPUT_BYTES = 500_000 _ANTHROPIC_CHUNK_SIZE = 120_000 _SYSTEM_PROMPT = ( - "You are a helpful coding assistant with access to a PowerShell tool and MCP tools (file access: read_file, list_directory, search_files, get_file_summary, web access: web_search, fetch_url). " - "When calling file/directory tools, always use the 'path' parameter for the target path. " - "When asked to create or edit files, prefer targeted edits over full rewrites. " - "Always explain what you are doing before invoking the tool.\n\n" - "When writing or rewriting large files (especially those containing quotes, backticks, or special characters), " - "avoid python -c with inline strings. Instead: (1) write a .py helper script to disk using a PS here-string " - "(@'...'@ for literal content), (2) run it with `python