diff --git a/auto-claude/agent.py b/auto-claude/agent.py index 3999e37c..03da7512 100644 --- a/auto-claude/agent.py +++ b/auto-claude/agent.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from core.agent instead.""" + from core.agent import * # noqa: F403 diff --git a/auto-claude/agents/tools_pkg/models.py b/auto-claude/agents/tools_pkg/models.py index b6098a17..dc27ec03 100644 --- a/auto-claude/agents/tools_pkg/models.py +++ b/auto-claude/agents/tools_pkg/models.py @@ -24,13 +24,13 @@ TOOL_UPDATE_QA_STATUS = "mcp__auto-claude__update_qa_status" # Electron app must be started with --remote-debugging-port=9222 (or ELECTRON_DEBUG_PORT). # These tools are only available to QA agents (qa_reviewer, qa_fixer), not Coder/Planner. ELECTRON_TOOLS = [ - "mcp__electron__electron_connect", # Connect to Electron app via DevTools - "mcp__electron__electron_screenshot", # Take screenshot of Electron window - "mcp__electron__electron_click", # Click element in Electron app - "mcp__electron__electron_fill", # Fill input field in Electron app - "mcp__electron__electron_evaluate", # Execute JS in Electron renderer - "mcp__electron__electron_get_window_info", # Get window state/bounds - "mcp__electron__electron_get_console", # Get console logs from renderer + "mcp__electron__electron_connect", # Connect to Electron app via DevTools + "mcp__electron__electron_screenshot", # Take screenshot of Electron window + "mcp__electron__electron_click", # Click element in Electron app + "mcp__electron__electron_fill", # Fill input field in Electron app + "mcp__electron__electron_evaluate", # Execute JS in Electron renderer + "mcp__electron__electron_get_window_info", # Get window state/bounds + "mcp__electron__electron_get_console", # Get console logs from renderer ] # Base tools available to all agents diff --git a/auto-claude/agents/tools_pkg/permissions.py b/auto-claude/agents/tools_pkg/permissions.py index 518f62cb..f8d78049 100644 --- a/auto-claude/agents/tools_pkg/permissions.py +++ b/auto-claude/agents/tools_pkg/permissions.py @@ -6,7 +6,6 @@ Manages which tools are allowed for each agent type to prevent context pollution and accidental misuse. """ - from .models import ( BASE_READ_TOOLS, BASE_WRITE_TOOLS, diff --git a/auto-claude/analysis/analyzers/context/env_detector.py b/auto-claude/analysis/analyzers/context/env_detector.py index cad85269..aa0817a5 100644 --- a/auto-claude/analysis/analyzers/context/env_detector.py +++ b/auto-claude/analysis/analyzers/context/env_detector.py @@ -97,7 +97,9 @@ class EnvironmentDetector(BaseAnalyzer): "sensitive": is_sensitive, } - def _parse_env_example(self, env_vars: dict[str, Any], required_vars: set[str]) -> None: + def _parse_env_example( + self, env_vars: dict[str, Any], required_vars: set[str] + ) -> None: """Parse .env.example to find required variables.""" example_content = self._read_file(".env.example") or self._read_file( ".env.sample" @@ -156,7 +158,9 @@ class EnvironmentDetector(BaseAnalyzer): "sensitive": False, } - def _parse_code_references(self, env_vars: dict[str, Any], optional_vars: set[str]) -> None: + def _parse_code_references( + self, env_vars: dict[str, Any], optional_vars: set[str] + ) -> None: """Scan code for os.getenv() / process.env usage to find optional vars.""" entry_files = [ "app.py", diff --git a/auto-claude/analysis/analyzers/context/jobs_detector.py b/auto-claude/analysis/analyzers/context/jobs_detector.py index f5a53a84..0c27b8f0 100644 --- a/auto-claude/analysis/analyzers/context/jobs_detector.py +++ b/auto-claude/analysis/analyzers/context/jobs_detector.py @@ -32,7 +32,9 @@ class JobsDetector(BaseAnalyzer): jobs_info = None # Try each job system in order - jobs_info = self._detect_celery() or self._detect_bullmq() or self._detect_sidekiq() + jobs_info = ( + self._detect_celery() or self._detect_bullmq() or self._detect_sidekiq() + ) if jobs_info: self.analysis["background_jobs"] = jobs_info diff --git a/auto-claude/analysis/analyzers/context/monitoring_detector.py b/auto-claude/analysis/analyzers/context/monitoring_detector.py index eb4402fa..d4928918 100644 --- a/auto-claude/analysis/analyzers/context/monitoring_detector.py +++ b/auto-claude/analysis/analyzers/context/monitoring_detector.py @@ -98,7 +98,10 @@ class MonitoringDetector(BaseAnalyzer): def _get_apm_tools(self) -> list[str] | None: """Get APM tools from existing services analysis.""" - if "services" not in self.analysis or "monitoring" not in self.analysis["services"]: + if ( + "services" not in self.analysis + or "monitoring" not in self.analysis["services"] + ): return None return [s["type"] for s in self.analysis["services"]["monitoring"]] diff --git a/auto-claude/analysis/analyzers/context/services_detector.py b/auto-claude/analysis/analyzers/context/services_detector.py index d7ff22db..80879bc6 100644 --- a/auto-claude/analysis/analyzers/context/services_detector.py +++ b/auto-claude/analysis/analyzers/context/services_detector.py @@ -152,7 +152,9 @@ class ServicesDetector(BaseAnalyzer): return all_deps - def _detect_databases(self, all_deps: set[str], databases: list[dict[str, str]]) -> None: + def _detect_databases( + self, all_deps: set[str], databases: list[dict[str, str]] + ) -> None: """Detect database clients.""" for dep, db_type in self.DATABASE_INDICATORS.items(): if dep in all_deps: @@ -164,7 +166,9 @@ class ServicesDetector(BaseAnalyzer): if indicator in all_deps: cache.append({"type": indicator}) - def _detect_message_queues(self, all_deps: set[str], queues: list[dict[str, str]]) -> None: + def _detect_message_queues( + self, all_deps: set[str], queues: list[dict[str, str]] + ) -> None: """Detect message queue systems.""" for dep, queue_type in self.QUEUE_INDICATORS.items(): if dep in all_deps: @@ -176,25 +180,33 @@ class ServicesDetector(BaseAnalyzer): if dep in all_deps: email.append({"provider": email_type, "client": dep}) - def _detect_payments(self, all_deps: set[str], payments: list[dict[str, str]]) -> None: + def _detect_payments( + self, all_deps: set[str], payments: list[dict[str, str]] + ) -> None: """Detect payment processors.""" for dep, payment_type in self.PAYMENT_INDICATORS.items(): if dep in all_deps: payments.append({"provider": payment_type, "client": dep}) - def _detect_storage(self, all_deps: set[str], storage: list[dict[str, str]]) -> None: + def _detect_storage( + self, all_deps: set[str], storage: list[dict[str, str]] + ) -> None: """Detect storage services.""" for dep, storage_type in self.STORAGE_INDICATORS.items(): if dep in all_deps: storage.append({"provider": storage_type, "client": dep}) - def _detect_auth_providers(self, all_deps: set[str], auth: list[dict[str, str]]) -> None: + def _detect_auth_providers( + self, all_deps: set[str], auth: list[dict[str, str]] + ) -> None: """Detect authentication providers.""" for dep, auth_type in self.AUTH_INDICATORS.items(): if dep in all_deps: auth.append({"type": auth_type, "client": dep}) - def _detect_monitoring(self, all_deps: set[str], monitoring: list[dict[str, str]]) -> None: + def _detect_monitoring( + self, all_deps: set[str], monitoring: list[dict[str, str]] + ) -> None: """Detect monitoring and observability tools.""" for dep, monitoring_type in self.MONITORING_INDICATORS.items(): if dep in all_deps: diff --git a/auto-claude/analysis/analyzers/route_detector.py b/auto-claude/analysis/analyzers/route_detector.py index 2c601cf5..d3cd824b 100644 --- a/auto-claude/analysis/analyzers/route_detector.py +++ b/auto-claude/analysis/analyzers/route_detector.py @@ -19,7 +19,7 @@ class RouteDetector(BaseAnalyzer): """Detects API routes across multiple web frameworks.""" # Directories to exclude from route detection - EXCLUDED_DIRS = {'node_modules', '.venv', 'venv', '__pycache__', '.git'} + EXCLUDED_DIRS = {"node_modules", ".venv", "venv", "__pycache__", ".git"} def __init__(self, path: Path): super().__init__(path) @@ -58,7 +58,9 @@ class RouteDetector(BaseAnalyzer): def _detect_fastapi_routes(self) -> list[dict]: """Detect FastAPI routes.""" routes = [] - files_to_check = [f for f in self.path.glob("**/*.py") if self._should_include_file(f)] + files_to_check = [ + f for f in self.path.glob("**/*.py") if self._should_include_file(f) + ] for file_path in files_to_check: try: @@ -120,7 +122,9 @@ class RouteDetector(BaseAnalyzer): def _detect_flask_routes(self) -> list[dict]: """Detect Flask routes.""" routes = [] - files_to_check = [f for f in self.path.glob("**/*.py") if self._should_include_file(f)] + files_to_check = [ + f for f in self.path.glob("**/*.py") if self._should_include_file(f) + ] for file_path in files_to_check: try: @@ -167,7 +171,9 @@ class RouteDetector(BaseAnalyzer): def _detect_django_routes(self) -> list[dict]: """Detect Django routes from urls.py files.""" routes = [] - url_files = [f for f in self.path.glob("**/urls.py") if self._should_include_file(f)] + url_files = [ + f for f in self.path.glob("**/urls.py") if self._should_include_file(f) + ] for file_path in url_files: try: @@ -201,8 +207,12 @@ class RouteDetector(BaseAnalyzer): def _detect_express_routes(self) -> list[dict]: """Detect Express/Fastify/Koa routes.""" routes = [] - js_files = [f for f in self.path.glob("**/*.js") if self._should_include_file(f)] - ts_files = [f for f in self.path.glob("**/*.ts") if self._should_include_file(f)] + js_files = [ + f for f in self.path.glob("**/*.js") if self._should_include_file(f) + ] + ts_files = [ + f for f in self.path.glob("**/*.ts") if self._should_include_file(f) + ] files_to_check = js_files + ts_files for file_path in files_to_check: try: @@ -256,7 +266,11 @@ class RouteDetector(BaseAnalyzer): app_dir = self.path / "app" if app_dir.exists(): # Find all route.ts/js files - route_files = [f for f in app_dir.glob("**/route.{ts,js,tsx,jsx}") if self._should_include_file(f)] + route_files = [ + f + for f in app_dir.glob("**/route.{ts,js,tsx,jsx}") + if self._should_include_file(f) + ] for route_file in route_files: # Convert file path to route path # app/api/users/[id]/route.ts -> /api/users/:id @@ -290,9 +304,13 @@ class RouteDetector(BaseAnalyzer): # Next.js Pages Router (pages/api directory) pages_api = self.path / "pages" / "api" if pages_api.exists(): - api_files = [f for f in pages_api.glob("**/*.{ts,js,tsx,jsx}") if self._should_include_file(f)] + api_files = [ + f + for f in pages_api.glob("**/*.{ts,js,tsx,jsx}") + if self._should_include_file(f) + ] for api_file in api_files: - if api_file.name.startswith('_'): + if api_file.name.startswith("_"): continue # Convert file path to route @@ -322,7 +340,9 @@ class RouteDetector(BaseAnalyzer): def _detect_go_routes(self) -> list[dict]: """Detect Go framework routes (Gin, Echo, Chi, Fiber).""" routes = [] - go_files = [f for f in self.path.glob("**/*.go") if self._should_include_file(f)] + go_files = [ + f for f in self.path.glob("**/*.go") if self._should_include_file(f) + ] for file_path in go_files: try: @@ -356,7 +376,9 @@ class RouteDetector(BaseAnalyzer): def _detect_rust_routes(self) -> list[dict]: """Detect Rust framework routes (Axum, Actix).""" routes = [] - rust_files = [f for f in self.path.glob("**/*.rs") if self._should_include_file(f)] + rust_files = [ + f for f in self.path.glob("**/*.rs") if self._should_include_file(f) + ] for file_path in rust_files: try: diff --git a/auto-claude/analysis/insight_extractor.py b/auto-claude/analysis/insight_extractor.py index 281fdb87..be2c1e8b 100644 --- a/auto-claude/analysis/insight_extractor.py +++ b/auto-claude/analysis/insight_extractor.py @@ -21,6 +21,7 @@ logger = logging.getLogger(__name__) # Check for Claude SDK availability try: from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient + SDK_AVAILABLE = True except ImportError: SDK_AVAILABLE = False @@ -330,7 +331,9 @@ def _format_attempt_history(attempts: list[dict]) -> str: return "\n".join(lines) -async def run_insight_extraction(inputs: dict, project_dir: Path | None = None) -> dict | None: +async def run_insight_extraction( + inputs: dict, project_dir: Path | None = None +) -> dict | None: """ Run the insight extraction using Claude Agent SDK. diff --git a/auto-claude/analyzer.py b/auto-claude/analyzer.py index ef05d6f5..084b7b8b 100644 --- a/auto-claude/analyzer.py +++ b/auto-claude/analyzer.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from analysis.analyzer instead.""" + from analysis.analyzer import * # noqa: F403 diff --git a/auto-claude/analyzers/__init__.py b/auto-claude/analyzers/__init__.py index 895be274..e871cd82 100644 --- a/auto-claude/analyzers/__init__.py +++ b/auto-claude/analyzers/__init__.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from analysis.analyzers instead.""" + from analysis.analyzers import * # noqa: F403 diff --git a/auto-claude/ci_discovery.py b/auto-claude/ci_discovery.py index d5e83742..db46d7ce 100644 --- a/auto-claude/ci_discovery.py +++ b/auto-claude/ci_discovery.py @@ -1,4 +1,5 @@ """Backward compatibility shim - import from analysis.ci_discovery instead.""" + from analysis.ci_discovery import ( HAS_YAML, CIConfig, diff --git a/auto-claude/cli/build_commands.py b/auto-claude/cli/build_commands.py index 86d5c702..d46c7fed 100644 --- a/auto-claude/cli/build_commands.py +++ b/auto-claude/cli/build_commands.py @@ -318,9 +318,7 @@ def _handle_build_interrupt( from agent import run_autonomous_agent # Print paused banner - print_paused_banner( - spec_dir, spec_dir.name, has_worktree=bool(worktree_manager) - ) + print_paused_banner(spec_dir, spec_dir.name, has_worktree=bool(worktree_manager)) # Update status file status_manager = StatusManager(project_dir) diff --git a/auto-claude/cli/main.py b/auto-claude/cli/main.py index 1dc2618a..8b7bb25f 100644 --- a/auto-claude/cli/main.py +++ b/auto-claude/cli/main.py @@ -302,14 +302,18 @@ def main() -> None: # Handle build management commands if args.merge_preview: from cli.workspace_commands import handle_merge_preview_command + result = handle_merge_preview_command(project_dir, spec_dir.name) # Output as JSON for the UI to parse import json + print(json.dumps(result)) return if args.merge: - success = handle_merge_command(project_dir, spec_dir.name, no_commit=args.no_commit) + success = handle_merge_command( + project_dir, spec_dir.name, no_commit=args.no_commit + ) if not success: sys.exit(1) return diff --git a/auto-claude/cli/workspace_commands.py b/auto-claude/cli/workspace_commands.py index f39537bf..a53bc0e8 100644 --- a/auto-claude/cli/workspace_commands.py +++ b/auto-claude/cli/workspace_commands.py @@ -40,13 +40,28 @@ try: is_debug_enabled, ) except ImportError: - def debug(*args, **kwargs): pass - def debug_detailed(*args, **kwargs): pass - def debug_verbose(*args, **kwargs): pass - def debug_success(*args, **kwargs): pass - def debug_error(*args, **kwargs): pass - def debug_section(*args, **kwargs): pass - def is_debug_enabled(): return False + + def debug(*args, **kwargs): + pass + + def debug_detailed(*args, **kwargs): + pass + + def debug_verbose(*args, **kwargs): + pass + + def debug_success(*args, **kwargs): + pass + + def debug_error(*args, **kwargs): + pass + + def debug_section(*args, **kwargs): + pass + + def is_debug_enabled(): + return False + MODULE = "cli.workspace_commands" @@ -208,7 +223,9 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict: result["commits_behind"] = commits_behind if commits_behind > 0: result["needs_rebase"] = True - debug(MODULE, f"Main is {commits_behind} commits ahead of worktree base") + debug( + MODULE, f"Main is {commits_behind} commits ahead of worktree base" + ) # Get commit hashes for merge-tree main_commit_result = subprocess.run( @@ -234,7 +251,15 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict: # Use git merge-tree to check for conflicts WITHOUT touching working directory # This is a plumbing command that does a 3-way merge in memory merge_tree_result = subprocess.run( - ["git", "merge-tree", "--write-tree", "--no-messages", merge_base, main_commit, spec_commit], + [ + "git", + "merge-tree", + "--write-tree", + "--no-messages", + merge_base, + main_commit, + spec_commit, + ], cwd=project_dir, capture_output=True, text=True, @@ -253,7 +278,11 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict: if "CONFLICT" in line: # Extract file path from conflict message import re - match = re.search(r"(?:Merge conflict in|CONFLICT.*?:)\s*(.+?)(?:\s*$|\s+\()", line) + + match = re.search( + r"(?:Merge conflict in|CONFLICT.*?:)\s*(.+?)(?:\s*$|\s+\()", + line, + ) if match: file_path = match.group(1).strip() if file_path and file_path not in result["conflicting_files"]: @@ -268,7 +297,11 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict: capture_output=True, text=True, ) - main_files = set(main_files_result.stdout.strip().split("\n")) if main_files_result.stdout.strip() else set() + main_files = ( + set(main_files_result.stdout.strip().split("\n")) + if main_files_result.stdout.strip() + else set() + ) # Files changed in spec branch since merge-base spec_files_result = subprocess.run( @@ -277,12 +310,18 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict: capture_output=True, text=True, ) - spec_files = set(spec_files_result.stdout.strip().split("\n")) if spec_files_result.stdout.strip() else set() + spec_files = ( + set(spec_files_result.stdout.strip().split("\n")) + if spec_files_result.stdout.strip() + else set() + ) # Files modified in both = potential conflicts conflicting = main_files & spec_files result["conflicting_files"] = list(conflicting) - debug(MODULE, f"Found {len(conflicting)} files modified in both branches") + debug( + MODULE, f"Found {len(conflicting)} files modified in both branches" + ) debug(MODULE, f"Conflicting files: {result['conflicting_files']}") else: @@ -291,6 +330,7 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict: except Exception as e: debug_error(MODULE, f"Error checking git conflicts: {e}") import traceback + debug_verbose(MODULE, "Exception traceback", traceback=traceback.format_exc()) return result @@ -316,16 +356,22 @@ def handle_merge_preview_command(project_dir: Path, spec_name: str) -> dict: Dictionary with preview information """ debug_section(MODULE, "Merge Preview Command") - debug(MODULE, "handle_merge_preview_command() called", - project_dir=str(project_dir), - spec_name=spec_name) + debug( + MODULE, + "handle_merge_preview_command() called", + project_dir=str(project_dir), + spec_name=spec_name, + ) from merge import MergeOrchestrator from workspace import get_existing_build_worktree worktree_path = get_existing_build_worktree(project_dir, spec_name) - debug(MODULE, "Worktree lookup result", - worktree_path=str(worktree_path) if worktree_path else None) + debug( + MODULE, + "Worktree lookup result", + worktree_path=str(worktree_path) if worktree_path else None, + ) if not worktree_path: debug_error(MODULE, f"No existing build found for '{spec_name}'") @@ -340,7 +386,7 @@ def handle_merge_preview_command(project_dir: Path, spec_name: str) -> dict: "conflictFiles": 0, "totalConflicts": 0, "autoMergeable": 0, - } + }, } try: @@ -367,36 +413,47 @@ def handle_merge_preview_command(project_dir: Path, spec_name: str) -> dict: # Transform semantic conflicts to UI-friendly format conflicts = [] for c in preview.get("conflicts", []): - debug_verbose(MODULE, "Processing semantic conflict", - file=c.get("file", ""), - severity=c.get("severity", "unknown")) - conflicts.append({ - "file": c.get("file", ""), - "location": c.get("location", ""), - "tasks": c.get("tasks", []), - "severity": c.get("severity", "unknown"), - "canAutoMerge": c.get("can_auto_merge", False), - "strategy": c.get("strategy"), - "reason": c.get("reason", ""), - "type": "semantic", - }) + debug_verbose( + MODULE, + "Processing semantic conflict", + file=c.get("file", ""), + severity=c.get("severity", "unknown"), + ) + conflicts.append( + { + "file": c.get("file", ""), + "location": c.get("location", ""), + "tasks": c.get("tasks", []), + "severity": c.get("severity", "unknown"), + "canAutoMerge": c.get("can_auto_merge", False), + "strategy": c.get("strategy"), + "reason": c.get("reason", ""), + "type": "semantic", + } + ) # Add git conflicts to the list for file_path in git_conflicts.get("conflicting_files", []): - conflicts.append({ - "file": file_path, - "location": "file-level", - "tasks": [spec_name, git_conflicts["base_branch"]], - "severity": "high", - "canAutoMerge": False, - "strategy": None, - "reason": f"File modified in both {git_conflicts['base_branch']} and worktree since branch point", - "type": "git", - }) + conflicts.append( + { + "file": file_path, + "location": "file-level", + "tasks": [spec_name, git_conflicts["base_branch"]], + "severity": "high", + "canAutoMerge": False, + "strategy": None, + "reason": f"File modified in both {git_conflicts['base_branch']} and worktree since branch point", + "type": "git", + } + ) summary = preview.get("summary", {}) - total_conflicts = summary.get("total_conflicts", 0) + len(git_conflicts.get("conflicting_files", [])) - conflict_files = summary.get("conflict_files", 0) + len(git_conflicts.get("conflicting_files", [])) + total_conflicts = summary.get("total_conflicts", 0) + len( + git_conflicts.get("conflicting_files", []) + ) + conflict_files = summary.get("conflict_files", 0) + len( + git_conflicts.get("conflicting_files", []) + ) result = { "success": True, @@ -416,20 +473,24 @@ def handle_merge_preview_command(project_dir: Path, spec_name: str) -> dict: "totalConflicts": total_conflicts, "autoMergeable": summary.get("auto_mergeable", 0), "hasGitConflicts": git_conflicts["has_conflicts"], - } + }, } - debug_success(MODULE, "Merge preview complete", - total_files=result["summary"]["totalFiles"], - total_conflicts=result["summary"]["totalConflicts"], - has_git_conflicts=git_conflicts["has_conflicts"], - auto_mergeable=result["summary"]["autoMergeable"]) + debug_success( + MODULE, + "Merge preview complete", + total_files=result["summary"]["totalFiles"], + total_conflicts=result["summary"]["totalConflicts"], + has_git_conflicts=git_conflicts["has_conflicts"], + auto_mergeable=result["summary"]["autoMergeable"], + ) return result except Exception as e: debug_error(MODULE, "Merge preview failed", error=str(e)) import traceback + debug_verbose(MODULE, "Exception traceback", traceback=traceback.format_exc()) return { "success": False, @@ -442,5 +503,5 @@ def handle_merge_preview_command(project_dir: Path, spec_name: str) -> dict: "conflictFiles": 0, "totalConflicts": 0, "autoMergeable": 0, - } + }, } diff --git a/auto-claude/client.py b/auto-claude/client.py index e95a08bc..61271333 100644 --- a/auto-claude/client.py +++ b/auto-claude/client.py @@ -1,4 +1,5 @@ """Backward compatibility shim - import from core.client instead.""" + import os import sys @@ -7,8 +8,10 @@ _auto_claude_dir = os.path.dirname(os.path.abspath(__file__)) if _auto_claude_dir not in sys.path: sys.path.insert(0, _auto_claude_dir) + # Use lazy imports to avoid circular dependency def __getattr__(name): """Lazy import to avoid circular imports with auto_claude_tools.""" from core import client as _client + return getattr(_client, name) diff --git a/auto-claude/context/graphiti_integration.py b/auto-claude/context/graphiti_integration.py index a8aae03c..2a909f2b 100644 --- a/auto-claude/context/graphiti_integration.py +++ b/auto-claude/context/graphiti_integration.py @@ -22,7 +22,9 @@ except ImportError: return [] -async def fetch_graph_hints(query: str, project_id: str, max_results: int = 5) -> list[dict]: +async def fetch_graph_hints( + query: str, project_id: str, max_results: int = 5 +) -> list[dict]: """ Get historical hints from Graphiti knowledge graph. diff --git a/auto-claude/core/__init__.py b/auto-claude/core/__init__.py index 9c6bc9f1..5dbdeb76 100644 --- a/auto-claude/core/__init__.py +++ b/auto-claude/core/__init__.py @@ -16,21 +16,27 @@ __all__ = [ "ProgressTracker", ] + def __getattr__(name): """Lazy imports to avoid circular dependencies and heavy imports.""" if name in ("run_autonomous_agent", "run_followup_planner"): from .agent import run_autonomous_agent, run_followup_planner + return locals()[name] elif name == "WorkspaceManager": from .workspace import WorkspaceManager + return WorkspaceManager elif name == "WorktreeManager": from .worktree import WorktreeManager + return WorktreeManager elif name == "ProgressTracker": from .progress import ProgressTracker + return ProgressTracker elif name in ("create_claude_client", "ClaudeClient"): from . import client as _client + return getattr(_client, name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/auto-claude/core/client.py b/auto-claude/core/client.py index 8a628f2a..8fa28d5c 100644 --- a/auto-claude/core/client.py +++ b/auto-claude/core/client.py @@ -106,13 +106,13 @@ GRAPHITI_MCP_TOOLS = [ # Electron app must be started with --remote-debugging-port=9222 (or ELECTRON_DEBUG_PORT). # These tools are only available to QA agents (qa_reviewer, qa_fixer), not Coder/Planner. ELECTRON_TOOLS = [ - "mcp__electron__electron_connect", # Connect to Electron app via DevTools - "mcp__electron__electron_screenshot", # Take screenshot of Electron window - "mcp__electron__electron_click", # Click element in Electron app - "mcp__electron__electron_fill", # Fill input field in Electron app - "mcp__electron__electron_evaluate", # Execute JS in Electron renderer - "mcp__electron__electron_get_window_info", # Get window state/bounds - "mcp__electron__electron_get_console", # Get console logs from renderer + "mcp__electron__electron_connect", # Connect to Electron app via DevTools + "mcp__electron__electron_screenshot", # Take screenshot of Electron window + "mcp__electron__electron_click", # Click element in Electron app + "mcp__electron__electron_fill", # Fill input field in Electron app + "mcp__electron__electron_evaluate", # Execute JS in Electron renderer + "mcp__electron__electron_get_window_info", # Get window state/bounds + "mcp__electron__electron_get_console", # Get console logs from renderer ] # Built-in tools @@ -217,7 +217,12 @@ def create_client( # Allow Graphiti MCP tools for knowledge graph memory (if enabled) *(GRAPHITI_MCP_TOOLS if graphiti_mcp_enabled else []), # Allow Electron MCP tools for QA agents only (if enabled) - *(ELECTRON_TOOLS if electron_mcp_enabled and agent_type in ("qa_reviewer", "qa_fixer") else []), + *( + ELECTRON_TOOLS + if electron_mcp_enabled + and agent_type in ("qa_reviewer", "qa_fixer") + else [] + ), ], }, } @@ -238,7 +243,9 @@ def create_client( if graphiti_mcp_enabled: mcp_servers_list.append("graphiti-memory (knowledge graph)") if electron_mcp_enabled: - mcp_servers_list.append(f"electron (desktop automation, port {get_electron_debug_port()})") + mcp_servers_list.append( + f"electron (desktop automation, port {get_electron_debug_port()})" + ) if auto_claude_tools_enabled: mcp_servers_list.append(f"auto-claude ({agent_type} tools)") print(f" - MCP servers: {', '.join(mcp_servers_list)}") diff --git a/auto-claude/core/workspace.py b/auto-claude/core/workspace.py index 5a3077a3..2d4e63af 100644 --- a/auto-claude/core/workspace.py +++ b/auto-claude/core/workspace.py @@ -46,13 +46,28 @@ try: is_debug_enabled, ) except ImportError: - def debug(*args, **kwargs): pass - def debug_detailed(*args, **kwargs): pass - def debug_verbose(*args, **kwargs): pass - def debug_success(*args, **kwargs): pass - def debug_error(*args, **kwargs): pass - def debug_warning(*args, **kwargs): pass - def is_debug_enabled(): return False + + def debug(*args, **kwargs): + pass + + def debug_detailed(*args, **kwargs): + pass + + def debug_verbose(*args, **kwargs): + pass + + def debug_success(*args, **kwargs): + pass + + def debug_error(*args, **kwargs): + pass + + def debug_warning(*args, **kwargs): + pass + + def is_debug_enabled(): + return False + # Import merge system from core.workspace.display import ( @@ -110,6 +125,7 @@ MODULE = "workspace" # - _merge_file_with_ai # - _heuristic_merge + def merge_existing_build( project_dir: Path, spec_name: str, @@ -207,7 +223,7 @@ def merge_existing_build( print() print_status( f"{len(remaining)} conflict(s) require manual resolution:", - "warning" + "warning", ) _print_conflict_info(smart_result) @@ -291,10 +307,13 @@ def _try_smart_merge_inner( no_commit: bool = False, ) -> dict | None: """Inner implementation of smart merge (called with lock held).""" - debug(MODULE, "=== SMART MERGE START ===", - spec_name=spec_name, - worktree_path=str(worktree_path), - no_commit=no_commit) + debug( + MODULE, + "=== SMART MERGE START ===", + spec_name=spec_name, + worktree_path=str(worktree_path), + no_commit=no_commit, + ) try: print(muted(" Analyzing changes with intent-aware merge...")) @@ -308,9 +327,12 @@ def _try_smart_merge_inner( debug_warning(MODULE, f"Could not capture worktree state: {e}") # Initialize the orchestrator - debug(MODULE, "Initializing MergeOrchestrator", - project_dir=str(project_dir), - enable_ai=True) + debug( + MODULE, + "Initializing MergeOrchestrator", + project_dir=str(project_dir), + enable_ai=True, + ) orchestrator = MergeOrchestrator( project_dir, enable_ai=True, # Enable AI for ambiguous conflicts @@ -318,25 +340,38 @@ def _try_smart_merge_inner( ) # Refresh evolution data from the worktree - debug(MODULE, "Refreshing evolution data from git", - spec_name=spec_name) + debug(MODULE, "Refreshing evolution data from git", spec_name=spec_name) orchestrator.evolution_tracker.refresh_from_git(spec_name, worktree_path) # Check for git-level conflicts first (branch divergence) debug(MODULE, "Checking for git-level conflicts") git_conflicts = _check_git_conflicts(project_dir, spec_name) - debug_detailed(MODULE, "Git conflict check result", - has_conflicts=git_conflicts.get("has_conflicts"), - conflicting_files=git_conflicts.get("conflicting_files", []), - base_branch=git_conflicts.get("base_branch")) + debug_detailed( + MODULE, + "Git conflict check result", + has_conflicts=git_conflicts.get("has_conflicts"), + conflicting_files=git_conflicts.get("conflicting_files", []), + base_branch=git_conflicts.get("base_branch"), + ) if git_conflicts.get("has_conflicts"): - print(muted(f" Branch has diverged from {git_conflicts.get('base_branch', 'main')}")) - print(muted(f" Conflicting files: {len(git_conflicts.get('conflicting_files', []))}")) + print( + muted( + f" Branch has diverged from {git_conflicts.get('base_branch', 'main')}" + ) + ) + print( + muted( + f" Conflicting files: {len(git_conflicts.get('conflicting_files', []))}" + ) + ) - debug(MODULE, "Starting AI conflict resolution", - num_conflicts=len(git_conflicts.get("conflicting_files", []))) + debug( + MODULE, + "Starting AI conflict resolution", + num_conflicts=len(git_conflicts.get("conflicting_files", [])), + ) # Try to resolve git conflicts with AI resolution_result = _resolve_git_conflicts_with_ai( @@ -349,16 +384,24 @@ def _try_smart_merge_inner( ) if resolution_result.get("success"): - debug_success(MODULE, "AI conflict resolution succeeded", - resolved_files=resolution_result.get("resolved_files", []), - stats=resolution_result.get("stats", {})) + debug_success( + MODULE, + "AI conflict resolution succeeded", + resolved_files=resolution_result.get("resolved_files", []), + stats=resolution_result.get("stats", {}), + ) return resolution_result else: # AI couldn't resolve all conflicts - debug_error(MODULE, "AI conflict resolution failed", - remaining_conflicts=resolution_result.get("remaining_conflicts", []), - resolved_files=resolution_result.get("resolved_files", []), - error=resolution_result.get("error")) + debug_error( + MODULE, + "AI conflict resolution failed", + remaining_conflicts=resolution_result.get( + "remaining_conflicts", [] + ), + resolved_files=resolution_result.get("resolved_files", []), + error=resolution_result.get("error"), + ) return { "success": False, "conflicts": resolution_result.get("remaining_conflicts", []), @@ -382,10 +425,7 @@ def _try_smart_merge_inner( print(muted(f" Auto-mergeable: {auto_mergeable}/{len(conflicts)}")) # Check if any conflicts need human review - needs_human = [ - c for c in conflicts - if not c.get("can_auto_merge") - ] + needs_human = [c for c in conflicts if not c.get("can_auto_merge")] if needs_human: return { @@ -407,6 +447,7 @@ def _try_smart_merge_inner( except Exception as e: # If smart merge fails, fall back to git import traceback + print(muted(f" Smart merge error: {e}")) traceback.print_exc() return None @@ -479,7 +520,15 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict: # Use git merge-tree to check for conflicts WITHOUT touching working directory merge_tree_result = subprocess.run( - ["git", "merge-tree", "--write-tree", "--no-messages", merge_base, main_commit, spec_commit], + [ + "git", + "merge-tree", + "--write-tree", + "--no-messages", + merge_base, + main_commit, + spec_commit, + ], cwd=project_dir, capture_output=True, text=True, @@ -493,7 +542,10 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict: output = merge_tree_result.stdout + merge_tree_result.stderr for line in output.split("\n"): if "CONFLICT" in line: - match = re.search(r"(?:Merge conflict in|CONFLICT.*?:)\s*(.+?)(?:\s*$|\s+\()", line) + match = re.search( + r"(?:Merge conflict in|CONFLICT.*?:)\s*(.+?)(?:\s*$|\s+\()", + line, + ) if match: file_path = match.group(1).strip() if file_path and file_path not in result["conflicting_files"]: @@ -507,7 +559,11 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict: capture_output=True, text=True, ) - main_files = set(main_files_result.stdout.strip().split("\n")) if main_files_result.stdout.strip() else set() + main_files = ( + set(main_files_result.stdout.strip().split("\n")) + if main_files_result.stdout.strip() + else set() + ) spec_files_result = subprocess.run( ["git", "diff", "--name-only", merge_base, spec_commit], @@ -515,7 +571,11 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict: capture_output=True, text=True, ) - spec_files = set(spec_files_result.stdout.strip().split("\n")) if spec_files_result.stdout.strip() else set() + spec_files = ( + set(spec_files_result.stdout.strip().split("\n")) + if spec_files_result.stdout.strip() + else set() + ) # Files modified in both = potential conflicts conflicting = main_files & spec_files @@ -550,18 +610,24 @@ def _resolve_git_conflicts_with_ai( Dict with success, resolved_files, remaining_conflicts """ - debug(MODULE, "=== AI CONFLICT RESOLUTION START ===", - spec_name=spec_name, - num_conflicting_files=len(git_conflicts.get("conflicting_files", []))) + debug( + MODULE, + "=== AI CONFLICT RESOLUTION START ===", + spec_name=spec_name, + num_conflicting_files=len(git_conflicts.get("conflicting_files", [])), + ) conflicting_files = git_conflicts.get("conflicting_files", []) base_branch = git_conflicts.get("base_branch", "main") spec_branch = git_conflicts.get("spec_branch", f"auto-claude/{spec_name}") - debug_detailed(MODULE, "Conflict resolution params", - base_branch=base_branch, - spec_branch=spec_branch, - conflicting_files=conflicting_files) + debug_detailed( + MODULE, + "Conflict resolution params", + base_branch=base_branch, + spec_branch=spec_branch, + conflicting_files=conflicting_files, + ) resolved_files = [] remaining_conflicts = [] @@ -569,7 +635,9 @@ def _resolve_git_conflicts_with_ai( ai_merged_count = 0 print() - print_status(f"Resolving {len(conflicting_files)} conflicting file(s) with AI...", "progress") + print_status( + f"Resolving {len(conflicting_files)} conflicting file(s) with AI...", "progress" + ) # Get merge-base commit merge_base_result = subprocess.run( @@ -578,24 +646,38 @@ def _resolve_git_conflicts_with_ai( capture_output=True, text=True, ) - merge_base = merge_base_result.stdout.strip() if merge_base_result.returncode == 0 else None - debug(MODULE, "Found merge-base commit", merge_base=merge_base[:12] if merge_base else None) + merge_base = ( + merge_base_result.stdout.strip() if merge_base_result.returncode == 0 else None + ) + debug( + MODULE, + "Found merge-base commit", + merge_base=merge_base[:12] if merge_base else None, + ) # FIX: Copy NEW files FIRST before resolving conflicts # This ensures dependencies exist before files that import them are written - changed_files = _get_changed_files_from_branch(project_dir, base_branch, spec_branch) - new_files = [(f, s) for f, s in changed_files if s == "A" and f not in conflicting_files] + changed_files = _get_changed_files_from_branch( + project_dir, base_branch, spec_branch + ) + new_files = [ + (f, s) for f, s in changed_files if s == "A" and f not in conflicting_files + ] if new_files: print(muted(f" Copying {len(new_files)} new file(s) first (dependencies)...")) for file_path, status in new_files: try: - content = _get_file_content_from_ref(project_dir, spec_branch, file_path) + content = _get_file_content_from_ref( + project_dir, spec_branch, file_path + ) if content is not None: target_path = project_dir / file_path target_path.parent.mkdir(parents=True, exist_ok=True) target_path.write_text(content, encoding="utf-8") - subprocess.run(["git", "add", file_path], cwd=project_dir, capture_output=True) + subprocess.run( + ["git", "add", file_path], cwd=project_dir, capture_output=True + ) resolved_files.append(file_path) debug(MODULE, f"Copied new file: {file_path}") except Exception as e: @@ -603,7 +685,9 @@ def _resolve_git_conflicts_with_ai( # Categorize conflicting files for processing files_needing_ai_merge: list[ParallelMergeTask] = [] - simple_merges: list[tuple[str, str | None]] = [] # (file_path, merged_content or None for delete) + simple_merges: list[ + tuple[str, str | None] + ] = [] # (file_path, merged_content or None for delete) debug(MODULE, "Categorizing conflicting files for parallel processing") @@ -612,15 +696,21 @@ def _resolve_git_conflicts_with_ai( try: # Get content from main branch - main_content = _get_file_content_from_ref(project_dir, base_branch, file_path) + main_content = _get_file_content_from_ref( + project_dir, base_branch, file_path + ) # Get content from worktree branch - worktree_content = _get_file_content_from_ref(project_dir, spec_branch, file_path) + worktree_content = _get_file_content_from_ref( + project_dir, spec_branch, file_path + ) # Get content from merge-base (common ancestor) base_content = None if merge_base: - base_content = _get_file_content_from_ref(project_dir, merge_base, file_path) + base_content = _get_file_content_from_ref( + project_dir, merge_base, file_path + ) if main_content is None and worktree_content is None: # File doesn't exist in either - skip @@ -643,22 +733,26 @@ def _resolve_git_conflicts_with_ai( debug(MODULE, f" {file_path}: lock file (taking worktree version)") else: # Regular file - needs AI merge - files_needing_ai_merge.append(ParallelMergeTask( - file_path=file_path, - main_content=main_content, - worktree_content=worktree_content, - base_content=base_content, - spec_name=spec_name, - )) + files_needing_ai_merge.append( + ParallelMergeTask( + file_path=file_path, + main_content=main_content, + worktree_content=worktree_content, + base_content=base_content, + spec_name=spec_name, + ) + ) debug(MODULE, f" {file_path}: needs AI merge") except Exception as e: print(error(f" ✗ Failed to categorize {file_path}: {e}")) - remaining_conflicts.append({ - "file": file_path, - "reason": str(e), - "severity": "high", - }) + remaining_conflicts.append( + { + "file": file_path, + "reason": str(e), + "severity": "high", + } + ) # Process simple merges first (fast, no AI) if simple_merges: @@ -669,11 +763,17 @@ def _resolve_git_conflicts_with_ai( target_path = project_dir / file_path target_path.parent.mkdir(parents=True, exist_ok=True) target_path.write_text(merged_content, encoding="utf-8") - subprocess.run(["git", "add", file_path], cwd=project_dir, capture_output=True) + subprocess.run( + ["git", "add", file_path], cwd=project_dir, capture_output=True + ) resolved_files.append(file_path) # Determine the type for display if _is_lock_file(file_path): - print(success(f" ✓ {file_path} (lock file - took worktree version)")) + print( + success( + f" ✓ {file_path} (lock file - took worktree version)" + ) + ) else: print(success(f" ✓ {file_path} (new file)")) else: @@ -681,23 +781,33 @@ def _resolve_git_conflicts_with_ai( target_path = project_dir / file_path if target_path.exists(): target_path.unlink() - subprocess.run(["git", "add", file_path], cwd=project_dir, capture_output=True) + subprocess.run( + ["git", "add", file_path], + cwd=project_dir, + capture_output=True, + ) resolved_files.append(file_path) print(success(f" ✓ {file_path} (deleted)")) except Exception as e: print(error(f" ✗ {file_path}: {e}")) - remaining_conflicts.append({ - "file": file_path, - "reason": str(e), - "severity": "high", - }) + remaining_conflicts.append( + { + "file": file_path, + "reason": str(e), + "severity": "high", + } + ) # Process AI merges in parallel if files_needing_ai_merge: print() - print_status(f"Merging {len(files_needing_ai_merge)} file(s) with AI (parallel)...", "progress") + print_status( + f"Merging {len(files_needing_ai_merge)} file(s) with AI (parallel)...", + "progress", + ) import time + start_time = time.time() # Run parallel merges @@ -717,7 +827,11 @@ def _resolve_git_conflicts_with_ai( target_path = project_dir / result.file_path target_path.parent.mkdir(parents=True, exist_ok=True) target_path.write_text(result.merged_content, encoding="utf-8") - subprocess.run(["git", "add", result.file_path], cwd=project_dir, capture_output=True) + subprocess.run( + ["git", "add", result.file_path], + cwd=project_dir, + capture_output=True, + ) resolved_files.append(result.file_path) if result.was_auto_merged: @@ -728,11 +842,13 @@ def _resolve_git_conflicts_with_ai( print(success(f" ✓ {result.file_path} (AI merged)")) else: print(error(f" ✗ {result.file_path}: {result.error}")) - remaining_conflicts.append({ - "file": result.file_path, - "reason": result.error or "AI could not resolve the conflict", - "severity": "high", - }) + remaining_conflicts.append( + { + "file": result.file_path, + "reason": result.error or "AI could not resolve the conflict", + "severity": "high", + } + ) # Print summary print() @@ -749,7 +865,8 @@ def _resolve_git_conflicts_with_ai( # Get list of modified/deleted files (new files already copied at start) non_conflicting = [ - (f, s) for f, s in changed_files + (f, s) + for f, s in changed_files if f not in conflicting_files and s != "A" # Skip new files, already copied ] @@ -760,15 +877,21 @@ def _resolve_git_conflicts_with_ai( target_path = project_dir / file_path if target_path.exists(): target_path.unlink() - subprocess.run(["git", "add", file_path], cwd=project_dir, capture_output=True) + subprocess.run( + ["git", "add", file_path], cwd=project_dir, capture_output=True + ) else: # Added or modified - copy from worktree - content = _get_file_content_from_ref(project_dir, spec_branch, file_path) + content = _get_file_content_from_ref( + project_dir, spec_branch, file_path + ) if content is not None: target_path = project_dir / file_path target_path.parent.mkdir(parents=True, exist_ok=True) target_path.write_text(content, encoding="utf-8") - subprocess.run(["git", "add", file_path], cwd=project_dir, capture_output=True) + subprocess.run( + ["git", "add", file_path], cwd=project_dir, capture_output=True + ) resolved_files.append(file_path) except Exception as e: print(muted(f" Warning: Could not process {file_path}: {e}")) @@ -796,7 +919,9 @@ def _resolve_git_conflicts_with_ai( result["remaining_conflicts"] = remaining_conflicts result["partial_success"] = len(resolved_files) > 0 print() - print(warning(f" ⚠ {len(remaining_conflicts)} file(s) could not be auto-merged:")) + print( + warning(f" ⚠ {len(remaining_conflicts)} file(s) could not be auto-merged:") + ) for conflict in remaining_conflicts: print(muted(f" - {conflict['file']}: {conflict['reason']}")) print(muted(" These files may need manual review.")) diff --git a/auto-claude/core/workspace/__init__.py b/auto-claude/core/workspace/__init__.py index 93d76ccf..43a9e23e 100644 --- a/auto-claude/core/workspace/__init__.py +++ b/auto-claude/core/workspace/__init__.py @@ -15,6 +15,7 @@ This package provides: Public API exported from sub-modules. """ + import importlib.util import sys from pathlib import Path @@ -103,42 +104,42 @@ from .setup import ( __all__ = [ # Merge Operations (from workspace.py) - 'merge_existing_build', + "merge_existing_build", # '_run_parallel_merges', # TODO: not yet implemented - Private but used by tests # Models - 'WorkspaceMode', - 'WorkspaceChoice', - 'ParallelMergeTask', - 'ParallelMergeResult', - 'MergeLock', - 'MergeLockError', + "WorkspaceMode", + "WorkspaceChoice", + "ParallelMergeTask", + "ParallelMergeResult", + "MergeLock", + "MergeLockError", # Git Utils - 'has_uncommitted_changes', - 'get_current_branch', - 'get_existing_build_worktree', - 'get_file_content_from_ref', - 'get_changed_files_from_branch', - 'is_process_running', - 'is_binary_file', - 'validate_merged_syntax', - 'create_conflict_file_with_git', + "has_uncommitted_changes", + "get_current_branch", + "get_existing_build_worktree", + "get_file_content_from_ref", + "get_changed_files_from_branch", + "is_process_running", + "is_binary_file", + "validate_merged_syntax", + "create_conflict_file_with_git", # Setup - 'choose_workspace', - 'copy_spec_to_worktree', - 'setup_workspace', - 'ensure_timeline_hook_installed', - 'initialize_timeline_tracking', + "choose_workspace", + "copy_spec_to_worktree", + "setup_workspace", + "ensure_timeline_hook_installed", + "initialize_timeline_tracking", # Display - 'show_build_summary', - 'show_changed_files', - 'print_merge_success', - 'print_conflict_info', + "show_build_summary", + "show_changed_files", + "print_merge_success", + "print_conflict_info", # Finalization - 'finalize_workspace', - 'handle_workspace_choice', - 'review_existing_build', - 'discard_existing_build', - 'check_existing_build', - 'list_all_worktrees', - 'cleanup_all_worktrees', + "finalize_workspace", + "handle_workspace_choice", + "review_existing_build", + "discard_existing_build", + "check_existing_build", + "list_all_worktrees", + "cleanup_all_worktrees", ] diff --git a/auto-claude/core/workspace/display.py b/auto-claude/core/workspace/display.py index 07650752..1ac6bd43 100644 --- a/auto-claude/core/workspace/display.py +++ b/auto-claude/core/workspace/display.py @@ -6,7 +6,6 @@ Workspace Display Functions for displaying workspace information and build summaries. """ - from ui import ( bold, error, @@ -91,17 +90,25 @@ def print_merge_success(no_commit: bool, stats: dict | None = None) -> None: if stats: lines.append("What changed:") if stats.get("files_added", 0) > 0: - lines.append(f" + {stats['files_added']} file{'s' if stats['files_added'] != 1 else ''} added") + lines.append( + f" + {stats['files_added']} file{'s' if stats['files_added'] != 1 else ''} added" + ) if stats.get("files_modified", 0) > 0: - lines.append(f" ~ {stats['files_modified']} file{'s' if stats['files_modified'] != 1 else ''} modified") + lines.append( + f" ~ {stats['files_modified']} file{'s' if stats['files_modified'] != 1 else ''} modified" + ) if stats.get("files_deleted", 0) > 0: - lines.append(f" - {stats['files_deleted']} file{'s' if stats['files_deleted'] != 1 else ''} deleted") + lines.append( + f" - {stats['files_deleted']} file{'s' if stats['files_deleted'] != 1 else ''} deleted" + ) lines.append("") - lines.extend([ - "Your new feature is now part of your project.", - "The separate workspace has been cleaned up.", - ]) + lines.extend( + [ + "Your new feature is now part of your project.", + "The separate workspace has been cleaned up.", + ] + ) content = lines print() @@ -118,7 +125,11 @@ def print_conflict_info(result: dict) -> None: return print() - print(warning(f" {len(conflicts)} file{'s' if len(conflicts) != 1 else ''} had conflicts:")) + print( + warning( + f" {len(conflicts)} file{'s' if len(conflicts) != 1 else ''} had conflicts:" + ) + ) for conflict_file in conflicts: print(f" {highlight(conflict_file)}") print() diff --git a/auto-claude/core/workspace/git_utils.py b/auto-claude/core/workspace/git_utils.py index 0512430f..4daa3e46 100644 --- a/auto-claude/core/workspace/git_utils.py +++ b/auto-claude/core/workspace/git_utils.py @@ -18,26 +18,60 @@ MAX_PARALLEL_AI_MERGES = 5 # Limit concurrent AI merge operations # These are auto-generated and should just take the worktree version # then regenerate via package manager install LOCK_FILES = { - 'package-lock.json', - 'pnpm-lock.yaml', - 'yarn.lock', - 'bun.lockb', - 'Pipfile.lock', - 'poetry.lock', - 'Cargo.lock', - 'Gemfile.lock', - 'composer.lock', - 'go.sum', + "package-lock.json", + "pnpm-lock.yaml", + "yarn.lock", + "bun.lockb", + "Pipfile.lock", + "poetry.lock", + "Cargo.lock", + "Gemfile.lock", + "composer.lock", + "go.sum", } BINARY_EXTENSIONS = { - '.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.bmp', '.svg', - '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', - '.zip', '.tar', '.gz', '.rar', '.7z', - '.exe', '.dll', '.so', '.dylib', '.bin', - '.mp3', '.mp4', '.wav', '.avi', '.mov', '.mkv', - '.woff', '.woff2', '.ttf', '.otf', '.eot', - '.pyc', '.pyo', '.class', '.o', '.obj', + ".png", + ".jpg", + ".jpeg", + ".gif", + ".ico", + ".webp", + ".bmp", + ".svg", + ".pdf", + ".doc", + ".docx", + ".xls", + ".xlsx", + ".ppt", + ".pptx", + ".zip", + ".tar", + ".gz", + ".rar", + ".7z", + ".exe", + ".dll", + ".so", + ".dylib", + ".bin", + ".mp3", + ".mp4", + ".wav", + ".avi", + ".mov", + ".mkv", + ".woff", + ".woff2", + ".ttf", + ".otf", + ".eot", + ".pyc", + ".pyo", + ".class", + ".o", + ".obj", } # Merge lock timeout in seconds @@ -88,7 +122,9 @@ def get_existing_build_worktree(project_dir: Path, spec_name: str) -> Path | Non return None -def get_file_content_from_ref(project_dir: Path, ref: str, file_path: str) -> str | None: +def get_file_content_from_ref( + project_dir: Path, ref: str, file_path: str +) -> str | None: """Get file content from a git ref (branch, commit, etc.).""" result = subprocess.run( ["git", "show", f"{ref}:{file_path}"], @@ -127,6 +163,7 @@ def get_changed_files_from_branch( def is_process_running(pid: int) -> bool: """Check if a process with the given PID is running.""" import os + try: os.kill(pid, 0) return True @@ -149,7 +186,9 @@ def is_lock_file(file_path: str) -> bool: return Path(file_path).name in LOCK_FILES -def validate_merged_syntax(file_path: str, content: str, project_dir: Path) -> tuple[bool, str]: +def validate_merged_syntax( + file_path: str, content: str, project_dir: Path +) -> tuple[bool, str]: """ Validate the syntax of merged code. @@ -166,11 +205,11 @@ def validate_merged_syntax(file_path: str, content: str, project_dir: Path) -> t ext = P(file_path).suffix.lower() # TypeScript/JavaScript validation using esbuild - if ext in {'.ts', '.tsx', '.js', '.jsx'}: + if ext in {".ts", ".tsx", ".js", ".jsx"}: try: # Write to temp file in system temp dir (NOT project dir to avoid HMR triggers) with tempfile.NamedTemporaryFile( - mode='w', + mode="w", suffix=ext, delete=False, # Don't set dir= to avoid writing to project directory which triggers HMR @@ -185,14 +224,16 @@ def validate_merged_syntax(file_path: str, content: str, project_dir: Path) -> t # Try to find esbuild in node_modules (works with pnpm, npm, yarn) for search_dir in [project_dir, project_dir.parent]: # pnpm stores it differently - pnpm_esbuild = search_dir / 'node_modules' / '.pnpm' + pnpm_esbuild = search_dir / "node_modules" / ".pnpm" if pnpm_esbuild.exists(): - for esbuild_dir in pnpm_esbuild.glob('esbuild@*/node_modules/esbuild/bin/esbuild'): + for esbuild_dir in pnpm_esbuild.glob( + "esbuild@*/node_modules/esbuild/bin/esbuild" + ): if esbuild_dir.exists(): esbuild_cmd = str(esbuild_dir) break # Standard npm/yarn location - npm_esbuild = search_dir / 'node_modules' / '.bin' / 'esbuild' + npm_esbuild = search_dir / "node_modules" / ".bin" / "esbuild" if npm_esbuild.exists(): esbuild_cmd = str(npm_esbuild) break @@ -201,10 +242,10 @@ def validate_merged_syntax(file_path: str, content: str, project_dir: Path) -> t # Fall back to npx if not found if not esbuild_cmd: - esbuild_cmd = 'npx' - args = ['npx', 'esbuild', tmp_path, '--log-level=error'] + esbuild_cmd = "npx" + args = ["npx", "esbuild", tmp_path, "--log-level=error"] else: - args = [esbuild_cmd, tmp_path, '--log-level=error'] + args = [esbuild_cmd, tmp_path, "--log-level=error"] # Use esbuild for fast, accurate syntax validation # esbuild infers loader from extension (.tsx, .ts, etc.) @@ -221,12 +262,15 @@ def validate_merged_syntax(file_path: str, content: str, project_dir: Path) -> t # Filter out npm warnings and extract actual errors error_output = result.stderr.strip() error_lines = [ - line for line in error_output.split('\n') - if line and not line.startswith('npm warn') and not line.startswith('npm WARN') + line + for line in error_output.split("\n") + if line + and not line.startswith("npm warn") + and not line.startswith("npm WARN") ] if error_lines: # Extract just the error message, not full path - error_msg = '\n'.join(error_lines[:3]) + error_msg = "\n".join(error_lines[:3]) return False, f"Syntax error: {error_msg}" return True, "" @@ -242,15 +286,15 @@ def validate_merged_syntax(file_path: str, content: str, project_dir: Path) -> t return True, "" # Other errors = skip validation # Python validation - elif ext == '.py': + elif ext == ".py": try: - compile(content, file_path, 'exec') + compile(content, file_path, "exec") return True, "" except SyntaxError as e: return False, f"Python syntax error: {e.msg} at line {e.lineno}" # JSON validation - elif ext == '.json': + elif ext == ".json": try: json.loads(content) return True, "" @@ -278,21 +322,27 @@ def create_conflict_file_with_git( try: # Create temp files for three-way merge - with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.tmp') as main_f: + with tempfile.NamedTemporaryFile( + mode="w", delete=False, suffix=".tmp" + ) as main_f: main_f.write(main_content) main_path = main_f.name - with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.tmp') as wt_f: + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".tmp") as wt_f: wt_f.write(worktree_content) wt_path = wt_f.name # Use empty base if not available if base_content: - with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.tmp') as base_f: + with tempfile.NamedTemporaryFile( + mode="w", delete=False, suffix=".tmp" + ) as base_f: base_f.write(base_content) base_path = base_f.name else: - with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.tmp') as base_f: + with tempfile.NamedTemporaryFile( + mode="w", delete=False, suffix=".tmp" + ) as base_f: base_f.write("") base_path = base_f.name @@ -300,7 +350,7 @@ def create_conflict_file_with_git( # git merge-file # Exit codes: 0 = clean merge, 1 = conflicts, >1 = error result = subprocess.run( - ['git', 'merge-file', '-p', main_path, base_path, wt_path], + ["git", "merge-file", "-p", main_path, base_path, wt_path], cwd=project_dir, capture_output=True, text=True, diff --git a/auto-claude/core/workspace/models.py b/auto-claude/core/workspace/models.py index 66ee4472..164e3418 100644 --- a/auto-claude/core/workspace/models.py +++ b/auto-claude/core/workspace/models.py @@ -30,6 +30,7 @@ class WorkspaceChoice(Enum): @dataclass class ParallelMergeTask: """A file merge task to be executed in parallel.""" + file_path: str main_content: str worktree_content: str @@ -40,6 +41,7 @@ class ParallelMergeTask: @dataclass class ParallelMergeResult: """Result of a parallel merge task.""" + file_path: str merged_content: str | None success: bool @@ -49,6 +51,7 @@ class ParallelMergeResult: class MergeLockError(Exception): """Raised when a merge lock cannot be acquired.""" + pass @@ -100,6 +103,7 @@ class MergeLock: pid = int(self.lock_file.read_text().strip()) # Import locally to avoid circular dependency import os as _os + try: _os.kill(pid, 0) is_running = True diff --git a/auto-claude/core/workspace/setup.py b/auto-claude/core/workspace/setup.py index 142c4ff2..b1dabeda 100644 --- a/auto-claude/core/workspace/setup.py +++ b/auto-claude/core/workspace/setup.py @@ -32,8 +32,13 @@ from .models import WorkspaceMode try: from debug import debug, debug_warning except ImportError: - def debug(*args, **kwargs): pass - def debug_warning(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + + def debug_warning(*args, **kwargs): + pass + # Track if we've already tried to install the git hook this session _git_hook_check_done = False @@ -274,6 +279,7 @@ def ensure_timeline_hook_installed(project_dir: Path) -> None: # Auto-install the hook (silent, non-intrusive) from merge.install_hook import install_hook + install_hook(project_dir) debug(MODULE, "Auto-installed FileTimelineTracker git hook") @@ -333,9 +339,12 @@ def initialize_timeline_tracking( task_intent=task_intent, task_title=task_title, ) - debug(MODULE, f"Timeline tracking initialized for {spec_name}", - files_tracked=len(files_to_modify), - branch_point=branch_point[:8] if branch_point else None) + debug( + MODULE, + f"Timeline tracking initialized for {spec_name}", + files_tracked=len(files_to_modify), + branch_point=branch_point[:8] if branch_point else None, + ) else: # Initialize retroactively from worktree if no plan tracker.initialize_from_worktree( diff --git a/auto-claude/critique.py b/auto-claude/critique.py index c3a69d49..0310aac5 100644 --- a/auto-claude/critique.py +++ b/auto-claude/critique.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from spec.critique instead.""" + from spec.critique import * # noqa: F403 diff --git a/auto-claude/debug.py b/auto-claude/debug.py index 6b7b22a2..e3759336 100644 --- a/auto-claude/debug.py +++ b/auto-claude/debug.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from core.debug instead.""" + from core.debug import * # noqa: F403 diff --git a/auto-claude/graphiti_config.py b/auto-claude/graphiti_config.py index 5729e86f..a5e67807 100644 --- a/auto-claude/graphiti_config.py +++ b/auto-claude/graphiti_config.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from integrations.graphiti.config instead.""" + from integrations.graphiti.config import * # noqa: F403 diff --git a/auto-claude/graphiti_providers.py b/auto-claude/graphiti_providers.py index a679abe8..a5571fdc 100644 --- a/auto-claude/graphiti_providers.py +++ b/auto-claude/graphiti_providers.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from integrations.graphiti.providers_pkg instead.""" + from integrations.graphiti.providers_pkg import * # noqa: F403 diff --git a/auto-claude/ideation/output_streamer.py b/auto-claude/ideation/output_streamer.py index 2fa4d313..34102704 100644 --- a/auto-claude/ideation/output_streamer.py +++ b/auto-claude/ideation/output_streamer.py @@ -46,9 +46,7 @@ class OutputStreamer: Returns: IdeationPhaseResult for the completed phase """ - result = await phase_executor.execute_ideation_type( - ideation_type, max_retries - ) + result = await phase_executor.execute_ideation_type(ideation_type, max_retries) if result.success: # Signal that this type is complete - UI can now show these ideas diff --git a/auto-claude/implementation_plan.py b/auto-claude/implementation_plan.py index ecb0adcd..6669efbe 100644 --- a/auto-claude/implementation_plan.py +++ b/auto-claude/implementation_plan.py @@ -1,3 +1,4 @@ """Backward compatibility shim - import from implementation_plan package instead.""" + from implementation_plan import * # noqa: F403 from implementation_plan.main import * # noqa: F403 diff --git a/auto-claude/linear_config.py b/auto-claude/linear_config.py index b373574b..20ac16d3 100644 --- a/auto-claude/linear_config.py +++ b/auto-claude/linear_config.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from integrations.linear.config instead.""" + from integrations.linear.config import * # noqa: F403 diff --git a/auto-claude/linear_integration.py b/auto-claude/linear_integration.py index bca92799..cf052f3a 100644 --- a/auto-claude/linear_integration.py +++ b/auto-claude/linear_integration.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from integrations.linear.integration instead.""" + from integrations.linear.integration import * # noqa: F403 diff --git a/auto-claude/linear_updater.py b/auto-claude/linear_updater.py index ffcd25a2..ab44130c 100644 --- a/auto-claude/linear_updater.py +++ b/auto-claude/linear_updater.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from integrations.linear.updater instead.""" + from integrations.linear.updater import * # noqa: F403 diff --git a/auto-claude/memory/sessions.py b/auto-claude/memory/sessions.py index 0ff3ddd0..3bd7fdb6 100644 --- a/auto-claude/memory/sessions.py +++ b/auto-claude/memory/sessions.py @@ -22,7 +22,9 @@ from .paths import get_session_insights_dir logger = logging.getLogger(__name__) -def save_session_insights(spec_dir: Path, session_num: int, insights: dict[str, Any]) -> None: +def save_session_insights( + spec_dir: Path, session_num: int, insights: dict[str, Any] +) -> None: """ Save insights from a completed session. diff --git a/auto-claude/merge/ai_resolver/context.py b/auto-claude/merge/ai_resolver/context.py index e2833f6f..a175bada 100644 --- a/auto-claude/merge/ai_resolver/context.py +++ b/auto-claude/merge/ai_resolver/context.py @@ -29,7 +29,9 @@ class ConflictContext: file_path: str location: str baseline_code: str # The code before any task modified it - task_changes: list[tuple[str, str, list[SemanticChange]]] # (task_id, intent, changes) + task_changes: list[ + tuple[str, str, list[SemanticChange]] + ] # (task_id, intent, changes) conflict_description: str language: str = "unknown" @@ -60,10 +62,12 @@ class ConflictContext: content = content[:500] + "... (truncated)" lines.append(f" Code: {content}") - lines.extend([ - "", - f"CONFLICT: {self.conflict_description}", - ]) + lines.extend( + [ + "", + f"CONFLICT: {self.conflict_description}", + ] + ) return "\n".join(lines) diff --git a/auto-claude/merge/ai_resolver/parsers.py b/auto-claude/merge/ai_resolver/parsers.py index 686784bd..2e9cc07e 100644 --- a/auto-claude/merge/ai_resolver/parsers.py +++ b/auto-claude/merge/ai_resolver/parsers.py @@ -71,7 +71,9 @@ def looks_like_code(text: str, language: str) -> bool: return any(ind in text for ind in lang_indicators) # Generic code indicators - return any(ind in text for ind in ["=", "(", ")", "{", "}", "import", "def", "function"]) + return any( + ind in text for ind in ["=", "(", ")", "{", "}", "import", "def", "function"] + ) def extract_batch_code_blocks( diff --git a/auto-claude/merge/ai_resolver/prompts.py b/auto-claude/merge/ai_resolver/prompts.py index b70f3a6b..de7df7f7 100644 --- a/auto-claude/merge/ai_resolver/prompts.py +++ b/auto-claude/merge/ai_resolver/prompts.py @@ -14,7 +14,7 @@ from __future__ import annotations SYSTEM_PROMPT = "You are an expert code merge assistant. Be concise and precise." # Main merge prompt template -MERGE_PROMPT_TEMPLATE = '''You are a code merge assistant. Your task is to merge changes from multiple development tasks into a single coherent result. +MERGE_PROMPT_TEMPLATE = """You are a code merge assistant. Your task is to merge changes from multiple development tasks into a single coherent result. CONTEXT: {context} @@ -38,10 +38,10 @@ Return only the merged code block, wrapped in triple backticks with the language merged code here ``` -Merge the code now:''' +Merge the code now:""" # Batch merge prompt template for multiple conflicts in the same file -BATCH_MERGE_PROMPT_TEMPLATE = '''You are a code merge assistant. Your task is to merge changes from multiple development tasks. +BATCH_MERGE_PROMPT_TEMPLATE = """You are a code merge assistant. Your task is to merge changes from multiple development tasks. There are {num_conflicts} conflict regions in {file_path}. Resolve each one. @@ -54,7 +54,7 @@ For each conflict region, output the merged code in a separate code block labele merged code ``` -Resolve all conflicts now:''' +Resolve all conflicts now:""" def format_merge_prompt(context: str, language: str) -> str: diff --git a/auto-claude/merge/ai_resolver/resolver.py b/auto-claude/merge/ai_resolver/resolver.py index 4ed35f83..257d6c07 100644 --- a/auto-claude/merge/ai_resolver/resolver.py +++ b/auto-claude/merge/ai_resolver/resolver.py @@ -114,16 +114,20 @@ class AIResolver: continue relevant_changes = [ - c for c in snapshot.semantic_changes - if c.location == conflict.location or locations_overlap(c.location, conflict.location) + c + for c in snapshot.semantic_changes + if c.location == conflict.location + or locations_overlap(c.location, conflict.location) ] if relevant_changes: - task_changes.append(( - snapshot.task_id, - snapshot.task_intent or "No intent specified", - relevant_changes, - )) + task_changes.append( + ( + snapshot.task_id, + snapshot.task_intent or "No intent specified", + relevant_changes, + ) + ) # Determine language from file extension language = infer_language(conflict.file_path) @@ -264,9 +268,11 @@ class AIResolver: if len(file_conflicts) == 1: # Single conflict, resolve individually baseline = baseline_codes.get(file_conflicts[0].location, "") - results.append(self.resolve_conflict( - file_conflicts[0], baseline, task_snapshots - )) + results.append( + self.resolve_conflict( + file_conflicts[0], baseline, task_snapshots + ) + ) else: # Multiple conflicts in same file - batch resolve result = self._resolve_file_batch( @@ -277,9 +283,9 @@ class AIResolver: # Resolve each individually for conflict in conflicts: baseline = baseline_codes.get(conflict.location, "") - results.append(self.resolve_conflict( - conflict, baseline, task_snapshots - )) + results.append( + self.resolve_conflict(conflict, baseline, task_snapshots) + ) return results @@ -317,7 +323,9 @@ class AIResolver: results = [] for conflict in conflicts: baseline = baseline_codes.get(conflict.location, "") - results.append(self.resolve_conflict(conflict, baseline, task_snapshots)) + results.append( + self.resolve_conflict(conflict, baseline, task_snapshots) + ) # Combine results merged = results[0] @@ -354,7 +362,9 @@ class AIResolver: for conflict in conflicts: # Try to find the resolution for this location - code_block = extract_batch_code_blocks(response, conflict.location, language) + code_block = extract_batch_code_blocks( + response, conflict.location, language + ) if code_block: resolved.append(conflict) @@ -364,7 +374,9 @@ class AIResolver: # Return combined result if resolved: return MergeResult( - decision=MergeDecision.AI_MERGED if not remaining else MergeDecision.NEEDS_HUMAN_REVIEW, + decision=MergeDecision.AI_MERGED + if not remaining + else MergeDecision.NEEDS_HUMAN_REVIEW, file_path=file_path, merged_content=response, # Full response for manual extraction conflicts_resolved=resolved, diff --git a/auto-claude/merge/auto_merger/helpers.py b/auto-claude/merge/auto_merger/helpers.py index 79ae5262..86ce4a75 100644 --- a/auto-claude/merge/auto_merger/helpers.py +++ b/auto-claude/merge/auto_merger/helpers.py @@ -24,7 +24,11 @@ class MergeHelpers: stripped = line.strip() if MergeHelpers.is_import_line(stripped, ext): last_import_line = i + 1 - elif stripped and not stripped.startswith("#") and not stripped.startswith("//"): + elif ( + stripped + and not stripped.startswith("#") + and not stripped.startswith("//") + ): # Non-empty, non-comment line after imports if last_import_line > 0: break @@ -45,7 +49,9 @@ class MergeHelpers: """Extract the hook call from a change.""" if change.content_after: # Look for useXxx() pattern - match = re.search(r"(const\s+\{[^}]+\}\s*=\s*)?use\w+\([^)]*\);?", change.content_after) + match = re.search( + r"(const\s+\{[^}]+\}\s*=\s*)?use\w+\([^)]*\);?", change.content_after + ) if match: return match.group(0) @@ -212,7 +218,4 @@ class MergeHelpers: ChangeType.MODIFY_JSX_PROPS: 5, } - return sorted( - all_changes, - key=lambda c: priority.get(c.change_type, 10) - ) + return sorted(all_changes, key=lambda c: priority.get(c.change_type, 10)) diff --git a/auto-claude/merge/auto_merger/strategies/append_strategy.py b/auto-claude/merge/auto_merger/strategies/append_strategy.py index d11455ec..666f3222 100644 --- a/auto-claude/merge/auto_merger/strategies/append_strategy.py +++ b/auto-claude/merge/auto_merger/strategies/append_strategy.py @@ -27,7 +27,10 @@ class AppendFunctionsStrategy(MergeStrategyHandler): for snapshot in context.task_snapshots: for change in snapshot.semantic_changes: - if change.change_type == ChangeType.ADD_FUNCTION and change.content_after: + if ( + change.change_type == ChangeType.ADD_FUNCTION + and change.content_after + ): new_functions.append(change.content_after) # Append at the end (before any module.exports in JS) @@ -69,7 +72,9 @@ class AppendMethodsStrategy(MergeStrategyHandler): for change in snapshot.semantic_changes: if change.change_type == ChangeType.ADD_METHOD and change.content_after: # Extract class name from location - class_name = change.target.split(".")[0] if "." in change.target else None + class_name = ( + change.target.split(".")[0] if "." in change.target else None + ) if class_name: if class_name not in new_methods: new_methods[class_name] = [] @@ -77,7 +82,9 @@ class AppendMethodsStrategy(MergeStrategyHandler): # Insert methods into their classes for class_name, methods in new_methods.items(): - content = MergeHelpers.insert_methods_into_class(content, class_name, methods) + content = MergeHelpers.insert_methods_into_class( + content, class_name, methods + ) total_methods = sum(len(m) for m in new_methods.values()) return MergeResult( diff --git a/auto-claude/merge/auto_merger/strategies/hooks_strategy.py b/auto-claude/merge/auto_merger/strategies/hooks_strategy.py index 86860984..05849a5c 100644 --- a/auto-claude/merge/auto_merger/strategies/hooks_strategy.py +++ b/auto-claude/merge/auto_merger/strategies/hooks_strategy.py @@ -83,7 +83,9 @@ class HooksThenWrapStrategy(MergeStrategyHandler): # First add hooks if hooks: - content = MergeHelpers.insert_hooks_into_function(content, func_name, hooks) + content = MergeHelpers.insert_hooks_into_function( + content, func_name, hooks + ) # Then apply wraps for wrapper_name, wrapper_props in wraps: diff --git a/auto-claude/merge/auto_merger/strategies/import_strategy.py b/auto-claude/merge/auto_merger/strategies/import_strategy.py index 121c0d8e..99760cd6 100644 --- a/auto-claude/merge/auto_merger/strategies/import_strategy.py +++ b/auto-claude/merge/auto_merger/strategies/import_strategy.py @@ -31,7 +31,10 @@ class ImportStrategy(MergeStrategyHandler): for change in snapshot.semantic_changes: if change.change_type == ChangeType.ADD_IMPORT and change.content_after: imports_to_add.append(change.content_after.strip()) - elif change.change_type == ChangeType.REMOVE_IMPORT and change.content_before: + elif ( + change.change_type == ChangeType.REMOVE_IMPORT + and change.content_before + ): imports_to_remove.add(change.content_before.strip()) # Find where imports end in the file @@ -48,7 +51,11 @@ class ImportStrategy(MergeStrategyHandler): seen_imports = set() new_imports = [] for imp in imports_to_add: - if imp not in existing_imports and imp not in imports_to_remove and imp not in seen_imports: + if ( + imp not in existing_imports + and imp not in imports_to_remove + and imp not in seen_imports + ): new_imports.append(imp) seen_imports.add(imp) diff --git a/auto-claude/merge/auto_merger/strategies/ordering_strategy.py b/auto-claude/merge/auto_merger/strategies/ordering_strategy.py index 8993e7a1..808c5969 100644 --- a/auto-claude/merge/auto_merger/strategies/ordering_strategy.py +++ b/auto-claude/merge/auto_merger/strategies/ordering_strategy.py @@ -27,15 +27,27 @@ class OrderByDependencyStrategy(MergeStrategyHandler): for change in ordered_changes: if change.content_after: if change.change_type == ChangeType.ADD_HOOK_CALL: - func_name = change.target.split(".")[-1] if "." in change.target else change.target + func_name = ( + change.target.split(".")[-1] + if "." in change.target + else change.target + ) hook_call = MergeHelpers.extract_hook_call(change) if hook_call: - content = MergeHelpers.insert_hooks_into_function(content, func_name, [hook_call]) + content = MergeHelpers.insert_hooks_into_function( + content, func_name, [hook_call] + ) elif change.change_type == ChangeType.WRAP_JSX: wrapper = MergeHelpers.extract_jsx_wrapper(change) if wrapper: - func_name = change.target.split(".")[-1] if "." in change.target else change.target - content = MergeHelpers.wrap_function_return(content, func_name, wrapper[0], wrapper[1]) + func_name = ( + change.target.split(".")[-1] + if "." in change.target + else change.target + ) + content = MergeHelpers.wrap_function_return( + content, func_name, wrapper[0], wrapper[1] + ) return MergeResult( decision=MergeDecision.AUTO_MERGED, diff --git a/auto-claude/merge/auto_merger_old.py b/auto-claude/merge/auto_merger_old.py index 04bb5cb3..64cd7e2b 100644 --- a/auto-claude/merge/auto_merger_old.py +++ b/auto-claude/merge/auto_merger_old.py @@ -132,7 +132,10 @@ class AutoMerger: for change in snapshot.semantic_changes: if change.change_type == ChangeType.ADD_IMPORT and change.content_after: imports_to_add.append(change.content_after.strip()) - elif change.change_type == ChangeType.REMOVE_IMPORT and change.content_before: + elif ( + change.change_type == ChangeType.REMOVE_IMPORT + and change.content_before + ): imports_to_remove.add(change.content_before.strip()) # Find where imports end in the file @@ -146,7 +149,8 @@ class AutoMerger: existing_imports.add(stripped) new_imports = [ - imp for imp in imports_to_add + imp + for imp in imports_to_add if imp not in existing_imports and imp not in imports_to_remove ] @@ -261,7 +265,10 @@ class AutoMerger: for snapshot in context.task_snapshots: for change in snapshot.semantic_changes: - if change.change_type == ChangeType.ADD_FUNCTION and change.content_after: + if ( + change.change_type == ChangeType.ADD_FUNCTION + and change.content_after + ): new_functions.append(change.content_after) # Append at the end (before any module.exports in JS) @@ -299,7 +306,9 @@ class AutoMerger: for change in snapshot.semantic_changes: if change.change_type == ChangeType.ADD_METHOD and change.content_after: # Extract class name from location - class_name = change.target.split(".")[0] if "." in change.target else None + class_name = ( + change.target.split(".")[0] if "." in change.target else None + ) if class_name: if class_name not in new_methods: new_methods[class_name] = [] @@ -362,15 +371,27 @@ class AutoMerger: for change in ordered_changes: if change.content_after: if change.change_type == ChangeType.ADD_HOOK_CALL: - func_name = change.target.split(".")[-1] if "." in change.target else change.target + func_name = ( + change.target.split(".")[-1] + if "." in change.target + else change.target + ) hook_call = self._extract_hook_call(change) if hook_call: - content = self._insert_hooks_into_function(content, func_name, [hook_call]) + content = self._insert_hooks_into_function( + content, func_name, [hook_call] + ) elif change.change_type == ChangeType.WRAP_JSX: wrapper = self._extract_jsx_wrapper(change) if wrapper: - func_name = change.target.split(".")[-1] if "." in change.target else change.target - content = self._wrap_function_return(content, func_name, wrapper[0], wrapper[1]) + func_name = ( + change.target.split(".")[-1] + if "." in change.target + else change.target + ) + content = self._wrap_function_return( + content, func_name, wrapper[0], wrapper[1] + ) return MergeResult( decision=MergeDecision.AUTO_MERGED, @@ -441,7 +462,11 @@ class AutoMerger: stripped = line.strip() if self._is_import_line(stripped, ext): last_import_line = i + 1 - elif stripped and not stripped.startswith("#") and not stripped.startswith("//"): + elif ( + stripped + and not stripped.startswith("#") + and not stripped.startswith("//") + ): # Non-empty, non-comment line after imports if last_import_line > 0: break @@ -460,7 +485,9 @@ class AutoMerger: """Extract the hook call from a change.""" if change.content_after: # Look for useXxx() pattern - match = re.search(r"(const\s+\{[^}]+\}\s*=\s*)?use\w+\([^)]*\);?", change.content_after) + match = re.search( + r"(const\s+\{[^}]+\}\s*=\s*)?use\w+\([^)]*\);?", change.content_after + ) if match: return match.group(0) @@ -624,7 +651,4 @@ class AutoMerger: ChangeType.MODIFY_JSX_PROPS: 5, } - return sorted( - all_changes, - key=lambda c: priority.get(c.change_type, 10) - ) + return sorted(all_changes, key=lambda c: priority.get(c.change_type, 10)) diff --git a/auto-claude/merge/compatibility_rules.py b/auto-claude/merge/compatibility_rules.py index e1b963c7..fb18a2f5 100644 --- a/auto-claude/merge/compatibility_rules.py +++ b/auto-claude/merge/compatibility_rules.py @@ -322,7 +322,9 @@ def build_default_rules() -> list[CompatibilityRule]: return rules -def index_rules(rules: list[CompatibilityRule]) -> dict[tuple[ChangeType, ChangeType], CompatibilityRule]: +def index_rules( + rules: list[CompatibilityRule], +) -> dict[tuple[ChangeType, ChangeType], CompatibilityRule]: """ Create an index for fast rule lookup. diff --git a/auto-claude/merge/conflict_analysis.py b/auto-claude/merge/conflict_analysis.py index 8b1dfd62..3fb50931 100644 --- a/auto-claude/merge/conflict_analysis.py +++ b/auto-claude/merge/conflict_analysis.py @@ -30,9 +30,16 @@ from .types import ( try: from debug import debug, debug_detailed, debug_verbose except ImportError: - def debug(*args, **kwargs): pass - def debug_detailed(*args, **kwargs): pass - def debug_verbose(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + + def debug_detailed(*args, **kwargs): + pass + + def debug_verbose(*args, **kwargs): + pass + logger = logging.getLogger(__name__) MODULE = "merge.conflict_analysis" @@ -53,8 +60,11 @@ def detect_conflicts( List of detected conflict regions """ task_ids = list(task_analyses.keys()) - debug(MODULE, f"Detecting conflicts between {len(task_analyses)} tasks", - tasks=task_ids) + debug( + MODULE, + f"Detecting conflicts between {len(task_analyses)} tasks", + tasks=task_ids, + ) if len(task_analyses) <= 1: debug(MODULE, "No conflicts possible with 0-1 tasks") @@ -66,9 +76,12 @@ def detect_conflicts( location_changes: dict[str, list[tuple[str, SemanticChange]]] = defaultdict(list) for task_id, analysis in task_analyses.items(): - debug_detailed(MODULE, f"Processing task {task_id}", - changes_count=len(analysis.changes), - file=analysis.file_path) + debug_detailed( + MODULE, + f"Processing task {task_id}", + changes_count=len(analysis.changes), + file=analysis.file_path, + ) for change in analysis.changes: location_changes[change.location].append((task_id, change)) @@ -79,18 +92,24 @@ def detect_conflicts( if len(task_changes) <= 1: continue # No conflict at this location - debug_verbose(MODULE, f"Checking location {location}", - task_changes_count=len(task_changes)) + debug_verbose( + MODULE, + f"Checking location {location}", + task_changes_count=len(task_changes), + ) file_path = next(iter(task_analyses.values())).file_path conflict = analyze_location_conflict( file_path, location, task_changes, rule_index ) if conflict: - debug_detailed(MODULE, f"Conflict detected at {location}", - severity=conflict.severity.value, - can_auto_merge=conflict.can_auto_merge, - tasks=conflict.tasks_involved) + debug_detailed( + MODULE, + f"Conflict detected at {location}", + severity=conflict.severity.value, + can_auto_merge=conflict.can_auto_merge, + tasks=conflict.tasks_involved, + ) conflicts.append(conflict) # Also check for implicit conflicts (e.g., changes to related code) diff --git a/auto-claude/merge/conflict_detector.py b/auto-claude/merge/conflict_detector.py index 611496f4..f4f8d3f4 100644 --- a/auto-claude/merge/conflict_detector.py +++ b/auto-claude/merge/conflict_detector.py @@ -48,8 +48,13 @@ from .types import ( try: from debug import debug, debug_success except ImportError: - def debug(*args, **kwargs): pass - def debug_success(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + + def debug_success(*args, **kwargs): + pass + logger = logging.getLogger(__name__) MODULE = "merge.conflict_detector" @@ -81,7 +86,9 @@ class ConflictDetector: debug(MODULE, "Initializing ConflictDetector") self._rules = build_default_rules() self._rule_index = index_rules(self._rules) - debug_success(MODULE, "ConflictDetector initialized", rule_count=len(self._rules)) + debug_success( + MODULE, "ConflictDetector initialized", rule_count=len(self._rules) + ) def add_rule(self, rule: CompatibilityRule) -> None: """ @@ -113,15 +120,21 @@ class ConflictDetector: # Summary logging auto_mergeable = sum(1 for c in conflicts if c.can_auto_merge) from .types import ConflictSeverity + critical = sum(1 for c in conflicts if c.severity == ConflictSeverity.CRITICAL) - debug_success(MODULE, "Conflict detection complete", - total_conflicts=len(conflicts), - auto_mergeable=auto_mergeable, - critical=critical) + debug_success( + MODULE, + "Conflict detection complete", + total_conflicts=len(conflicts), + auto_mergeable=auto_mergeable, + critical=critical, + ) return conflicts - def get_compatible_pairs(self) -> list[tuple[ChangeType, ChangeType, MergeStrategy]]: + def get_compatible_pairs( + self, + ) -> list[tuple[ChangeType, ChangeType, MergeStrategy]]: """ Get all compatible change type pairs and their strategies. @@ -166,4 +179,5 @@ def analyze_compatibility( detector = ConflictDetector() from .conflict_analysis import analyze_compatibility as analyze_compat_internal + return analyze_compat_internal(change_a, change_b, detector._rule_index) diff --git a/auto-claude/merge/conflict_explanation.py b/auto-claude/merge/conflict_explanation.py index c0d7053e..02c1bd64 100644 --- a/auto-claude/merge/conflict_explanation.py +++ b/auto-claude/merge/conflict_explanation.py @@ -34,7 +34,9 @@ def explain_conflict(conflict: ConflictRegion) -> str: ] if conflict.can_auto_merge: - lines.append(f"Can be auto-merged using strategy: {conflict.merge_strategy.value}") + lines.append( + f"Can be auto-merged using strategy: {conflict.merge_strategy.value}" + ) else: lines.append("Cannot be auto-merged:") lines.append(f" Reason: {conflict.reason}") diff --git a/auto-claude/merge/file_evolution/baseline_capture.py b/auto-claude/merge/file_evolution/baseline_capture.py index 5434bad4..c3cd0919 100644 --- a/auto-claude/merge/file_evolution/baseline_capture.py +++ b/auto-claude/merge/file_evolution/baseline_capture.py @@ -22,8 +22,13 @@ from .storage import EvolutionStorage try: from debug import debug, debug_success except ImportError: - def debug(*args, **kwargs): pass - def debug_success(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + + def debug_success(*args, **kwargs): + pass + logger = logging.getLogger(__name__) MODULE = "merge.file_evolution.baseline_capture" @@ -31,10 +36,25 @@ MODULE = "merge.file_evolution.baseline_capture" # Default extensions to track for baselines DEFAULT_EXTENSIONS = { - ".py", ".js", ".ts", ".tsx", ".jsx", - ".json", ".yaml", ".yml", ".toml", - ".md", ".txt", ".html", ".css", ".scss", - ".go", ".rs", ".java", ".kt", ".swift", + ".py", + ".js", + ".ts", + ".tsx", + ".jsx", + ".json", + ".yaml", + ".yml", + ".toml", + ".md", + ".txt", + ".html", + ".css", + ".scss", + ".go", + ".rs", + ".java", + ".kt", + ".swift", } @@ -142,8 +162,7 @@ class BaselineCapture: if files is None: files = self.discover_trackable_files() - debug(MODULE, f"Capturing baselines for {len(files)} files", - task_id=task_id) + debug(MODULE, f"Capturing baselines for {len(files)} files", task_id=task_id) for file_path in files: rel_path = self.storage.get_relative_path(file_path) @@ -183,6 +202,7 @@ class BaselineCapture: evolution.add_task_snapshot(snapshot) captured[rel_path] = evolution - debug_success(MODULE, f"Captured baselines for {len(captured)} files", - task_id=task_id) + debug_success( + MODULE, f"Captured baselines for {len(captured)} files", task_id=task_id + ) return captured diff --git a/auto-claude/merge/file_evolution/evolution_queries.py b/auto-claude/merge/file_evolution/evolution_queries.py index 2b505dbf..22c509e5 100644 --- a/auto-claude/merge/file_evolution/evolution_queries.py +++ b/auto-claude/merge/file_evolution/evolution_queries.py @@ -148,10 +148,7 @@ class EvolutionQueries: List of file paths modified by 2+ tasks """ file_tasks = self.get_files_modified_by_tasks(task_ids, evolutions) - return [ - file_path for file_path, tasks in file_tasks.items() - if len(tasks) > 1 - ] + return [file_path for file_path, tasks in file_tasks.items() if len(tasks) > 1] def get_active_tasks( self, @@ -238,10 +235,7 @@ class EvolutionQueries: # Filter snapshots if task_ids specified snapshots = evolution.task_snapshots if task_ids: - snapshots = [ - ts for ts in snapshots - if ts.task_id in task_ids - ] + snapshots = [ts for ts in snapshots if ts.task_id in task_ids] return { "file_path": rel_path, @@ -253,7 +247,9 @@ class EvolutionQueries: "task_id": ts.task_id, "intent": ts.task_intent, "started_at": ts.started_at.isoformat(), - "completed_at": ts.completed_at.isoformat() if ts.completed_at else None, + "completed_at": ts.completed_at.isoformat() + if ts.completed_at + else None, "changes": [c.to_dict() for c in ts.semantic_changes], "hash_before": ts.content_hash_before, "hash_after": ts.content_hash_after, @@ -282,8 +278,7 @@ class EvolutionQueries: # Remove task snapshots from evolutions for evolution in evolutions.values(): evolution.task_snapshots = [ - ts for ts in evolution.task_snapshots - if ts.task_id != task_id + ts for ts in evolution.task_snapshots if ts.task_id != task_id ] # Remove baseline directory if requested diff --git a/auto-claude/merge/file_evolution/modification_tracker.py b/auto-claude/merge/file_evolution/modification_tracker.py index c3da12db..5cb0a0ff 100644 --- a/auto-claude/merge/file_evolution/modification_tracker.py +++ b/auto-claude/merge/file_evolution/modification_tracker.py @@ -23,8 +23,13 @@ from .storage import EvolutionStorage try: from debug import debug, debug_warning except ImportError: - def debug(*args, **kwargs): pass - def debug_warning(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + + def debug_warning(*args, **kwargs): + pass + logger = logging.getLogger(__name__) MODULE = "merge.file_evolution.modification_tracker" @@ -101,9 +106,7 @@ class ModificationTracker: ) # Analyze semantic changes - analysis = self.analyzer.analyze_diff( - rel_path, old_content, new_content - ) + analysis = self.analyzer.analyze_diff(rel_path, old_content, new_content) semantic_changes = analysis.changes # Update snapshot @@ -138,9 +141,12 @@ class ModificationTracker: worktree_path: Path to the task's worktree evolutions: Current evolution data (will be updated) """ - debug(MODULE, f"refresh_from_git() for task {task_id}", - task_id=task_id, - worktree_path=str(worktree_path)) + debug( + MODULE, + f"refresh_from_git() for task {task_id}", + task_id=task_id, + worktree_path=str(worktree_path), + ) try: # Get list of files changed in the worktree @@ -153,8 +159,13 @@ class ModificationTracker: ) changed_files = [f for f in result.stdout.strip().split("\n") if f] - debug(MODULE, f"Found {len(changed_files)} changed files", - changed_files=changed_files[:10] if len(changed_files) > 10 else changed_files) + debug( + MODULE, + f"Found {len(changed_files)} changed files", + changed_files=changed_files[:10] + if len(changed_files) > 10 + else changed_files, + ) for file_path in changed_files: # Get the diff for this file @@ -197,7 +208,9 @@ class ModificationTracker: raw_diff=diff_result.stdout, ) - logger.info(f"Refreshed {len(changed_files)} files from worktree for task {task_id}") + logger.info( + f"Refreshed {len(changed_files)} files from worktree for task {task_id}" + ) except subprocess.CalledProcessError as e: logger.error(f"Failed to refresh from git: {e}") diff --git a/auto-claude/merge/file_evolution/tracker.py b/auto-claude/merge/file_evolution/tracker.py index abf2f945..16c4ac1c 100644 --- a/auto-claude/merge/file_evolution/tracker.py +++ b/auto-claude/merge/file_evolution/tracker.py @@ -25,8 +25,13 @@ from .storage import EvolutionStorage try: from debug import debug, debug_success except ImportError: - def debug(*args, **kwargs): pass - def debug_success(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + + def debug_success(*args, **kwargs): + pass + logger = logging.getLogger(__name__) MODULE = "merge.file_evolution" @@ -72,8 +77,7 @@ class FileEvolutionTracker: storage_dir: Directory for evolution data (default: .auto-claude/) semantic_analyzer: Optional pre-configured analyzer """ - debug(MODULE, "Initializing FileEvolutionTracker", - project_dir=str(project_dir)) + debug(MODULE, "Initializing FileEvolutionTracker", project_dir=str(project_dir)) self.project_dir = Path(project_dir).resolve() storage_dir = storage_dir or (self.project_dir / ".auto-claude") @@ -81,8 +85,7 @@ class FileEvolutionTracker: # Initialize modular components self.storage = EvolutionStorage(self.project_dir, storage_dir) self.baseline_capture = BaselineCapture( - self.storage, - extensions=self.DEFAULT_EXTENSIONS + self.storage, extensions=self.DEFAULT_EXTENSIONS ) self.modification_tracker = ModificationTracker( self.storage, @@ -93,8 +96,11 @@ class FileEvolutionTracker: # Load existing evolution data self._evolutions: dict[str, FileEvolution] = self.storage.load_evolutions() - debug_success(MODULE, "FileEvolutionTracker initialized", - evolutions_loaded=len(self._evolutions)) + debug_success( + MODULE, + "FileEvolutionTracker initialized", + evolutions_loaded=len(self._evolutions), + ) # Expose storage_dir and baselines_dir for backward compatibility @property diff --git a/auto-claude/merge/file_timeline.py b/auto-claude/merge/file_timeline.py index 6597c081..4bbe8b50 100644 --- a/auto-claude/merge/file_timeline.py +++ b/auto-claude/merge/file_timeline.py @@ -67,7 +67,6 @@ from .timeline_tracker import FileTimelineTracker __all__ = [ # Main service "FileTimelineTracker", - # Core data models "MainBranchEvent", "BranchPoint", @@ -76,7 +75,6 @@ __all__ = [ "TaskFileView", "FileTimeline", "MergeContext", - # Helper components (advanced usage) "TimelineGitHelper", "TimelinePersistence", diff --git a/auto-claude/merge/git_utils.py b/auto-claude/merge/git_utils.py index 44817f09..92bfd40f 100644 --- a/auto-claude/merge/git_utils.py +++ b/auto-claude/merge/git_utils.py @@ -53,9 +53,7 @@ def find_worktree(project_dir: Path, task_id: str) -> Path | None: return None -def get_file_from_branch( - project_dir: Path, file_path: str, branch: str -) -> str | None: +def get_file_from_branch(project_dir: Path, file_path: str, branch: str) -> str | None: """ Get file content from a specific git branch. diff --git a/auto-claude/merge/install_hook.py b/auto-claude/merge/install_hook.py index 2e3a3dd1..c9288d98 100644 --- a/auto-claude/merge/install_hook.py +++ b/auto-claude/merge/install_hook.py @@ -14,7 +14,7 @@ import stat import sys from pathlib import Path -HOOK_SCRIPT = '''#!/bin/bash +HOOK_SCRIPT = """#!/bin/bash # # Git post-commit hook for FileTimelineTracker # ============================================= @@ -53,7 +53,7 @@ fi # Not main branch or in worktree, do nothing exit 0 -''' +""" def find_project_root() -> Path: @@ -114,7 +114,9 @@ def install_hook(project_path: Path) -> bool: print(f"Created new hook at {hook_path}") # Make executable - hook_path.chmod(hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + hook_path.chmod( + hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) print("Hook is now executable") return True diff --git a/auto-claude/merge/models.py b/auto-claude/merge/models.py index fe151488..447dcc7a 100644 --- a/auto-claude/merge/models.py +++ b/auto-claude/merge/models.py @@ -94,11 +94,12 @@ class MergeReport: """Convert to dictionary for serialization.""" return { "started_at": self.started_at.isoformat(), - "completed_at": self.completed_at.isoformat() if self.completed_at else None, + "completed_at": self.completed_at.isoformat() + if self.completed_at + else None, "tasks_merged": self.tasks_merged, "file_results": { - path: result.to_dict() - for path, result in self.file_results.items() + path: result.to_dict() for path, result in self.file_results.items() }, "stats": self.stats.to_dict(), "success": self.success, diff --git a/auto-claude/merge/prompts.py b/auto-claude/merge/prompts.py index b4bc259d..1b428e83 100644 --- a/auto-claude/merge/prompts.py +++ b/auto-claude/merge/prompts.py @@ -103,16 +103,20 @@ def _build_main_evolution_section(context: MergeContext) -> str: No changes have been made to main branch since this task started. """ - lines = [f"MAIN BRANCH EVOLUTION ({len(context.main_evolution)} commits since task branched)"] + lines = [ + f"MAIN BRANCH EVOLUTION ({len(context.main_evolution)} commits since task branched)" + ] lines.append("─" * 79) lines.append("") for event in context.main_evolution: source_label = event.source.upper() - if event.source == 'merged_task' and event.merged_from_task: + if event.source == "merged_task" and event.merged_from_task: source_label = f"MERGED FROM {event.merged_from_task}" - lines.append(f'COMMIT {event.commit_hash[:12]} [{source_label}]: "{event.commit_message}"') + lines.append( + f'COMMIT {event.commit_hash[:12]} [{source_label}]: "{event.commit_message}"' + ) lines.append(f"Timestamp: {event.timestamp}") if event.diff_summary: @@ -144,7 +148,9 @@ No other tasks are pending for this file. branch_point = task.get("branch_point", "unknown")[:12] commits_behind = task.get("commits_behind", 0) - lines.append(f"• {task_id} (branched at {branch_point}, {commits_behind} commits behind)") + lines.append( + f"• {task_id} (branched at {branch_point}, {commits_behind} commits behind)" + ) lines.append(f' Intent: "{intent}"') lines.append("") @@ -156,7 +162,9 @@ def _build_compatibility_instructions(context: MergeContext) -> str: if not context.other_pending_tasks: return "- No other tasks pending for this file" - lines = [f"- {len(context.other_pending_tasks)} other task(s) will merge after this"] + lines = [ + f"- {len(context.other_pending_tasks)} other task(s) will merge after this" + ] lines.append(" - Structure your merge to accommodate their upcoming changes:") for task in context.other_pending_tasks: @@ -188,13 +196,15 @@ def build_simple_merge_prompt( if task_intent: intent_section = f""" === FEATURE BRANCH INTENT ({spec_name}) === -Task: {task_intent.get('title', spec_name)} -Description: {task_intent.get('description', 'No description')} +Task: {task_intent.get("title", spec_name)} +Description: {task_intent.get("description", "No description")} """ - if task_intent.get('spec_summary'): + if task_intent.get("spec_summary"): intent_section += f"Summary: {task_intent['spec_summary']}\n" - base_section = base_content if base_content else "(File did not exist in common ancestor)" + base_section = ( + base_content if base_content else "(File did not exist in common ancestor)" + ) prompt = f'''You are a code merge expert. Merge the following conflicting versions of a file. @@ -259,17 +269,17 @@ def build_conflict_only_prompt( intent_section = "" if task_intent: intent_section = f""" -FEATURE INTENT: {task_intent.get('title', spec_name)} -{task_intent.get('description', '')} +FEATURE INTENT: {task_intent.get("title", spec_name)} +{task_intent.get("description", "")} """ conflict_sections = [] for i, conflict in enumerate(conflicts, 1): - context_before = conflict.get('context_before', '') - context_after = conflict.get('context_after', '') - main_lines = conflict.get('main_lines', '') - worktree_lines = conflict.get('worktree_lines', '') - conflict_id = conflict.get('id', f'CONFLICT_{i}') + context_before = conflict.get("context_before", "") + context_after = conflict.get("context_after", "") + main_lines = conflict.get("main_lines", "") + worktree_lines = conflict.get("worktree_lines", "") + conflict_id = conflict.get("id", f"CONFLICT_{i}") section = f""" --- {conflict_id} --- @@ -289,7 +299,7 @@ FEATURE BRANCH VERSION ({spec_name}): all_conflicts = "\n".join(conflict_sections) - prompt = f'''You are a code merge expert. Resolve the following {len(conflicts)} conflict(s) in {file_path}. + prompt = f"""You are a code merge expert. Resolve the following {len(conflicts)} conflict(s) in {file_path}. {intent_section} FILE: {file_path} LANGUAGE: {language} @@ -305,7 +315,7 @@ MERGE RULES: For EACH conflict, output the resolved code in this exact format: ---- {conflicts[0].get('id', 'CONFLICT_1')} RESOLVED --- +--- {conflicts[0].get("id", "CONFLICT_1")} RESOLVED --- ```{language} resolved code here ``` @@ -316,7 +326,7 @@ resolved code here {"```" if len(conflicts) > 1 else ""} (continue for each conflict) -''' +""" return prompt @@ -344,43 +354,49 @@ def parse_conflict_markers(content: str) -> tuple[list[dict], list[str]]: # content from incoming branch # >>>>>>> branch_name or commit_hash conflict_pattern = re.compile( - r'<<<<<<<[^\n]*\n' # Start marker - r'(.*?)' # Main/HEAD content (group 1) - r'=======\n' # Separator - r'(.*?)' # Incoming/feature content (group 2) - r'>>>>>>>[^\n]*\n?', # End marker - re.DOTALL + r"<<<<<<<[^\n]*\n" # Start marker + r"(.*?)" # Main/HEAD content (group 1) + r"=======\n" # Separator + r"(.*?)" # Incoming/feature content (group 2) + r">>>>>>>[^\n]*\n?", # End marker + re.DOTALL, ) last_end = 0 for i, match in enumerate(conflict_pattern.finditer(content), 1): # Get the clean section before this conflict - clean_before = content[last_end:match.start()] + clean_before = content[last_end : match.start()] clean_sections.append(clean_before) # Extract context (last 3 lines before conflict) - before_lines = clean_before.rstrip().split('\n') - context_before = '\n'.join(before_lines[-3:]) if len(before_lines) >= 3 else clean_before.rstrip() + before_lines = clean_before.rstrip().split("\n") + context_before = ( + "\n".join(before_lines[-3:]) + if len(before_lines) >= 3 + else clean_before.rstrip() + ) # Extract the conflict content - main_lines = match.group(1).rstrip('\n') - worktree_lines = match.group(2).rstrip('\n') + main_lines = match.group(1).rstrip("\n") + worktree_lines = match.group(2).rstrip("\n") # Get context after (first 3 lines after conflict) after_start = match.end() - after_content = content[after_start:after_start + 500] # Look ahead 500 chars - after_lines = after_content.split('\n')[:3] - context_after = '\n'.join(after_lines) + after_content = content[after_start : after_start + 500] # Look ahead 500 chars + after_lines = after_content.split("\n")[:3] + context_after = "\n".join(after_lines) - conflicts.append({ - 'id': f'CONFLICT_{i}', - 'start': match.start(), - 'end': match.end(), - 'main_lines': main_lines, - 'worktree_lines': worktree_lines, - 'context_before': context_before, - 'context_after': context_after, - }) + conflicts.append( + { + "id": f"CONFLICT_{i}", + "start": match.start(), + "end": match.end(), + "main_lines": main_lines, + "worktree_lines": worktree_lines, + "context_before": context_before, + "context_after": context_after, + } + ) last_end = match.end() @@ -408,32 +424,34 @@ def reassemble_with_resolutions( Clean file with conflicts resolved """ # Sort conflicts by start position (should already be sorted, but ensure it) - sorted_conflicts = sorted(conflicts, key=lambda c: c['start']) + sorted_conflicts = sorted(conflicts, key=lambda c: c["start"]) result_parts = [] last_end = 0 for conflict in sorted_conflicts: # Add clean content before this conflict - result_parts.append(original_content[last_end:conflict['start']]) + result_parts.append(original_content[last_end : conflict["start"]]) # Add the resolution (or keep conflict if no resolution) - conflict_id = conflict['id'] + conflict_id = conflict["id"] if conflict_id in resolutions: result_parts.append(resolutions[conflict_id]) else: # Fallback: prefer feature branch version if no resolution - result_parts.append(conflict['worktree_lines']) + result_parts.append(conflict["worktree_lines"]) - last_end = conflict['end'] + last_end = conflict["end"] # Add remaining content after last conflict result_parts.append(original_content[last_end:]) - return ''.join(result_parts) + return "".join(result_parts) -def extract_conflict_resolutions(response: str, conflicts: list[dict], language: str) -> dict[str, str]: +def extract_conflict_resolutions( + response: str, conflicts: list[dict], language: str +) -> dict[str, str]: """ Extract resolved code for each conflict from AI response. @@ -452,28 +470,25 @@ def extract_conflict_resolutions(response: str, conflicts: list[dict], language: # Pattern to match resolution blocks # --- CONFLICT_1 RESOLVED --- or similar variations resolution_pattern = re.compile( - r'---\s*(CONFLICT_\d+)\s*RESOLVED\s*---\s*\n' - r'```(?:\w+)?\n' - r'(.*?)' - r'```', - re.DOTALL | re.IGNORECASE + r"---\s*(CONFLICT_\d+)\s*RESOLVED\s*---\s*\n" + r"```(?:\w+)?\n" + r"(.*?)" + r"```", + re.DOTALL | re.IGNORECASE, ) for match in resolution_pattern.finditer(response): conflict_id = match.group(1).upper() - resolved_code = match.group(2).rstrip('\n') + resolved_code = match.group(2).rstrip("\n") resolutions[conflict_id] = resolved_code # Fallback: if only one conflict and we can find a single code block if len(conflicts) == 1 and not resolutions: - code_block_pattern = re.compile( - r'```(?:\w+)?\n(.*?)```', - re.DOTALL - ) + code_block_pattern = re.compile(r"```(?:\w+)?\n(.*?)```", re.DOTALL) matches = list(code_block_pattern.finditer(response)) if matches: # Use the first (or only) code block - resolutions[conflicts[0]['id']] = matches[0].group(1).rstrip('\n') + resolutions[conflicts[0]["id"]] = matches[0].group(1).rstrip("\n") return resolutions @@ -536,8 +551,6 @@ def optimize_prompt_for_length( context.task_worktree_content = _trim_content( context.task_worktree_content, "worktree" ) - context.current_main_content = _trim_content( - context.current_main_content, "main" - ) + context.current_main_content = _trim_content(context.current_main_content, "main") return context diff --git a/auto-claude/merge/semantic_analysis/python_analyzer.py b/auto-claude/merge/semantic_analysis/python_analyzer.py index c82e9248..def71a94 100644 --- a/auto-claude/merge/semantic_analysis/python_analyzer.py +++ b/auto-claude/merge/semantic_analysis/python_analyzer.py @@ -100,9 +100,7 @@ def extract_python_elements( # Handle decorated functions/classes for sub in child.children: if sub.type in {"function_definition", "class_definition"}: - extract_python_elements( - child, elements, get_text, get_line, parent - ) + extract_python_elements(child, elements, get_text, get_line, parent) break # Recurse for other compound statements diff --git a/auto-claude/merge/timeline_git.py b/auto-claude/merge/timeline_git.py index 50700c0b..5db9590a 100644 --- a/auto-claude/merge/timeline_git.py +++ b/auto-claude/merge/timeline_git.py @@ -23,9 +23,16 @@ logger = logging.getLogger(__name__) try: from debug import debug, debug_error, debug_warning except ImportError: - def debug(*args, **kwargs): pass - def debug_error(*args, **kwargs): pass - def debug_warning(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + + def debug_error(*args, **kwargs): + pass + + def debug_warning(*args, **kwargs): + pass + MODULE = "merge.timeline_git" @@ -60,7 +67,9 @@ class TimelineGitHelper: except subprocess.CalledProcessError: return "unknown" - def get_file_content_at_commit(self, file_path: str, commit_hash: str) -> str | None: + def get_file_content_at_commit( + self, file_path: str, commit_hash: str + ) -> str | None: """ Get file content at a specific commit. @@ -96,7 +105,14 @@ class TimelineGitHelper: """ try: result = subprocess.run( - ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", commit_hash], + [ + "git", + "diff-tree", + "--no-commit-id", + "--name-only", + "-r", + commit_hash, + ], cwd=self.project_path, capture_output=True, text=True, @@ -146,7 +162,11 @@ class TimelineGitHelper: text=True, ) if result.returncode == 0: - info["diff_summary"] = result.stdout.strip().split("\n")[-1] if result.stdout.strip() else None + info["diff_summary"] = ( + result.stdout.strip().split("\n")[-1] + if result.stdout.strip() + else None + ) except Exception: pass @@ -165,7 +185,9 @@ class TimelineGitHelper: File content as string, or empty string if file doesn't exist """ # Extract spec name from task_id (remove 'task-' prefix if present) - spec_name = task_id.replace("task-", "") if task_id.startswith("task-") else task_id + spec_name = ( + task_id.replace("task-", "") if task_id.startswith("task-") else task_id + ) worktree_path = self.project_path / ".worktrees" / spec_name / file_path if worktree_path.exists(): diff --git a/auto-claude/merge/timeline_models.py b/auto-claude/merge/timeline_models.py index c7038b4a..9103ec96 100644 --- a/auto-claude/merge/timeline_models.py +++ b/auto-claude/merge/timeline_models.py @@ -26,6 +26,7 @@ class MainBranchEvent: These events form the "spine" of the file's timeline - the authoritative history that all task worktrees diverge from and merge back into. """ + # Git identification commit_hash: str timestamp: datetime @@ -34,7 +35,7 @@ class MainBranchEvent: content: str # Source of change - source: Literal['human', 'merged_task'] + source: Literal["human", "merged_task"] merged_from_task: str | None = None # If source is 'merged_task' # Intent/reason for change @@ -73,6 +74,7 @@ class MainBranchEvent: @dataclass class BranchPoint: """The exact point a task branched from main.""" + commit_hash: str content: str timestamp: datetime @@ -96,6 +98,7 @@ class BranchPoint: @dataclass class WorktreeState: """Current state of a file in a task's worktree.""" + content: str last_modified: datetime @@ -116,6 +119,7 @@ class WorktreeState: @dataclass class TaskIntent: """What the task intends to do with this file.""" + title: str description: str from_plan: bool = False # True if extracted from implementation_plan.json @@ -144,6 +148,7 @@ class TaskFileView: This captures everything we need to know about how one task sees and modifies one file. """ + task_id: str # The exact point this task branched from main @@ -159,14 +164,16 @@ class TaskFileView: commits_behind_main: int = 0 # Lifecycle status - status: Literal['active', 'merged', 'abandoned'] = 'active' + status: Literal["active", "merged", "abandoned"] = "active" merged_at: datetime | None = None def to_dict(self) -> dict: return { "task_id": self.task_id, "branch_point": self.branch_point.to_dict(), - "worktree_state": self.worktree_state.to_dict() if self.worktree_state else None, + "worktree_state": self.worktree_state.to_dict() + if self.worktree_state + else None, "task_intent": self.task_intent.to_dict(), "commits_behind_main": self.commits_behind_main, "status": self.status, @@ -178,11 +185,17 @@ class TaskFileView: return cls( task_id=data["task_id"], branch_point=BranchPoint.from_dict(data["branch_point"]), - worktree_state=WorktreeState.from_dict(data["worktree_state"]) if data.get("worktree_state") else None, - task_intent=TaskIntent.from_dict(data["task_intent"]) if data.get("task_intent") else TaskIntent("", ""), + worktree_state=WorktreeState.from_dict(data["worktree_state"]) + if data.get("worktree_state") + else None, + task_intent=TaskIntent.from_dict(data["task_intent"]) + if data.get("task_intent") + else TaskIntent("", ""), commits_behind_main=data.get("commits_behind_main", 0), status=data.get("status", "active"), - merged_at=datetime.fromisoformat(data["merged_at"]) if data.get("merged_at") else None, + merged_at=datetime.fromisoformat(data["merged_at"]) + if data.get("merged_at") + else None, ) @@ -194,6 +207,7 @@ class FileTimeline: This is the "file-centric" view - instead of asking "what did Task X change?", we ask "what happened to File Y over time, from ALL sources?" """ + file_path: str # Main branch evolution - the authoritative history @@ -213,7 +227,7 @@ class FileTimeline: # Update commits_behind_main for all active tasks for task_view in self.task_views.values(): - if task_view.status == 'active': + if task_view.status == "active": task_view.commits_behind_main += 1 def add_task_view(self, task_view: TaskFileView) -> None: @@ -227,7 +241,7 @@ class FileTimeline: def get_active_tasks(self) -> list[TaskFileView]: """Get all tasks that are still active (not merged/abandoned).""" - return [tv for tv in self.task_views.values() if tv.status == 'active'] + return [tv for tv in self.task_views.values() if tv.status == "active"] def get_events_since_commit(self, commit_hash: str) -> list[MainBranchEvent]: """Get all main branch events since a given commit.""" @@ -279,6 +293,7 @@ class MergeContext: This is the "situational awareness" the AI needs to make intelligent merge decisions. """ + file_path: str # The task being merged diff --git a/auto-claude/merge/timeline_persistence.py b/auto-claude/merge/timeline_persistence.py index d8a84337..afe29352 100644 --- a/auto-claude/merge/timeline_persistence.py +++ b/auto-claude/merge/timeline_persistence.py @@ -27,7 +27,10 @@ logger = logging.getLogger(__name__) try: from debug import debug except ImportError: - def debug(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + MODULE = "merge.timeline_persistence" diff --git a/auto-claude/merge/timeline_tracker.py b/auto-claude/merge/timeline_tracker.py index 3db1a039..6099ce60 100644 --- a/auto-claude/merge/timeline_tracker.py +++ b/auto-claude/merge/timeline_tracker.py @@ -34,9 +34,16 @@ logger = logging.getLogger(__name__) try: from debug import debug, debug_success, debug_warning except ImportError: - def debug(*args, **kwargs): pass - def debug_success(*args, **kwargs): pass - def debug_warning(*args, **kwargs): pass + + def debug(*args, **kwargs): + pass + + def debug_success(*args, **kwargs): + pass + + def debug_warning(*args, **kwargs): + pass + MODULE = "merge.timeline_tracker" @@ -56,8 +63,9 @@ class FileTimelineTracker: project_path: Root directory of the project storage_path: Directory for timeline storage (default: .auto-claude/) """ - debug(MODULE, "Initializing FileTimelineTracker", - project_path=str(project_path)) + debug( + MODULE, "Initializing FileTimelineTracker", project_path=str(project_path) + ) self.project_path = Path(project_path).resolve() self.storage_path = storage_path or (self.project_path / ".auto-claude") @@ -72,8 +80,11 @@ class FileTimelineTracker: # Load existing timelines self._timelines = self.persistence.load_all_timelines() - debug_success(MODULE, "FileTimelineTracker initialized", - timelines_loaded=len(self._timelines)) + debug_success( + MODULE, + "FileTimelineTracker initialized", + timelines_loaded=len(self._timelines), + ) # ========================================================================= # EVENT HANDLERS @@ -103,9 +114,12 @@ class FileTimelineTracker: task_intent: Description of what the task intends to do task_title: Short title for the task """ - debug(MODULE, f"on_task_start: {task_id}", - files_to_modify=files_to_modify, - branch_point=branch_point_commit) + debug( + MODULE, + f"on_task_start: {task_id}", + files_to_modify=files_to_modify, + branch_point=branch_point_commit, + ) # Get actual branch point commit if not provided if not branch_point_commit: @@ -118,7 +132,9 @@ class FileTimelineTracker: timeline = self._get_or_create_timeline(file_path) # Get file content at branch point - content = self.git.get_file_content_at_commit(file_path, branch_point_commit) + content = self.git.get_file_content_at_commit( + file_path, branch_point_commit + ) if content is None: # File doesn't exist at this commit - might be created by task content = "" @@ -137,13 +153,15 @@ class FileTimelineTracker: from_plan=bool(task_intent), ), commits_behind_main=0, - status='active', + status="active", ) timeline.add_task_view(task_view) self._persist_timeline(file_path) - debug_success(MODULE, f"Task {task_id} registered with {len(files_to_modify)} files") + debug_success( + MODULE, f"Task {task_id} registered with {len(files_to_modify)} files" + ) def on_main_branch_commit(self, commit_hash: str) -> None: """ @@ -180,17 +198,20 @@ class FileTimelineTracker: commit_hash=commit_hash, timestamp=datetime.now(), content=content, - source='human', - commit_message=commit_info.get('message', ''), - author=commit_info.get('author'), - diff_summary=commit_info.get('diff_summary'), + source="human", + commit_message=commit_info.get("message", ""), + author=commit_info.get("author"), + diff_summary=commit_info.get("diff_summary"), ) timeline.add_main_event(event) self._persist_timeline(file_path) - debug_success(MODULE, f"Processed main commit {commit_hash[:8]}", - files_updated=len(changed_files)) + debug_success( + MODULE, + f"Processed main commit {commit_hash[:8]}", + files_updated=len(changed_files), + ) def on_task_worktree_change( self, @@ -256,7 +277,7 @@ class FileTimelineTracker: continue # Mark task as merged - task_view.status = 'merged' + task_view.status = "merged" task_view.merged_at = datetime.now() # Add main branch event for the merge @@ -266,7 +287,7 @@ class FileTimelineTracker: commit_hash=merge_commit, timestamp=datetime.now(), content=content, - source='merged_task', + source="merged_task", merged_from_task=task_id, commit_message=f"Merged from {task_id}", ) @@ -294,7 +315,7 @@ class FileTimelineTracker: task_view = timeline.get_task_view(task_id) if task_view: - task_view.status = 'abandoned' + task_view.status = "abandoned" self._persist_timeline(file_path) @@ -325,16 +346,26 @@ class FileTimelineTracker: task_view = timeline.get_task_view(task_id) if not task_view: - debug_warning(MODULE, f"Task {task_id} not found in timeline for {file_path}") + debug_warning( + MODULE, f"Task {task_id} not found in timeline for {file_path}" + ) return None # Get main evolution since task branched - main_evolution = timeline.get_events_since_commit(task_view.branch_point.commit_hash) + main_evolution = timeline.get_events_since_commit( + task_view.branch_point.commit_hash + ) # Get current main state current_main = timeline.get_current_main_state() - current_main_content = current_main.content if current_main else task_view.branch_point.content - current_main_commit = current_main.commit_hash if current_main else task_view.branch_point.commit_hash + current_main_content = ( + current_main.content if current_main else task_view.branch_point.content + ) + current_main_commit = ( + current_main.commit_hash + if current_main + else task_view.branch_point.commit_hash + ) # Get task's worktree content worktree_content = "" @@ -348,12 +379,14 @@ class FileTimelineTracker: other_tasks = [] for tv in timeline.get_active_tasks(): if tv.task_id != task_id: - other_tasks.append({ - "task_id": tv.task_id, - "intent": tv.task_intent.description, - "branch_point": tv.branch_point.commit_hash, - "commits_behind": tv.commits_behind_main, - }) + other_tasks.append( + { + "task_id": tv.task_id, + "intent": tv.task_intent.description, + "branch_point": tv.branch_point.commit_hash, + "commits_behind": tv.commits_behind_main, + } + ) context = MergeContext( file_path=file_path, @@ -369,10 +402,13 @@ class FileTimelineTracker: total_pending_tasks=len(other_tasks), ) - debug_success(MODULE, "Built merge context", - commits_behind=task_view.commits_behind_main, - main_events=len(main_evolution), - other_tasks=len(other_tasks)) + debug_success( + MODULE, + "Built merge context", + commits_behind=task_view.commits_behind_main, + main_events=len(main_evolution), + other_tasks=len(other_tasks), + ) return context @@ -420,7 +456,7 @@ class FileTimelineTracker: drift = {} for file_path, timeline in self._timelines.items(): task_view = timeline.get_task_view(task_id) - if task_view and task_view.status == 'active': + if task_view and task_view.status == "active": drift[file_path] = task_view.commits_behind_main return drift @@ -532,9 +568,12 @@ class FileTimelineTracker: task_view.commits_behind_main = drift self._persist_timeline(file_path) - debug_success(MODULE, "Initialized from worktree", - files=len(changed_files), - branch_point=branch_point[:8]) + debug_success( + MODULE, + "Initialized from worktree", + files=len(changed_files), + branch_point=branch_point[:8], + ) except Exception as e: logger.error(f"Failed to initialize from worktree: {e}") diff --git a/auto-claude/merge/tracker_cli.py b/auto-claude/merge/tracker_cli.py index b6cd093e..7ed8b55f 100644 --- a/auto-claude/merge/tracker_cli.py +++ b/auto-claude/merge/tracker_cli.py @@ -64,7 +64,9 @@ def cmd_show_timeline(args): print(f"\n--- Main Branch History ({len(timeline.main_branch_history)} events) ---") for i, event in enumerate(timeline.main_branch_history): - print(f" [{i+1}] {event.commit_hash[:8]} ({event.source}): {event.commit_message[:50]}...") + print( + f" [{i + 1}] {event.commit_hash[:8]} ({event.source}): {event.commit_message[:50]}..." + ) print(f"\n--- Task Views ({len(timeline.task_views)} tasks) ---") for task_id, view in timeline.task_views.items(): @@ -120,7 +122,9 @@ def cmd_show_context(args): print(f"\n--- Main Evolution ({len(context.main_evolution)} events) ---") for event in context.main_evolution: - print(f" {event.commit_hash[:8]} ({event.source}): {event.commit_message[:50]}...") + print( + f" {event.commit_hash[:8]} ({event.source}): {event.commit_message[:50]}..." + ) def cmd_list_files(args): @@ -136,7 +140,9 @@ def cmd_list_files(args): for file_path in sorted(tracker._timelines.keys()): timeline = tracker._timelines[file_path] - active_tasks = len([tv for tv in timeline.task_views.values() if tv.status == 'active']) + active_tasks = len( + [tv for tv in timeline.task_views.values() if tv.status == "active"] + ) main_events = len(timeline.main_branch_history) print(f" {file_path}: {active_tasks} active tasks, {main_events} main events") @@ -171,47 +177,42 @@ def main(): # notify-commit notify_parser = subparsers.add_parser( "notify-commit", - help="Notify tracker of a new commit (called by git post-commit hook)" + help="Notify tracker of a new commit (called by git post-commit hook)", ) notify_parser.add_argument("commit_hash", help="The commit hash") notify_parser.set_defaults(func=cmd_notify_commit) # show-timeline timeline_parser = subparsers.add_parser( - "show-timeline", - help="Show the timeline for a file" + "show-timeline", help="Show the timeline for a file" + ) + timeline_parser.add_argument( + "file_path", help="The file path (relative to project)" ) - timeline_parser.add_argument("file_path", help="The file path (relative to project)") timeline_parser.set_defaults(func=cmd_show_timeline) # show-drift drift_parser = subparsers.add_parser( - "show-drift", - help="Show commits-behind-main for a task" + "show-drift", help="Show commits-behind-main for a task" ) drift_parser.add_argument("task_id", help="The task ID") drift_parser.set_defaults(func=cmd_show_drift) # show-context context_parser = subparsers.add_parser( - "show-context", - help="Show merge context for a task and file" + "show-context", help="Show merge context for a task and file" ) context_parser.add_argument("task_id", help="The task ID") context_parser.add_argument("file_path", help="The file path") context_parser.set_defaults(func=cmd_show_context) # list-files - list_parser = subparsers.add_parser( - "list-files", - help="List all tracked files" - ) + list_parser = subparsers.add_parser("list-files", help="List all tracked files") list_parser.set_defaults(func=cmd_list_files) # init-from-worktree init_parser = subparsers.add_parser( - "init-from-worktree", - help="Initialize tracking from an existing worktree" + "init-from-worktree", help="Initialize tracking from an existing worktree" ) init_parser.add_argument("task_id", help="The task ID") init_parser.add_argument("worktree_path", help="Path to the worktree") diff --git a/auto-claude/merge/types.py b/auto-claude/merge/types.py index bafe0edc..9e11832f 100644 --- a/auto-claude/merge/types.py +++ b/auto-claude/merge/types.py @@ -330,7 +330,9 @@ class ConflictRegion: "change_types": [ct.value for ct in self.change_types], "severity": self.severity.value, "can_auto_merge": self.can_auto_merge, - "merge_strategy": self.merge_strategy.value if self.merge_strategy else None, + "merge_strategy": self.merge_strategy.value + if self.merge_strategy + else None, "reason": self.reason, } @@ -344,7 +346,9 @@ class ConflictRegion: change_types=[ChangeType(ct) for ct in data["change_types"]], severity=ConflictSeverity(data["severity"]), can_auto_merge=data["can_auto_merge"], - merge_strategy=MergeStrategy(data["merge_strategy"]) if data.get("merge_strategy") else None, + merge_strategy=MergeStrategy(data["merge_strategy"]) + if data.get("merge_strategy") + else None, reason=data.get("reason", ""), ) @@ -383,7 +387,9 @@ class TaskSnapshot: "task_id": self.task_id, "task_intent": self.task_intent, "started_at": self.started_at.isoformat(), - "completed_at": self.completed_at.isoformat() if self.completed_at else None, + "completed_at": self.completed_at.isoformat() + if self.completed_at + else None, "content_hash_before": self.content_hash_before, "content_hash_after": self.content_hash_after, "semantic_changes": [c.to_dict() for c in self.semantic_changes], @@ -397,10 +403,14 @@ class TaskSnapshot: task_id=data["task_id"], task_intent=data["task_intent"], started_at=datetime.fromisoformat(data["started_at"]), - completed_at=datetime.fromisoformat(data["completed_at"]) if data.get("completed_at") else None, + completed_at=datetime.fromisoformat(data["completed_at"]) + if data.get("completed_at") + else None, content_hash_before=data.get("content_hash_before", ""), content_hash_after=data.get("content_hash_after", ""), - semantic_changes=[SemanticChange.from_dict(c) for c in data.get("semantic_changes", [])], + semantic_changes=[ + SemanticChange.from_dict(c) for c in data.get("semantic_changes", []) + ], raw_diff=data.get("raw_diff"), ) @@ -449,7 +459,9 @@ class FileEvolution: baseline_captured_at=datetime.fromisoformat(data["baseline_captured_at"]), baseline_content_hash=data["baseline_content_hash"], baseline_snapshot_path=data["baseline_snapshot_path"], - task_snapshots=[TaskSnapshot.from_dict(ts) for ts in data.get("task_snapshots", [])], + task_snapshots=[ + TaskSnapshot.from_dict(ts) for ts in data.get("task_snapshots", []) + ], ) def get_task_snapshot(self, task_id: str) -> TaskSnapshot | None: @@ -463,8 +475,7 @@ class FileEvolution: """Add or update a task snapshot.""" # Remove existing snapshot for this task if present self.task_snapshots = [ - ts for ts in self.task_snapshots - if ts.task_id != snapshot.task_id + ts for ts in self.task_snapshots if ts.task_id != snapshot.task_id ] self.task_snapshots.append(snapshot) # Keep sorted by start time @@ -528,12 +539,15 @@ class MergeResult: @property def needs_human_review(self) -> bool: """Check if human review is needed.""" - return len(self.conflicts_remaining) > 0 or self.decision == MergeDecision.NEEDS_HUMAN_REVIEW + return ( + len(self.conflicts_remaining) > 0 + or self.decision == MergeDecision.NEEDS_HUMAN_REVIEW + ) def compute_content_hash(content: str) -> str: """Compute a hash of file content for comparison.""" - return hashlib.sha256(content.encode('utf-8')).hexdigest()[:16] + return hashlib.sha256(content.encode("utf-8")).hexdigest()[:16] def sanitize_path_for_storage(file_path: str) -> str: diff --git a/auto-claude/planner_lib/generators.py b/auto-claude/planner_lib/generators.py index 09a2a8b2..6e552288 100644 --- a/auto-claude/planner_lib/generators.py +++ b/auto-claude/planner_lib/generators.py @@ -183,8 +183,7 @@ class InvestigationPlanGenerator(PlanGenerator): description="Add detailed logging around suspected problem areas", expected_output="Logs capture relevant state changes and events", files_to_modify=[ - f.get("path", "") - for f in self.context.files_to_modify[:3] + f.get("path", "") for f in self.context.files_to_modify[:3] ], ), Subtask( @@ -365,9 +364,7 @@ class RefactorPlanGenerator(PlanGenerator): ) -def get_plan_generator( - context: PlannerContext, spec_dir: Path -) -> PlanGenerator: +def get_plan_generator(context: PlannerContext, spec_dir: Path) -> PlanGenerator: """Factory function to get the appropriate plan generator.""" if context.workflow_type == WorkflowType.INVESTIGATION: return InvestigationPlanGenerator(context, spec_dir) diff --git a/auto-claude/planner_lib/utils.py b/auto-claude/planner_lib/utils.py index 4c7e244f..a458753d 100644 --- a/auto-claude/planner_lib/utils.py +++ b/auto-claude/planner_lib/utils.py @@ -2,7 +2,6 @@ Utility functions for implementation planner. """ - from implementation_plan import Verification, VerificationType from .models import PlannerContext diff --git a/auto-claude/prediction/formatter.py b/auto-claude/prediction/formatter.py index cd93ee61..acda738a 100644 --- a/auto-claude/prediction/formatter.py +++ b/auto-claude/prediction/formatter.py @@ -64,9 +64,7 @@ class ChecklistFormatter: # Escape pipe characters in content desc = issue.description.replace("|", "\\|") prev = issue.prevention.replace("|", "\\|") - lines.append( - f"| {desc} | {issue.likelihood.capitalize()} | {prev} |" - ) + lines.append(f"| {desc} | {issue.likelihood.capitalize()} | {prev} |") lines.append("") return lines diff --git a/auto-claude/prediction/patterns.py b/auto-claude/prediction/patterns.py index 862243fd..a4cd16ea 100644 --- a/auto-claude/prediction/patterns.py +++ b/auto-claude/prediction/patterns.py @@ -210,17 +210,14 @@ def detect_work_type(subtask: dict) -> list[str]: # API endpoint detection if any( - kw in description - for kw in ["endpoint", "api", "route", "request", "response"] + kw in description for kw in ["endpoint", "api", "route", "request", "response"] ): work_types.append("api_endpoint") if any("routes" in f or "api" in f for f in files): work_types.append("api_endpoint") # Database model detection - if any( - kw in description for kw in ["model", "database", "migration", "schema"] - ): + if any(kw in description for kw in ["model", "database", "migration", "schema"]): work_types.append("database_model") if any("models" in f or "migration" in f for f in files): work_types.append("database_model") @@ -239,8 +236,7 @@ def detect_work_type(subtask: dict) -> list[str]: # Authentication detection if any( - kw in description - for kw in ["auth", "login", "password", "token", "session"] + kw in description for kw in ["auth", "login", "password", "token", "session"] ): work_types.append("authentication") diff --git a/auto-claude/progress.py b/auto-claude/progress.py index fdd6c8cf..e38cf2b3 100644 --- a/auto-claude/progress.py +++ b/auto-claude/progress.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from core.progress instead.""" + from core.progress import * # noqa: F403 diff --git a/auto-claude/prompt_generator.py b/auto-claude/prompt_generator.py index 94730dfa..363a9d30 100644 --- a/auto-claude/prompt_generator.py +++ b/auto-claude/prompt_generator.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from prompts_pkg.prompt_generator instead.""" + from prompts_pkg.prompt_generator import * # noqa: F403 diff --git a/auto-claude/prompts.py b/auto-claude/prompts.py index e0b2c776..1de4181d 100644 --- a/auto-claude/prompts.py +++ b/auto-claude/prompts.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from prompts_pkg.prompts instead.""" + from prompts_pkg.prompts import * # noqa: F403 diff --git a/auto-claude/qa_loop.py b/auto-claude/qa_loop.py index 8ad2de98..b031db0c 100644 --- a/auto-claude/qa_loop.py +++ b/auto-claude/qa_loop.py @@ -1,4 +1,5 @@ """Backward compatibility shim - import from qa package instead.""" + from qa import ( ISSUE_SIMILARITY_THRESHOLD, # Configuration diff --git a/auto-claude/recovery.py b/auto-claude/recovery.py index 2478efd7..f2607d2d 100644 --- a/auto-claude/recovery.py +++ b/auto-claude/recovery.py @@ -1,4 +1,5 @@ """Backward compatibility shim - import from services.recovery instead.""" + from services.recovery import ( FailureType, RecoveryAction, diff --git a/auto-claude/risk_classifier.py b/auto-claude/risk_classifier.py index ab75bebd..4140046e 100644 --- a/auto-claude/risk_classifier.py +++ b/auto-claude/risk_classifier.py @@ -1,4 +1,5 @@ """Backward compatibility shim - import from analysis.risk_classifier instead.""" + from analysis.risk_classifier import ( AssessmentFlags, ComplexityAnalysis, diff --git a/auto-claude/runners/ai_analyzer/claude_client.py b/auto-claude/runners/ai_analyzer/claude_client.py index 47d73e5f..15aa2f2b 100644 --- a/auto-claude/runners/ai_analyzer/claude_client.py +++ b/auto-claude/runners/ai_analyzer/claude_client.py @@ -40,9 +40,7 @@ class ClaudeAnalysisClient: def _validate_oauth_token(self) -> None: """Validate that CLAUDE_CODE_OAUTH_TOKEN is set.""" if not os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"): - raise ValueError( - "CLAUDE_CODE_OAUTH_TOKEN not set. Run: claude setup-token" - ) + raise ValueError("CLAUDE_CODE_OAUTH_TOKEN not set. Run: claude setup-token") async def run_analysis_query(self, prompt: str) -> str: """ diff --git a/auto-claude/runners/ai_analyzer/runner.py b/auto-claude/runners/ai_analyzer/runner.py index 28c87dea..f30169be 100644 --- a/auto-claude/runners/ai_analyzer/runner.py +++ b/auto-claude/runners/ai_analyzer/runner.py @@ -92,9 +92,7 @@ class AIAnalyzerRunner: print(" AI-ENHANCED PROJECT ANALYSIS") print("=" * 60 + "\n") - def _get_analyzers_to_run( - self, selected_analyzers: list[str] | None - ) -> list[str]: + def _get_analyzers_to_run(self, selected_analyzers: list[str] | None) -> list[str]: """ Determine which analyzers to run. diff --git a/auto-claude/runners/roadmap/competitor_analyzer.py b/auto-claude/runners/roadmap/competitor_analyzer.py index 544438d6..829558b8 100644 --- a/auto-claude/runners/roadmap/competitor_analyzer.py +++ b/auto-claude/runners/roadmap/competitor_analyzer.py @@ -53,16 +53,25 @@ class CompetitorAnalyzer: ) if not self.discovery_file.exists(): - print_status("Discovery file not found, skipping competitor analysis", "warning") - self._create_error_analysis_file("Discovery file not found - cannot analyze competitors without project context") + print_status( + "Discovery file not found, skipping competitor analysis", "warning" + ) + self._create_error_analysis_file( + "Discovery file not found - cannot analyze competitors without project context" + ) return RoadmapPhaseResult( - "competitor_analysis", True, [str(self.analysis_file)], ["Discovery file not found"], 0 + "competitor_analysis", + True, + [str(self.analysis_file)], + ["Discovery file not found"], + 0, ) errors = [] for attempt in range(MAX_RETRIES): print_status( - f"Running competitor analysis agent (attempt {attempt + 1})...", "progress" + f"Running competitor analysis agent (attempt {attempt + 1})...", + "progress", ) context = self._build_context() @@ -82,7 +91,10 @@ class CompetitorAnalyzer: ) # Graceful degradation: if all retries fail, create empty analysis and continue - print_status("Competitor analysis failed, continuing without competitor insights", "warning") + print_status( + "Competitor analysis failed, continuing without competitor insights", + "warning", + ) for err in errors: print(f" {muted('Error:')} {err}") @@ -117,12 +129,11 @@ Output your findings to competitor_analysis.json. if "competitors" in data: competitor_count = len(data.get("competitors", [])) pain_point_count = sum( - len(c.get("pain_points", [])) - for c in data.get("competitors", []) + len(c.get("pain_points", [])) for c in data.get("competitors", []) ) print_status( f"Analyzed {competitor_count} competitors, found {pain_point_count} pain points", - "success" + "success", ) return RoadmapPhaseResult( "competitor_analysis", True, [str(self.analysis_file)], [], 0 @@ -145,7 +156,7 @@ Output your findings to competitor_analysis.json. "insights_summary": { "top_pain_points": [], "differentiator_opportunities": [], - "market_trends": [] + "market_trends": [], }, "created_at": datetime.now().isoformat(), }, @@ -163,7 +174,7 @@ Output your findings to competitor_analysis.json. "insights_summary": { "top_pain_points": [], "differentiator_opportunities": [], - "market_trends": [] + "market_trends": [], }, "created_at": datetime.now().isoformat(), } diff --git a/auto-claude/runners/roadmap/executor.py b/auto-claude/runners/roadmap/executor.py index fc25551e..2561b97b 100644 --- a/auto-claude/runners/roadmap/executor.py +++ b/auto-claude/runners/roadmap/executor.py @@ -156,5 +156,7 @@ class AgentExecutor: return True, response_text except Exception as e: - debug_error("roadmap_executor", f"Agent failed: {prompt_file}", error=str(e)) + debug_error( + "roadmap_executor", f"Agent failed: {prompt_file}", error=str(e) + ) return False, str(e) diff --git a/auto-claude/runners/roadmap/graph_integration.py b/auto-claude/runners/roadmap/graph_integration.py index 74708da4..3a4dbac8 100644 --- a/auto-claude/runners/roadmap/graph_integration.py +++ b/auto-claude/runners/roadmap/graph_integration.py @@ -36,13 +36,17 @@ class GraphHintsProvider: hints_file=str(self.hints_file), ) print_status("graph_hints.json already exists", "success") - return RoadmapPhaseResult("graph_hints", True, [str(self.hints_file)], [], 0) + return RoadmapPhaseResult( + "graph_hints", True, [str(self.hints_file)], [], 0 + ) if not is_graphiti_enabled(): debug("roadmap_graph", "Graphiti not enabled, creating placeholder") print_status("Graphiti not enabled, skipping graph hints", "info") self._create_disabled_hints_file() - return RoadmapPhaseResult("graph_hints", True, [str(self.hints_file)], [], 0) + return RoadmapPhaseResult( + "graph_hints", True, [str(self.hints_file)], [], 0 + ) debug("roadmap_graph", "Querying Graphiti for roadmap insights") print_status("Querying Graphiti for roadmap insights...", "progress") @@ -63,7 +67,9 @@ class GraphHintsProvider: else: print_status("No relevant graph hints found", "info") - return RoadmapPhaseResult("graph_hints", True, [str(self.hints_file)], [], 0) + return RoadmapPhaseResult( + "graph_hints", True, [str(self.hints_file)], [], 0 + ) except Exception as e: debug_error("roadmap_graph", "Graph query failed", error=str(e)) diff --git a/auto-claude/runners/roadmap/phases.py b/auto-claude/runners/roadmap/phases.py index 5b40ecc3..c495c60b 100644 --- a/auto-claude/runners/roadmap/phases.py +++ b/auto-claude/runners/roadmap/phases.py @@ -52,7 +52,9 @@ class ProjectIndexPhase: # Check if we can copy existing index if self.auto_build_index.exists() and not self.project_index.exists(): - debug("roadmap_phase", "Copying existing project_index.json from auto-claude") + debug( + "roadmap_phase", "Copying existing project_index.json from auto-claude" + ) shutil.copy(self.auto_build_index, self.project_index) print_status("Copied existing project_index.json", "success") debug_success("roadmap_phase", "Project index copied successfully") @@ -111,7 +113,9 @@ class DiscoveryPhase: if self.discovery_file.exists() and not self.refresh: debug("roadmap_phase", "roadmap_discovery.json already exists, skipping") print_status("roadmap_discovery.json already exists", "success") - return RoadmapPhaseResult("discovery", True, [str(self.discovery_file)], [], 0) + return RoadmapPhaseResult( + "discovery", True, [str(self.discovery_file)], [], 0 + ) errors = [] for attempt in range(MAX_RETRIES): @@ -189,9 +193,7 @@ Do NOT ask questions. Make educated inferences and create the file. return None except json.JSONDecodeError as e: - debug_error( - "roadmap_phase", "Invalid JSON in discovery file", error=str(e) - ) + debug_error("roadmap_phase", "Invalid JSON in discovery file", error=str(e)) return None @@ -313,7 +315,9 @@ Output the complete roadmap to roadmap.json. ) else: if missing: - debug_warning("roadmap_phase", f"Missing required fields: {missing}") + debug_warning( + "roadmap_phase", f"Missing required fields: {missing}" + ) else: debug_warning( "roadmap_phase", diff --git a/auto-claude/scan_secrets.py b/auto-claude/scan_secrets.py index ac8564e5..50a973b7 100644 --- a/auto-claude/scan_secrets.py +++ b/auto-claude/scan_secrets.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from security.scan_secrets instead.""" + from security.scan_secrets import * # noqa: F403 diff --git a/auto-claude/security.py b/auto-claude/security.py index 373ce65f..06b5ba64 100644 --- a/auto-claude/security.py +++ b/auto-claude/security.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from security module instead.""" + from security import * # noqa: F403 diff --git a/auto-claude/security/validator_registry.py b/auto-claude/security/validator_registry.py index caf33e18..64ad1820 100644 --- a/auto-claude/security/validator_registry.py +++ b/auto-claude/security/validator_registry.py @@ -5,7 +5,6 @@ Validator Registry Central registry mapping command names to their validation functions. """ - from .database_validators import ( validate_dropdb_command, validate_dropuser_command, diff --git a/auto-claude/security_scanner.py b/auto-claude/security_scanner.py index 88570844..10f831be 100644 --- a/auto-claude/security_scanner.py +++ b/auto-claude/security_scanner.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from analysis.security_scanner instead.""" + from analysis.security_scanner import * # noqa: F403 diff --git a/auto-claude/service_orchestrator.py b/auto-claude/service_orchestrator.py index 906a48ba..8d5935c2 100644 --- a/auto-claude/service_orchestrator.py +++ b/auto-claude/service_orchestrator.py @@ -1,4 +1,5 @@ """Backward compatibility shim - import from services.orchestrator instead.""" + from services.orchestrator import ( OrchestrationResult, ServiceConfig, diff --git a/auto-claude/spec/phases/utils.py b/auto-claude/spec/phases/utils.py index b4e05144..704b6bee 100644 --- a/auto-claude/spec/phases/utils.py +++ b/auto-claude/spec/phases/utils.py @@ -10,9 +10,7 @@ import sys from pathlib import Path -def run_script( - project_dir: Path, script: str, args: list[str] -) -> tuple[bool, str]: +def run_script(project_dir: Path, script: str, args: list[str]) -> tuple[bool, str]: """ Run a Python script and return (success, output). diff --git a/auto-claude/spec/pipeline/orchestrator.py b/auto-claude/spec/pipeline/orchestrator.py index 30ab0669..b488268b 100644 --- a/auto-claude/spec/pipeline/orchestrator.py +++ b/auto-claude/spec/pipeline/orchestrator.py @@ -326,9 +326,7 @@ class SpecOrchestrator: if linear_state: print_status(f"Linear task created: {linear_state.task_id}", "success") else: - print_status( - "Linear task creation failed (continuing without)", "warning" - ) + print_status("Linear task creation failed (continuing without)", "warning") async def _phase_complexity_assessment_with_requirements( self, @@ -409,9 +407,7 @@ class SpecOrchestrator: print_status(f"Complexity override: {comp.value.upper()}", "success") return assessment - async def _run_ai_assessment( - self, task_logger - ) -> complexity.ComplexityAssessment: + async def _run_ai_assessment(self, task_logger) -> complexity.ComplexityAssessment: """Run AI-based complexity assessment. Args: @@ -581,7 +577,10 @@ class SpecOrchestrator: parent = self.spec_dir.parent prefix = self.spec_dir.name[:4] # e.g., "001-" for candidate in parent.iterdir(): - if candidate.name.startswith(prefix) and "pending" not in candidate.name: + if ( + candidate.name.startswith(prefix) + and "pending" not in candidate.name + ): self.spec_dir = candidate break return result diff --git a/auto-claude/task_logger/capture.py b/auto-claude/task_logger/capture.py index eb3e6426..346011e2 100644 --- a/auto-claude/task_logger/capture.py +++ b/auto-claude/task_logger/capture.py @@ -62,7 +62,9 @@ class StreamingLogCapture: if self.current_tool == tool_name: self.current_tool = None - def process_message(self, msg, verbose: bool = False, capture_detail: bool = True) -> None: + def process_message( + self, msg, verbose: bool = False, capture_detail: bool = True + ) -> None: """ Process a message from the Claude SDK stream. diff --git a/auto-claude/test_discovery.py b/auto-claude/test_discovery.py index 36920055..00f16fe0 100644 --- a/auto-claude/test_discovery.py +++ b/auto-claude/test_discovery.py @@ -1,4 +1,5 @@ """Backward compatibility shim - import from analysis.test_discovery instead.""" + from analysis.test_discovery import ( FRAMEWORK_PATTERNS, TestDiscovery, diff --git a/auto-claude/validate_spec/__init__.py b/auto-claude/validate_spec/__init__.py index 26ca5397..acd5d2d7 100644 --- a/auto-claude/validate_spec/__init__.py +++ b/auto-claude/validate_spec/__init__.py @@ -41,4 +41,5 @@ def __getattr__(name): raise AttributeError(f"module 'validate_spec' has no attribute '{name}'") + __all__ = ["SpecValidator", "ValidationResult", "auto_fix_plan"] diff --git a/auto-claude/validate_spec/spec_validator.py b/auto-claude/validate_spec/spec_validator.py index 7eece00a..a304d1ae 100644 --- a/auto-claude/validate_spec/spec_validator.py +++ b/auto-claude/validate_spec/spec_validator.py @@ -36,4 +36,6 @@ def __getattr__(name): if str(spec_dir) in sys.path: sys.path.remove(str(spec_dir)) - raise AttributeError(f"module 'validate_spec.spec_validator' has no attribute '{name}'") + raise AttributeError( + f"module 'validate_spec.spec_validator' has no attribute '{name}'" + ) diff --git a/auto-claude/validation_strategy.py b/auto-claude/validation_strategy.py index 1ebbbf9b..479b3d30 100644 --- a/auto-claude/validation_strategy.py +++ b/auto-claude/validation_strategy.py @@ -1,2 +1,3 @@ """Backward compatibility shim - import from spec.validation_strategy instead.""" + from spec.validation_strategy import * # noqa: F403 diff --git a/auto-claude/workspace.py b/auto-claude/workspace.py index bcfbee47..fec822f1 100644 --- a/auto-claude/workspace.py +++ b/auto-claude/workspace.py @@ -26,17 +26,17 @@ if str(_auto_claude_dir) not in sys.path: sys.path.insert(0, str(_auto_claude_dir)) # Create a minimal 'core' module if it doesn't exist (to avoid importing core/__init__.py) -if 'core' not in sys.modules: - _core_module = ModuleType('core') - _core_module.__file__ = str(_auto_claude_dir / 'core' / '__init__.py') - _core_module.__path__ = [str(_auto_claude_dir / 'core')] - sys.modules['core'] = _core_module +if "core" not in sys.modules: + _core_module = ModuleType("core") + _core_module.__file__ = str(_auto_claude_dir / "core" / "__init__.py") + _core_module.__path__ = [str(_auto_claude_dir / "core")] + sys.modules["core"] = _core_module # Now load core.workspace package directly -_workspace_init = _auto_claude_dir / 'core' / 'workspace' / '__init__.py' -_spec = importlib.util.spec_from_file_location('core.workspace', _workspace_init) +_workspace_init = _auto_claude_dir / "core" / "workspace" / "__init__.py" +_spec = importlib.util.spec_from_file_location("core.workspace", _workspace_init) _workspace_module = importlib.util.module_from_spec(_spec) -sys.modules['core.workspace'] = _workspace_module +sys.modules["core.workspace"] = _workspace_module _spec.loader.exec_module(_workspace_module) # Re-export everything from core.workspace diff --git a/auto-claude/worktree.py b/auto-claude/worktree.py index 31692c91..d8a030a1 100644 --- a/auto-claude/worktree.py +++ b/auto-claude/worktree.py @@ -25,17 +25,17 @@ if str(_auto_claude_dir) not in sys.path: sys.path.insert(0, str(_auto_claude_dir)) # Create a minimal 'core' module if it doesn't exist (to avoid importing core/__init__.py) -if 'core' not in sys.modules: - _core_module = ModuleType('core') - _core_module.__file__ = str(_auto_claude_dir / 'core' / '__init__.py') - _core_module.__path__ = [str(_auto_claude_dir / 'core')] - sys.modules['core'] = _core_module +if "core" not in sys.modules: + _core_module = ModuleType("core") + _core_module.__file__ = str(_auto_claude_dir / "core" / "__init__.py") + _core_module.__path__ = [str(_auto_claude_dir / "core")] + sys.modules["core"] = _core_module # Now load core.worktree directly -_worktree_file = _auto_claude_dir / 'core' / 'worktree.py' -_spec = importlib.util.spec_from_file_location('core.worktree', _worktree_file) +_worktree_file = _auto_claude_dir / "core" / "worktree.py" +_spec = importlib.util.spec_from_file_location("core.worktree", _worktree_file) _worktree_module = importlib.util.module_from_spec(_spec) -sys.modules['core.worktree'] = _worktree_module +sys.modules["core.worktree"] = _worktree_module _spec.loader.exec_module(_worktree_module) # Re-export everything from core.worktree