organizing (mostly aggregate.py)

This commit is contained in:
ed
2026-06-05 22:34:26 -04:00
parent 8b83c5d0b7
commit aa56981c87
5 changed files with 148 additions and 186 deletions
+128 -150
View File
@@ -12,19 +12,24 @@ Instead of sending every file to the AI raw (which blows up tokens), this uses a
This is essential for keeping prompt tokens low while giving the AI enough structural info This is essential for keeping prompt tokens low while giving the AI enough structural info
to use the MCP tools to fetch only what it needs. to use the MCP tools to fetch only what it needs.
""" """
import ast
import glob import glob
import os import os
import re import re
import tomllib import tomllib
import traceback
from pathlib import Path, PureWindowsPath from pathlib import Path, PureWindowsPath
from typing import Any, cast from typing import Any, cast
from src import beads_client from src import beads_client
from src import mcp_client
from src import project_manager from src import project_manager
from src import summarize from src import summarize
from src.file_cache import ASTParser from src.fuzzy_anchor import FuzzyAnchor
from src.file_cache import ASTParser
from src.paths import get_config_path
from src.performance_monitor import get_monitor from src.performance_monitor import get_monitor
@@ -50,17 +55,16 @@ def resolve_paths(base_dir: Path, entry: str) -> list[Path]:
is_wildcard = "*" in entry is_wildcard = "*" in entry
matches = [] matches = []
if is_wildcard: if is_wildcard:
root = Path(entry) if has_drive else base_dir / entry 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()] matches = [Path(p) for p in glob.glob(str(root), recursive=True) if Path(p).is_file()]
else: else:
p = Path(entry) if has_drive else (base_dir / entry).resolve() p = Path(entry) if has_drive else (base_dir / entry).resolve()
matches = [p] matches = [p]
# Blacklist filter # Blacklist filter
filtered = [] filtered = []
for p in matches: for p in matches:
name = p.name.lower() name = p.name.lower()
if name == "history.toml" or name.endswith("_history.toml"): if name == "history.toml" or name.endswith("_history.toml"): continue
continue
filtered.append(p) filtered.append(p)
return sorted(filtered) return sorted(filtered)
@@ -93,7 +97,6 @@ def compute_file_stats(abs_path: str) -> dict[str, int]:
content = f.read() content = f.read()
stats["lines"] = len(content.splitlines()) stats["lines"] = len(content.splitlines())
if abs_path.endswith('.py'): if abs_path.endswith('.py'):
import ast
try: try:
tree = ast.parse(content) tree = ast.parse(content)
stats["ast_elements"] = sum(1 for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))) stats["ast_elements"] = sum(1 for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)))
@@ -111,19 +114,19 @@ def build_discussion_section(history: list[Any]) -> str:
sections = [] sections = []
for i, entry in enumerate(history, start=1): for i, entry in enumerate(history, start=1):
if isinstance(entry, dict): if isinstance(entry, dict):
role = entry.get("role", "Unknown") role = entry.get("role", "Unknown")
content = entry.get("content", "").strip() content = entry.get("content", "").strip()
text = f"{role}: {content}" text = f"{role}: {content}"
else: else:
text = str(entry).strip() text = str(entry).strip()
sections.append(f"### Discussion Excerpt {i}\n\n{text}") sections.append(f"### Discussion Excerpt {i}\n\n{text}")
return "\n\n---\n\n".join(sections) return "\n\n---\n\n".join(sections)
def build_screenshots_section(base_dir: Path, screenshots: list[str]) -> str: def build_screenshots_section(base_dir: Path, screenshots: list[str]) -> str:
sections = [] sections = []
for entry in screenshots: for entry in screenshots:
if not entry or not isinstance(entry, str): if not entry or not isinstance(entry, str): continue
continue
paths = resolve_paths(base_dir, entry) paths = resolve_paths(base_dir, entry)
if not paths: if not paths:
sections.append(f"### `{entry}`\n\n_ERROR: no files matched: {entry}_") sections.append(f"### `{entry}`\n\n_ERROR: no files matched: {entry}_")
@@ -158,63 +161,61 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
parser = None parser = None
for entry_raw in files: for entry_raw in files:
if isinstance(entry_raw, dict): if isinstance(entry_raw, dict):
entry = cast(str, entry_raw.get("path", "")) entry = cast(str, entry_raw.get("path", ""))
tier = entry_raw.get("tier") tier = entry_raw.get("tier")
auto_aggregate = entry_raw.get("auto_aggregate", True) auto_aggregate = entry_raw.get("auto_aggregate", True)
force_full = entry_raw.get("force_full", False) force_full = entry_raw.get("force_full", False)
view_mode = entry_raw.get("view_mode", "full") view_mode = entry_raw.get("view_mode", "full")
if force_full: if force_full: view_mode = "full"
view_mode = "full" ast_signatures = entry_raw.get("ast_signatures", False)
ast_signatures = entry_raw.get("ast_signatures", False)
ast_definitions = entry_raw.get("ast_definitions", False) ast_definitions = entry_raw.get("ast_definitions", False)
ast_mask = entry_raw.get("ast_mask", {}) ast_mask = entry_raw.get("ast_mask", {})
custom_slices = entry_raw.get("custom_slices", []) custom_slices = entry_raw.get("custom_slices", [])
elif hasattr(entry_raw, "path"): elif hasattr(entry_raw, "path"):
entry = entry_raw.path entry = entry_raw.path
tier = getattr(entry_raw, "tier", None) tier = getattr(entry_raw, "tier", None)
auto_aggregate = getattr(entry_raw, "auto_aggregate", True) auto_aggregate = getattr(entry_raw, "auto_aggregate", True)
force_full = getattr(entry_raw, "force_full", False) force_full = getattr(entry_raw, "force_full", False)
view_mode = getattr(entry_raw, "view_mode", "full") view_mode = getattr(entry_raw, "view_mode", "full")
if force_full: if force_full: view_mode = "full"
view_mode = "full" ast_signatures = getattr(entry_raw, "ast_signatures", False)
ast_signatures = getattr(entry_raw, "ast_signatures", False)
ast_definitions = getattr(entry_raw, "ast_definitions", False) ast_definitions = getattr(entry_raw, "ast_definitions", False)
ast_mask = getattr(entry_raw, "ast_mask", {}) ast_mask = getattr(entry_raw, "ast_mask", {})
custom_slices = getattr(entry_raw, "custom_slices", []) custom_slices = getattr(entry_raw, "custom_slices", [])
else: else:
entry = entry_raw entry = entry_raw
tier = None tier = None
auto_aggregate = True auto_aggregate = True
force_full = False force_full = False
view_mode = "full" view_mode = "full"
ast_signatures = False ast_signatures = False
ast_definitions = False ast_definitions = False
ast_mask = {} ast_mask = {}
custom_slices = [] custom_slices = []
if not entry or not isinstance(entry, str):
continue if not entry or not isinstance(entry, str): continue
paths = resolve_paths(base_dir, entry) paths = resolve_paths(base_dir, entry)
if not paths: if not paths:
items.append({"path": None, "entry": entry, "content": f"ERROR: no files matched: {entry}", "error": True, "mtime": 0.0, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "view_mode": view_mode, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices}) items.append({"path": None, "entry": entry, "content": f"ERROR: no files matched: {entry}", "error": True, "mtime": 0.0, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "view_mode": view_mode, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices})
continue continue
for path in paths: for path in paths:
try: try:
content = path.read_text(encoding="utf-8") content = path.read_text(encoding="utf-8")
mtime = path.stat().st_mtime mtime = path.stat().st_mtime
error = False error = False
if not error and view_mode != "full": if not error and view_mode != "full":
try: try:
if view_mode == "summary": if view_mode == "summary": content = summarize.summarise_file(path, content)
content = summarize.summarise_file(path, content)
elif view_mode == "skeleton": elif view_mode == "skeleton":
suffix_lower = path.suffix.lower() suffix_lower = path.suffix.lower()
if suffix_lower == ".py": if suffix_lower == ".py":
if not parser: parser = ASTParser("python") if not parser: parser = ASTParser("python")
content = parser.get_skeleton(content, path=str(path)) content = parser.get_skeleton(content, path=str(path))
elif suffix_lower in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']: elif suffix_lower in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']:
from src import mcp_client if suffix_lower in ['.c', '.h']: content = mcp_client.ts_c_get_skeleton(str(path))
if suffix_lower in ['.c', '.h']: content = mcp_client.ts_c_get_skeleton(str(path)) else: content = mcp_client.ts_cpp_get_skeleton(str(path))
else: content = mcp_client.ts_cpp_get_skeleton(str(path))
else: else:
content = summarize.summarise_file(path, content) content = summarize.summarise_file(path, content)
elif view_mode == "outline": elif view_mode == "outline":
@@ -223,7 +224,6 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
if not parser: parser = ASTParser("python") if not parser: parser = ASTParser("python")
content = parser.get_code_outline(content, path=str(path)) content = parser.get_code_outline(content, path=str(path))
elif suffix_lower in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']: elif suffix_lower in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']:
from src import mcp_client
if suffix_lower in ['.c', '.h']: content = mcp_client.ts_c_get_code_outline(str(path)) if suffix_lower in ['.c', '.h']: content = mcp_client.ts_c_get_code_outline(str(path))
else: content = mcp_client.ts_cpp_get_code_outline(str(path)) else: content = mcp_client.ts_cpp_get_code_outline(str(path))
else: else:
@@ -232,58 +232,50 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
suffix_lower = path.suffix.lower() suffix_lower = path.suffix.lower()
if ast_mask: if ast_mask:
mask_sections = [] mask_sections = []
from src import mcp_client
for symbol_raw, mode in ast_mask.items(): for symbol_raw, mode in ast_mask.items():
if mode == "hide": continue if mode == "hide": continue
import re
symbol = re.sub(r'\(\d+-\d+\)$', '', symbol_raw) symbol = re.sub(r'\(\d+-\d+\)$', '', symbol_raw)
res = "" res = ""
if suffix_lower == ".py": if suffix_lower == ".py":
res = mcp_client.py_get_definition(str(path), symbol) if mode == "def" else mcp_client.py_get_signature(str(path), symbol) res = mcp_client.py_get_definition(str(path), symbol) if mode == "def" else mcp_client.py_get_signature(str(path), symbol)
elif suffix_lower in [".c", ".h", ".cpp", ".hpp", ".cxx", ".cc"]: elif suffix_lower in [".c", ".h", ".cpp", ".hpp", ".cxx", ".cc"]:
is_cpp = any(ext in suffix_lower for ext in [".cpp", ".hpp", ".cxx", ".cc"]) is_cpp = any(ext in suffix_lower for ext in [".cpp", ".hpp", ".cxx", ".cc"])
if mode == "def": if mode == "def": res = mcp_client.ts_cpp_get_definition(str(path), symbol) if is_cpp else mcp_client.ts_c_get_definition(str(path), symbol)
res = mcp_client.ts_cpp_get_definition(str(path), symbol) if is_cpp else mcp_client.ts_c_get_definition(str(path), symbol) else: res = mcp_client.ts_cpp_get_signature(str(path), symbol) if is_cpp else mcp_client.ts_c_get_signature(str(path), symbol)
else:
res = mcp_client.ts_cpp_get_signature(str(path), symbol) if is_cpp else mcp_client.ts_c_get_signature(str(path), symbol)
if res: mask_sections.append(res) if res: mask_sections.append(res)
if mask_sections: if mask_sections: content = "\n\n".join(mask_sections)
content = "\n\n".join(mask_sections) else: content = "(no masked sections visible)"
else:
content = "(no masked sections visible)"
else: else:
content = "(no ast mask defined)" content = "(no ast mask defined)"
elif view_mode == "none": elif view_mode == "none": content = "(context excluded)"
content = "(context excluded)"
elif view_mode == "custom": elif view_mode == "custom":
if custom_slices: if custom_slices:
lines = content.splitlines() lines = content.splitlines()
slices_text = [] slices_text = []
for s in custom_slices: for s in custom_slices:
start = s.get("start_line", 1) start = s.get("start_line", 1)
end = s.get("end_line", len(lines)) end = s.get("end_line", len(lines))
tag = s.get("tag", "unnamed") tag = s.get("tag", "unnamed")
comment = s.get("comment", "") comment = s.get("comment", "")
s_idx = max(0, start - 1) s_idx = max(0, start - 1)
e_idx = min(len(lines), end) e_idx = min(len(lines), end)
chunk = "\n".join(lines[s_idx:e_idx]) chunk = "\n".join(lines[s_idx:e_idx])
slices_text.append(f"---\n[Slice: {tag}] ({comment})\nLines {start}-{end}:\n{chunk}") slices_text.append(f"---\n[Slice: {tag}] ({comment})\nLines {start}-{end}:\n{chunk}")
content = "\n\n".join(slices_text) content = "\n\n".join(slices_text)
else: else:
content = summarize.summarise_file(path, content) content = summarize.summarise_file(path, content)
except Exception as e: except Exception as e:
import traceback
content = f"ERROR in {view_mode} view mode for {path}:\n{traceback.format_exc()}" content = f"ERROR in {view_mode} view mode for {path}:\n{traceback.format_exc()}"
error = True error = True
except FileNotFoundError: except FileNotFoundError:
content = f"ERROR: file not found: {path}" content = f"ERROR: file not found: {path}"
mtime = 0.0 mtime = 0.0
error = True error = True
except Exception as e: except Exception as e:
import traceback
content = f"ERROR reading {path}:\n{traceback.format_exc()}" content = f"ERROR reading {path}:\n{traceback.format_exc()}"
mtime = 0.0 mtime = 0.0
error = True error = True
items.append({"path": path, "entry": entry, "content": content, "error": error, "mtime": mtime, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "view_mode": view_mode, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices}) items.append({"path": path, "entry": entry, "content": content, "error": error, "mtime": mtime, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "view_mode": view_mode, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices})
return items return items
@@ -294,11 +286,10 @@ def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str:
""" """
sections = [] sections = []
for item in file_items: for item in file_items:
if not item.get("auto_aggregate", True): if not item.get("auto_aggregate", True): continue
continue path = item.get("path")
path = item.get("path") entry = item.get("entry", "unknown")
entry = item.get("entry", "unknown") content = item.get("content", "")
content = item.get("content", "")
view_mode = item.get("view_mode", "full") view_mode = item.get("view_mode", "full")
if path is None: if path is None:
if view_mode == "summary": if view_mode == "summary":
@@ -320,23 +311,20 @@ def build_beads_section(base_dir: Path) -> str:
[C: tests/test_aggregate_beads.py:test_build_beads_compaction] [C: tests/test_aggregate_beads.py:test_build_beads_compaction]
""" """
client = beads_client.BeadsClient(base_dir) client = beads_client.BeadsClient(base_dir)
if not client.is_initialized(): if not client.is_initialized(): return ""
return ""
beads = client.list_beads() beads = client.list_beads()
if not beads: if not beads: return ""
return "" active = [b for b in beads if b.status == "active"]
active = [b for b in beads if b.status == "active"]
completed = [b for b in beads if b.status == "completed"] completed = [b for b in beads if b.status == "completed"]
parts = [] parts = []
parts.append("## Beads Mode: Progress Track") parts.append("## Beads Mode: Progress Track")
if completed: if completed:
parts.append("### Completed Beads") parts.append("### Completed Beads")
comp_list = ", ".join([f"`{b.title}`" for b in completed]) comp_list = ", ".join([f"`{b.title}`" for b in completed])
parts.append(comp_list) parts.append(comp_list)
if active: if active:
parts.append("### Active Beads") parts.append("### Active Beads")
for b in active: for b in active: parts.append(f"- **{b.title}** ({b.id}): {b.description}")
parts.append(f"- **{b.title}** ({b.id}): {b.description}")
return "\n\n".join(parts) return "\n\n".join(parts)
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, aggregation_strategy: str = "auto", execution_mode: str = "standard", base_dir: Path | None = None) -> str: 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, aggregation_strategy: str = "auto", execution_mode: str = "standard", base_dir: Path | None = None) -> str:
@@ -344,24 +332,17 @@ def build_markdown_from_items(file_items: list[dict[str, Any]], screenshot_base_
parts = [] parts = []
# STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits # STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits
if file_items: if file_items:
if aggregation_strategy == "summarize": if aggregation_strategy == "summarize": parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items))
parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items)) elif aggregation_strategy == "full": parts.append("## Files\n\n" + _build_files_section_from_items(file_items))
elif aggregation_strategy == "full":
parts.append("## Files\n\n" + _build_files_section_from_items(file_items))
else: # auto else: # auto
if summary_only: if summary_only: parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items))
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))
else: if screenshots: parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots))
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))
if execution_mode == "beads" and base_dir: if execution_mode == "beads" and base_dir:
beads_md = build_beads_section(base_dir) beads_md = build_beads_section(base_dir)
if beads_md: if beads_md: parts.append(beads_md)
parts.append(beads_md)
# DYNAMIC SUFFIX: History changes every turn, must go last # DYNAMIC SUFFIX: History changes every turn, must go last
if history: if history: parts.append("## Discussion History\n\n" + build_discussion_section(history))
parts.append("## Discussion History\n\n" + build_discussion_section(history))
return "\n\n---\n\n".join(parts) return "\n\n---\n\n".join(parts)
def build_markdown_no_history(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False, aggregation_strategy: str = "auto") -> str: def build_markdown_no_history(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False, aggregation_strategy: str = "auto") -> str:
@@ -388,67 +369,61 @@ def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
""" """
with get_monitor().scope("build_tier3_context"): with get_monitor().scope("build_tier3_context"):
focus_set = set(focus_files) focus_set = set(focus_files)
parser = ASTParser("python") parser = ASTParser("python")
sections = [] sections = []
for item in file_items: for item in file_items:
if not item.get("auto_aggregate", True): if not item.get("auto_aggregate", True): continue
continue path = item.get("path")
path = item.get("path") entry = item.get("entry", "")
entry = item.get("entry", "") path_str = str(path) if path else ""
path_str = str(path) if path else "" name = path.name if path else ""
name = path.name if path else "" tier = item.get("tier")
tier = item.get("tier") force_full = item.get("force_full")
force_full = item.get("force_full") ast_signatures = item.get("ast_signatures", False)
ast_signatures = item.get("ast_signatures", False)
ast_definitions = item.get("ast_definitions", False) ast_definitions = item.get("ast_definitions", False)
ast_mask = item.get("ast_mask", {}) ast_mask = item.get("ast_mask", {})
content = item.get("content", "") content = item.get("content", "")
is_focus = entry in focus_set or (name and name in focus_set) or (path_str and path_str in focus_set)
is_focus = entry in focus_set or (name and name in focus_set) or (path_str and path_str in focus_set)
if not is_focus and path_str: if not is_focus and path_str:
for focus in focus_set: for focus in focus_set:
if focus in path_str: if focus in path_str:
is_focus = True is_focus = True
break break
original = entry if entry and "*" not in entry else (str(path) if path else (entry or "unknown"))
slices = item.get('custom_slices', []) original = entry if entry and "*" not in entry else (str(path) if path else (entry or "unknown"))
slices = item.get('custom_slices', [])
if slices and not item.get('error'): if slices and not item.get('error'):
from src.fuzzy_anchor import FuzzyAnchor
resolved_blocks = [] resolved_blocks = []
content = item.get('content', '') content = item.get('content', '')
suffix = path.suffix.lstrip(".") if path and path.suffix else "text" suffix = path.suffix.lstrip(".") if path and path.suffix else "text"
for slc in slices: for slc in slices:
range_res = FuzzyAnchor.resolve_slice(content, slc) range_res = FuzzyAnchor.resolve_slice(content, slc)
if range_res: if range_res:
s, e = range_res s, e = range_res
lines = content.splitlines() lines = content.splitlines()
resolved_blocks.append("\n".join(lines[s-1:e])) resolved_blocks.append("\n".join(lines[s-1:e]))
if resolved_blocks: if resolved_blocks:
combined = "\n\n... [LINES SKIPPED] ...\n\n".join(resolved_blocks) combined = "\n\n... [LINES SKIPPED] ...\n\n".join(resolved_blocks)
sections.append(f"### `{original}` (Slices)\n\n```{suffix}\n{combined}\n```") sections.append(f"### `{original}` (Slices)\n\n```{suffix}\n{combined}\n```")
continue # Skip full file logic continue # Skip full file logic
if is_focus or tier == 3 or force_full: if is_focus or tier == 3 or force_full:
suffix = path.suffix.lstrip(".") if path and path.suffix else "text" suffix = path.suffix.lstrip(".") if path and path.suffix else "text"
sections.append(f"### `{original}`\n\n```{suffix}\n{content}\n```") sections.append(f"### `{original}`\n\n```{suffix}\n{content}\n```")
elif path: elif path:
if ast_mask and not item.get("error"): if ast_mask and not item.get("error"):
mask_sections = [] mask_sections = []
from src import mcp_client
for symbol_raw, mode in ast_mask.items(): for symbol_raw, mode in ast_mask.items():
if mode == "hide": if mode == "hide": continue
continue
import re
symbol = re.sub(r'\(\d+-\d+\)$', '', symbol_raw) symbol = re.sub(r'\(\d+-\d+\)$', '', symbol_raw)
res = "" res = ""
if path.suffix == ".py": if path.suffix == ".py":
res = mcp_client.py_get_definition(str(path), symbol) if mode == "def" else mcp_client.py_get_signature(str(path), symbol) res = mcp_client.py_get_definition(str(path), symbol) if mode == "def" else mcp_client.py_get_signature(str(path), symbol)
elif path.suffix in [".c", ".h", ".cpp", ".hpp", ".cxx", ".cc"]: elif path.suffix in [".c", ".h", ".cpp", ".hpp", ".cxx", ".cc"]:
is_cpp = any(ext in path.suffix for ext in [".cpp", ".hpp", ".cxx", ".cc"]) is_cpp = any(ext in path.suffix for ext in [".cpp", ".hpp", ".cxx", ".cc"])
if mode == "def": if mode == "def": res = mcp_client.ts_cpp_get_definition(str(path), symbol) if is_cpp else mcp_client.ts_c_get_definition(str(path), symbol)
res = mcp_client.ts_cpp_get_definition(str(path), symbol) if is_cpp else mcp_client.ts_c_get_definition(str(path), symbol) else: res = mcp_client.ts_cpp_get_signature(str(path), symbol) if is_cpp else mcp_client.ts_c_get_signature(str(path), symbol)
else:
res = mcp_client.ts_cpp_get_signature(str(path), symbol) if is_cpp else mcp_client.ts_c_get_signature(str(path), symbol)
if res: if res:
mask_sections.append(res) mask_sections.append(res)
if mask_sections: if mask_sections:
@@ -456,7 +431,6 @@ def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
sections.append(f"### `{original}` (Masked)\n\n```{suffix}\n" + "\n\n".join(mask_sections) + "\n```") sections.append(f"### `{original}` (Masked)\n\n```{suffix}\n" + "\n\n".join(mask_sections) + "\n```")
continue continue
if path.suffix in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc'] and not item.get("error"): if path.suffix in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc'] and not item.get("error"):
from src import mcp_client
if ast_definitions: if ast_definitions:
skeleton = mcp_client.ts_cpp_get_skeleton(str(path)) if 'cpp' in path.suffix or 'hpp' in path.suffix or 'cxx' in path.suffix or 'cc' in path.suffix else mcp_client.ts_c_get_skeleton(str(path)) skeleton = mcp_client.ts_cpp_get_skeleton(str(path)) if 'cpp' in path.suffix or 'hpp' in path.suffix or 'cxx' in path.suffix or 'cc' in path.suffix else mcp_client.ts_c_get_skeleton(str(path))
sections.append(f"### `{original}` (AST Definitions)\n\n```{path.suffix.lstrip('.')}\n{skeleton}\n```") sections.append(f"### `{original}` (AST Definitions)\n\n```{path.suffix.lstrip('.')}\n{skeleton}\n```")
@@ -474,12 +448,9 @@ def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
else: else:
sections.append(f"### `{original}`\n\n{summarize.summarise_file(path, content)}") sections.append(f"### `{original}`\n\n{summarize.summarise_file(path, content)}")
parts = [] parts = []
if sections: if sections: parts.append("## Files (Tier 3 - Focused)\n\n" + "\n\n---\n\n".join(sections))
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 screenshots: if history: parts.append("## Discussion History\n\n" + build_discussion_section(history))
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) 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, execution_mode: str = "standard") -> str: 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, execution_mode: str = "standard") -> str:
@@ -491,23 +462,31 @@ def run(config: dict[str, Any], aggregation_strategy: str = "auto") -> tuple[str
[C: simulation/sim_base.py:run_sim, src/ai_client.py:_send_anthropic, src/ai_client.py:_send_deepseek, src/ai_client.py:_send_gemini, src/ai_client.py:_send_gemini_cli, src/ai_client.py:_send_minimax, src/app_controller.py:AppController._cb_start_track, src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._process_event_queue, src/app_controller.py:AppController._start_track_logic, src/external_editor.py:_find_vscode_in_registry, src/gui_2.py:App._render_snapshot_tab, src/gui_2.py:App.run, src/gui_2.py:main, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run] [C: simulation/sim_base.py:run_sim, src/ai_client.py:_send_anthropic, src/ai_client.py:_send_deepseek, src/ai_client.py:_send_gemini, src/ai_client.py:_send_gemini_cli, src/ai_client.py:_send_minimax, src/app_controller.py:AppController._cb_start_track, src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._process_event_queue, src/app_controller.py:AppController._start_track_logic, src/external_editor.py:_find_vscode_in_registry, src/gui_2.py:App._render_snapshot_tab, src/gui_2.py:App.run, src/gui_2.py:main, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
""" """
namespace = config.get("project", {}).get("name") namespace = config.get("project", {}).get("name")
if not namespace: if not namespace: namespace = config.get("output", {}).get("namespace", "project")
namespace = config.get("output", {}).get("namespace", "project") output_dir = Path(config["output"]["output_dir"])
output_dir = Path(config["output"]["output_dir"]) base_dir = Path(config["files"]["base_dir"])
base_dir = Path(config["files"]["base_dir"]) files = config["files"].get("paths", [])
files = config["files"].get("paths", [])
screenshot_base_dir = Path(config.get("screenshots", {}).get("base_dir", ".")) screenshot_base_dir = Path(config.get("screenshots", {}).get("base_dir", "."))
screenshots = config.get("screenshots", {}).get("paths", []) screenshots = config.get("screenshots", {}).get("paths", [])
history = config.get("discussion", {}).get("history", []) history = config.get("discussion", {}).get("history", [])
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
increment = find_next_increment(output_dir, namespace) increment = find_next_increment(output_dir, namespace)
output_file = output_dir / f"{namespace}_{increment:03d}.md" output_file = output_dir / f"{namespace}_{increment:03d}.md"
# Build file items once, then construct markdown from them (avoids double I/O) # Build file items once, then construct markdown from them (avoids double I/O)
file_items = build_file_items(base_dir, files) file_items = build_file_items(base_dir, files)
summary_only = config.get("project", {}).get("summary_only", False) summary_only = config.get("project", {}).get("summary_only", False)
execution_mode = config.get("project", {}).get("execution_mode", "standard") execution_mode = config.get("project", {}).get("execution_mode", "standard")
markdown = build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history, markdown = build_markdown_from_items(
summary_only=summary_only, aggregation_strategy=aggregation_strategy, execution_mode=execution_mode, base_dir=base_dir) file_items,
screenshot_base_dir,
screenshots,
history,
summary_only = summary_only,
aggregation_strategy = aggregation_strategy,
execution_mode = execution_mode,
base_dir = base_dir)
output_file.write_text(markdown, encoding="utf-8") output_file.write_text(markdown, encoding="utf-8")
return markdown, output_file, file_items return markdown, output_file, file_items
@@ -516,7 +495,6 @@ def main() -> None:
""" """
[C: simulation/live_walkthrough.py:module, simulation/ping_pong.py:module, src/ai_server.py:module, src/api_hooks.py:WebSocketServer._run_loop, src/gui_2.py:module, tests/mock_concurrent_mma.py:module, tests/mock_gemini_cli.py:module, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_allow_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_deny_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_unreachable_hook_server, tests/test_cli_tool_bridge.py:module, tests/test_cli_tool_bridge_mapping.py:TestCliToolBridgeMapping.test_mapping_from_api_format, tests/test_cli_tool_bridge_mapping.py:module, tests/test_discussion_takes.py:module, tests/test_external_editor_gui.py:module, tests/test_headless_service.py:TestHeadlessStartup.test_headless_flag_triggers_run, tests/test_headless_service.py:TestHeadlessStartup.test_normal_startup_calls_app_run, tests/test_mma_skeleton.py:module, tests/test_orchestrator_pm.py:module, tests/test_orchestrator_pm_history.py:module, tests/test_presets.py:module, tests/test_project_serialization.py:module, tests/test_run_worker_lifecycle_abort.py:module, tests/test_symbol_lookup.py:module, tests/test_system_prompt_exposure.py:module, tests/test_theme_nerv_fx.py:module] [C: simulation/live_walkthrough.py:module, simulation/ping_pong.py:module, src/ai_server.py:module, src/api_hooks.py:WebSocketServer._run_loop, src/gui_2.py:module, tests/mock_concurrent_mma.py:module, tests/mock_gemini_cli.py:module, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_allow_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_deny_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_unreachable_hook_server, tests/test_cli_tool_bridge.py:module, tests/test_cli_tool_bridge_mapping.py:TestCliToolBridgeMapping.test_mapping_from_api_format, tests/test_cli_tool_bridge_mapping.py:module, tests/test_discussion_takes.py:module, tests/test_external_editor_gui.py:module, tests/test_headless_service.py:TestHeadlessStartup.test_headless_flag_triggers_run, tests/test_headless_service.py:TestHeadlessStartup.test_normal_startup_calls_app_run, tests/test_mma_skeleton.py:module, tests/test_orchestrator_pm.py:module, tests/test_orchestrator_pm_history.py:module, tests/test_presets.py:module, tests/test_project_serialization.py:module, tests/test_run_worker_lifecycle_abort.py:module, tests/test_symbol_lookup.py:module, tests/test_system_prompt_exposure.py:module, tests/test_theme_nerv_fx.py:module]
""" """
from src.paths import get_config_path
config_path = get_config_path() config_path = get_config_path()
if not config_path.exists(): if not config_path.exists():
@@ -528,7 +506,7 @@ def main() -> None:
if not active_path: if not active_path:
print(f"No active project found in {config_path}.") print(f"No active project found in {config_path}.")
return return
# Use project_manager to load project (handles history segregation) # Use project_manager to load project (handles history segregation)
proj = project_manager.load_project(active_path) proj = project_manager.load_project(active_path)
# Use flat_config to make it compatible with aggregate.run() # Use flat_config to make it compatible with aggregate.run()
config = project_manager.flat_config(proj) config = project_manager.flat_config(proj)
+8 -16
View File
@@ -45,10 +45,8 @@ def _now_ts() -> str:
def open_session(label: Optional[str] = None) -> None: def open_session(label: Optional[str] = None) -> None:
""" """
Called once at GUI startup. Creates the log directories if needed and
opens the log files for this session within a sub-directory.
Called once at GUI startup. Creates the log directories if needed and
opens the log files for this session within a sub-directory.
[C: tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_logging_e2e.py:test_logging_e2e, tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs, tests/test_session_logger_optimization.py:test_session_directory_and_subdirectories_creation, tests/test_session_logger_reset.py:test_reset_session, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry] [C: tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_logging_e2e.py:test_logging_e2e, tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs, tests/test_session_logger_optimization.py:test_session_directory_and_subdirectories_creation, tests/test_session_logger_reset.py:test_reset_session, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry]
""" """
global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq, _output_seq global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq, _output_seq
@@ -139,10 +137,8 @@ def log_api_hook(method: str, path: str, payload: str) -> None:
def log_comms(entry: dict[str, Any]) -> None: def log_comms(entry: dict[str, Any]) -> None:
""" """
Append one comms entry to the comms log file as a JSON-L line.
Thread-safe (GIL + line-buffered file).
Append one comms entry to the comms log file as a JSON-L line.
Thread-safe (GIL + line-buffered file).
[C: tests/test_logging_e2e.py:test_logging_e2e] [C: tests/test_logging_e2e.py:test_logging_e2e]
""" """
if _comms_fh is None: if _comms_fh is None:
@@ -154,10 +150,8 @@ def log_comms(entry: dict[str, Any]) -> None:
def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optional[str]: def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optional[str]:
""" """
Append a tool-call record to the toolcalls log and write the PS1 script to
the session's scripts directory. Returns the path of the written script file.
Append a tool-call record to the toolcalls log and write the PS1 script to
the session's scripts directory. Returns the path of the written script file.
[C: tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts] [C: tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts]
""" """
global _seq global _seq
@@ -199,10 +193,8 @@ def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optio
def log_tool_output(content: str) -> Optional[str]: def log_tool_output(content: str) -> Optional[str]:
""" """
Save tool output content to a unique file in the session's outputs directory.
Returns the path of the written file.
Save tool output content to a unique file in the session's outputs directory.
Returns the path of the written file.
[C: tests/test_session_logger_optimization.py:test_log_tool_output_returns_none_if_no_session, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs] [C: tests/test_session_logger_optimization.py:test_log_tool_output_returns_none_if_no_session, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs]
""" """
global _output_seq global _output_seq
+2 -4
View File
@@ -3,10 +3,8 @@ from imgui_bundle import imgui
def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: imgui.ImVec2, color: imgui.ImVec4, shadow_size: float = 10.0, rounding: float = 0.0) -> None: def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: imgui.ImVec2, color: imgui.ImVec4, shadow_size: float = 10.0, rounding: float = 0.0) -> None:
""" """
Simulates a soft shadow effect by drawing multiple concentric rounded rectangles
with decreasing alpha values. This is a faux-shader effect using primitive batching.
Simulates a soft shadow effect by drawing multiple concentric rounded rectangles
with decreasing alpha values. This is a faux-shader effect using primitive batching.
""" """
r, g, b, a = color.x, color.y, color.z, color.w r, g, b, a = color.x, color.y, color.z, color.w
steps = int(shadow_size) steps = int(shadow_size)
+6 -8
View File
@@ -55,14 +55,12 @@ def _build_subprocess_env() -> dict[str, str]:
def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> str: def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> str:
""" """
Run a PowerShell script with working directory set to base_dir.
Returns a string combining stdout, stderr, and exit code.
Run a PowerShell script with working directory set to base_dir. Environment is configured via mcp_env.toml (project root).
Returns a string combining stdout, stderr, and exit code. If qa_callback is provided and the command fails or has stderr,
Environment is configured via mcp_env.toml (project root). the callback is called with the stderr content and its result is appended.
If qa_callback is provided and the command fails or has stderr, If patch_callback is provided, it receives (error, file_context) and returns patch text.
the callback is called with the stderr content and its result is appended.
If patch_callback is provided, it receives (error, file_context) and returns patch text.
[C: tests/test_tier4_interceptor.py:test_run_powershell_no_qa_callback_on_success, tests/test_tier4_interceptor.py:test_run_powershell_optional_qa_callback, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_failure, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_stderr_only] [C: tests/test_tier4_interceptor.py:test_run_powershell_no_qa_callback_on_success, tests/test_tier4_interceptor.py:test_run_powershell_optional_qa_callback, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_failure, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_stderr_only]
""" """
safe_dir: str = str(base_dir).replace("'", "''") safe_dir: str = str(base_dir).replace("'", "''")
+4 -8
View File
@@ -205,10 +205,8 @@ def summarise_file(path: Path, content: str) -> str:
def summarise_items(file_items: list[dict[str, Any]]) -> list[dict[str, Any]]: 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.
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.
""" """
result = [] result = []
for item in file_items: for item in file_items:
@@ -225,10 +223,8 @@ def summarise_items(file_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
def build_summary_markdown(file_items: list[dict[str, Any]]) -> 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.
Build a compact markdown string of file summaries, suitable for the
initial <context> block instead of full file contents.
""" """
summarised = summarise_items(file_items) summarised = summarise_items(file_items)
parts = [] parts = []