From 138e31374b8f2c6cd65c1a226115783f6b5822be Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 27 Feb 2026 20:41:30 -0500 Subject: [PATCH] checkpoint --- .gemini/agents/tier1-orchestrator.md | 7 +-- .gemini/agents/tier2-tech-lead.md | 7 +-- .gemini/agents/tier3-worker.md | 7 +-- .gemini/agents/tier4-qa.md | 7 +-- .gemini/settings.json | 3 +- mcp_client.py | 42 ++++++++++++- outline_tool.py | 94 ++++++++++++++++++++++++++++ scripts/mma_exec.py | 6 +- scripts/tool_call.py | 47 ++++++++++++++ 9 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 outline_tool.py create mode 100644 scripts/tool_call.py diff --git a/.gemini/agents/tier1-orchestrator.md b/.gemini/agents/tier1-orchestrator.md index 139a933..410a79d 100644 --- a/.gemini/agents/tier1-orchestrator.md +++ b/.gemini/agents/tier1-orchestrator.md @@ -5,11 +5,10 @@ model: gemini-3.1-pro-preview tools: - read_file - list_directory - - glob + - discovered_tool_search_files - grep_search - - google_web_search - - web_fetch - - codebase_investigator + - discovered_tool_web_search + - discovered_tool_fetch_url - activate_skill - discovered_tool_run_powershell --- diff --git a/.gemini/agents/tier2-tech-lead.md b/.gemini/agents/tier2-tech-lead.md index 83eaa12..8b9f70d 100644 --- a/.gemini/agents/tier2-tech-lead.md +++ b/.gemini/agents/tier2-tech-lead.md @@ -7,11 +7,10 @@ tools: - write_file - replace - list_directory - - glob + - discovered_tool_search_files - grep_search - - google_web_search - - web_fetch - - codebase_investigator + - discovered_tool_web_search + - discovered_tool_fetch_url - activate_skill - discovered_tool_run_powershell --- diff --git a/.gemini/agents/tier3-worker.md b/.gemini/agents/tier3-worker.md index 3954633..5ba0689 100644 --- a/.gemini/agents/tier3-worker.md +++ b/.gemini/agents/tier3-worker.md @@ -7,11 +7,10 @@ tools: - write_file - replace - list_directory - - glob + - discovered_tool_search_files - grep_search - - google_web_search - - web_fetch - - codebase_investigator + - discovered_tool_web_search + - discovered_tool_fetch_url - activate_skill - discovered_tool_run_powershell --- diff --git a/.gemini/agents/tier4-qa.md b/.gemini/agents/tier4-qa.md index 2cba935..f78ebb8 100644 --- a/.gemini/agents/tier4-qa.md +++ b/.gemini/agents/tier4-qa.md @@ -5,11 +5,10 @@ model: gemini-2.5-flash-lite tools: - read_file - list_directory - - glob + - discovered_tool_search_files - grep_search - - google_web_search - - web_fetch - - codebase_investigator + - discovered_tool_web_search + - discovered_tool_fetch_url - activate_skill - discovered_tool_run_powershell --- diff --git a/.gemini/settings.json b/.gemini/settings.json index a532e1b..13dbaf2 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -3,7 +3,8 @@ "enableAgents": true }, "tools": { - "discoveryCommand": "python C:/projects/manual_slop/scripts/tool_discovery.py", + "discoveryCommand": "uv run python C:/projects/manual_slop/scripts/tool_discovery.py", + "callCommand": "uv run python C:/projects/manual_slop/scripts/tool_call.py", "whitelist": [ "*" ] diff --git a/mcp_client.py b/mcp_client.py index a8eed2c..79b126a 100644 --- a/mcp_client.py +++ b/mcp_client.py @@ -31,6 +31,7 @@ so the AI doesn't wander outside the project workspace. from pathlib import Path import summarize +import outline_tool import urllib.request import urllib.parse from html.parser import HTMLParser @@ -261,6 +262,25 @@ def get_python_skeleton(path: str) -> str: return f"ERROR generating skeleton for '{path}': {e}" +def get_code_outline(path: str) -> str: + """ + Returns a hierarchical outline of a code file (classes, functions, methods with line ranges). + """ + p, err = _resolve_and_check(path) + if err: + return err + if not p.exists(): + return f"ERROR: file not found: {path}" + if not p.is_file(): + return f"ERROR: not a file: {path}" + + try: + code = p.read_text(encoding="utf-8") + return outline_tool.get_outline(p, code) + except Exception as e: + return f"ERROR generating outline for '{path}': {e}" + + # ------------------------------------------------------------------ web tools @@ -387,7 +407,7 @@ def get_ui_performance() -> str: # ------------------------------------------------------------------ tool dispatch -TOOL_NAMES = {"read_file", "list_directory", "search_files", "get_file_summary", "get_python_skeleton", "web_search", "fetch_url", "get_ui_performance"} +TOOL_NAMES = {"read_file", "list_directory", "search_files", "get_file_summary", "get_python_skeleton", "get_code_outline", "web_search", "fetch_url", "get_ui_performance"} def dispatch(tool_name: str, tool_input: dict) -> str: @@ -404,6 +424,8 @@ def dispatch(tool_name: str, tool_input: dict) -> str: return get_file_summary(tool_input.get("path", "")) if tool_name == "get_python_skeleton": return get_python_skeleton(tool_input.get("path", "")) + if tool_name == "get_code_outline": + return get_code_outline(tool_input.get("path", "")) if tool_name == "web_search": return web_search(tool_input.get("query", "")) if tool_name == "fetch_url": @@ -511,6 +533,24 @@ MCP_TOOL_SPECS = [ "required": ["path"], }, }, + { + "name": "get_code_outline", + "description": ( + "Get a hierarchical outline of a code file. " + "This returns classes, functions, and methods with their line ranges and brief docstrings. " + "Use this to quickly map out a file's structure before reading specific sections." + ), + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the code file (currently supports .py).", + } + }, + "required": ["path"], + }, + }, { "name": "web_search", "description": "Search the web using DuckDuckGo. Returns the top 5 search results with titles, URLs, and snippets. Chain this with fetch_url to read specific pages.", diff --git a/outline_tool.py b/outline_tool.py new file mode 100644 index 0000000..0198b06 --- /dev/null +++ b/outline_tool.py @@ -0,0 +1,94 @@ +import tree_sitter +import tree_sitter_python +from pathlib import Path + +class CodeOutliner: + def __init__(self): + self.language = tree_sitter.Language(tree_sitter_python.language()) + self.parser = tree_sitter.Parser(self.language) + + def outline(self, code: str) -> str: + tree = self.parser.parse(bytes(code, "utf8")) + lines = code.splitlines() + + output = [] + + def get_docstring(node): + # In Python, docstring is usually the first expression statement in a block + body = node.child_by_field_name("body") + if body and body.type == "block": + for child in body.children: + if child.type == "comment": + continue + if child.type == "expression_statement": + expr = child.children[0] + if expr.type == "string": + doc = code[expr.start_byte:expr.end_byte].strip() + # Strip quotes + if doc.startswith(('"""', "'''")): + doc = doc[3:-3] + elif doc.startswith(('"', "'")): + doc = doc[1:-1] + return doc.splitlines()[0] if doc else "" + break + return None + + def walk(node, indent=0): + node_type = node.type + name = None + + if node_type == "class_definition": + name_node = node.child_by_field_name("name") + if name_node: + name = code[name_node.start_byte:name_node.end_byte] + start_line = node.start_point.row + 1 + end_line = node.end_point.row + 1 + output.append(f"{' ' * indent}[Class] {name} (Lines {start_line}-{end_line})") + doc = get_docstring(node) + if doc: + output.append(f"{' ' * (indent + 1)}\"\"\"{doc}\"\"\"") + + elif node_type in ("function_definition", "async_function_definition"): + name_node = node.child_by_field_name("name") + if name_node: + name = code[name_node.start_byte:name_node.end_byte] + start_line = node.start_point.row + 1 + end_line = node.end_point.row + 1 + prefix = "[Async Func]" if node_type == "async_function_definition" else "[Func]" + + # Check if it's a method (parent is a class body) + parent = node.parent + while parent and parent.type != "class_definition": + if parent.type == "module": + break + parent = parent.parent + + if parent and parent.type == "class_definition": + prefix = "[Method]" + + output.append(f"{' ' * indent}{prefix} {name} (Lines {start_line}-{end_line})") + doc = get_docstring(node) + if doc: + output.append(f"{' ' * (indent + 1)}\"\"\"{doc}\"\"\"") + + for child in node.children: + # Don't recurse into function bodies for outlining functions, + # but we DO want to recurse into classes to find methods. + if node_type == "class_definition": + if child.type == "block": + walk(child, indent + 1) + elif node_type == "module": + walk(child, indent) + elif node_type == "block": + walk(child, indent) + + walk(tree.root_node) + return "\n".join(output) + +def get_outline(path: Path, code: str) -> str: + suffix = path.suffix.lower() + if suffix == ".py": + outliner = CodeOutliner() + return outliner.outline(code) + else: + return f"Outlining not supported for {suffix} files yet." diff --git a/scripts/mma_exec.py b/scripts/mma_exec.py index 23ed3f0..57f0948 100644 --- a/scripts/mma_exec.py +++ b/scripts/mma_exec.py @@ -177,14 +177,14 @@ def execute_agent(role: str, prompt: str, docs: list[str]) -> str: system_directive = "STRICT SYSTEM DIRECTIVE: You are a stateless Tier 3 Worker (Contributor). " \ "Your goal is to implement specific code changes or tests based on the provided task. " \ "You have access to tools for reading and writing files (e.g., read_file, write_file, replace), " \ - "codebase investigation (codebase_investigator), and web tools (google_web_search, web_fetch). " \ + "codebase investigation (codebase_investigator), and web tools (discovered_tool_web_search, discovered_tool_fetch_url). " \ "You CAN execute PowerShell scripts via discovered_tool_run_powershell for verification and testing. " \ "Follow TDD and return success status or code changes. No pleasantries, no conversational filler." elif role in ['tier4', 'tier4-qa']: system_directive = "STRICT SYSTEM DIRECTIVE: You are a stateless Tier 4 QA Agent. " \ "Your goal is to analyze errors, summarize logs, or verify tests. " \ "You have access to tools for reading files, exploring the codebase (codebase_investigator), " \ - "and web tools (google_web_search, web_fetch). " \ + "and web tools (discovered_tool_web_search, discovered_tool_fetch_url). " \ "You CAN execute PowerShell scripts via discovered_tool_run_powershell for diagnostics. " \ "ONLY output the requested analysis. No pleasantries." else: @@ -209,7 +209,7 @@ def execute_agent(role: str, prompt: str, docs: list[str]) -> str: # We use -p 'mma_task' to ensure non-interactive (headless) mode and valid parsing. # Whitelist tools to ensure they are available to the model in headless mode. # Using 'discovered_tool_run_powershell' as it's the confirmed name for shell access. - allowed_tools = "read_file,write_file,replace,list_directory,glob,grep_search,search_files,get_file_summary,discovered_tool_run_powershell,activate_skill,codebase_investigator,google_web_search,web_fetch" + allowed_tools = "read_file,write_file,replace,list_directory,glob,grep_search,discovered_tool_search_files,discovered_tool_get_file_summary,discovered_tool_run_powershell,activate_skill,codebase_investigator,discovered_tool_web_search,discovered_tool_fetch_url" ps_command = ( f"if (Test-Path 'C:\\projects\\misc\\setup_gemini.ps1') {{ . 'C:\\projects\\misc\\setup_gemini.ps1' }}; " f"gemini -p 'mma_task' --allowed-tools {allowed_tools} --output-format json --model {model}" diff --git a/scripts/tool_call.py b/scripts/tool_call.py new file mode 100644 index 0000000..bc5d16c --- /dev/null +++ b/scripts/tool_call.py @@ -0,0 +1,47 @@ +import sys +import json +import os + +# Add project root to sys.path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +try: + import mcp_client +except ImportError: + print(json.dumps({"error": "Failed to import mcp_client"})) + sys.exit(1) + +def main(): + if len(sys.argv) < 2: + print(json.dumps({"error": "No tool name provided"})) + sys.exit(1) + + tool_name = sys.argv[1] + + # Read arguments from stdin + try: + input_data = sys.stdin.read() + if input_data: + tool_input = json.loads(input_data) + else: + tool_input = {} + except json.JSONDecodeError: + print(json.dumps({"error": "Invalid JSON input"})) + sys.exit(1) + + try: + # Note: mcp_client.configure() is usually called by the GUI before each session, + # but for direct CLI calls, we might need a basic configuration. + # However, mcp_client tools generally resolve paths relative to CWD if not configured. + result = mcp_client.dispatch(tool_name, tool_input) + + # We wrap the result in a JSON object for consistency if needed, + # but the CLI often expects just the string result from stdout. + # Actually, let's just print the raw result string as that's what mcp_client returns. + print(result) + except Exception as e: + print(f"ERROR executing tool {tool_name}: {e}") + sys.exit(1) + +if __name__ == "__main__": + main()