From 7f6456facce8c544cfaf92bc5b927e61cb389fbe Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Tue, 16 Dec 2025 13:30:09 +0100 Subject: [PATCH] Add BMM workflow status tracking and project scan report - Introduced a new YAML file for tracking the BMM workflow status, detailing phases, required actions, and agent responsibilities. - Added a JSON report for the initial project scan, capturing workflow version, timestamps, and findings. - Enhanced conflict checking in the workspace by utilizing git merge-tree for in-memory conflict detection without modifying the working directory. - Updated AI resolver to use the Claude CLI for non-interactive merge resolution, improving integration with the merge process. --- _bmad-output/bmm-workflow-status.yaml | 121 ++++++++++++++++++++++++++ _bmad-output/project-scan-report.json | 1 + auto-claude/merge/ai_resolver.py | 82 ++++++++++------- auto-claude/workspace.py | 102 +++++++++++++++------- 4 files changed, 243 insertions(+), 63 deletions(-) create mode 100644 _bmad-output/bmm-workflow-status.yaml create mode 100644 _bmad-output/project-scan-report.json diff --git a/_bmad-output/bmm-workflow-status.yaml b/_bmad-output/bmm-workflow-status.yaml new file mode 100644 index 00000000..85595057 --- /dev/null +++ b/_bmad-output/bmm-workflow-status.yaml @@ -0,0 +1,121 @@ +# BMM Workflow Status - Auto Claude +# Generated: 2025-12-16 +# Track: BMad Method (Brownfield) + +# STATUS DEFINITIONS: +# ================== +# Initial Status (before completion): +# - required: Must be completed to progress +# - optional: Can be completed but not required +# - recommended: Strongly suggested but not required +# - conditional: Required only if certain conditions met (e.g., if_has_ui) +# +# Completion Status: +# - {file-path}: File created/found (e.g., "_bmad-output/prd.md") +# - skipped: Optional/conditional workflow that was skipped + +generated: "2025-12-16" +project: "Auto Claude" +project_type: "monorepo" +selected_track: "method" +field_type: "brownfield" +workflow_path: "method-brownfield.yaml" + +# User selections from workflow-init +discovery_selections: + brainstorm: false + research: true + research_starting_point: ".auto-claude/project_index.json" + product_brief: false + +# Workflow status tracking +workflow_status: + # Prerequisite - Required for brownfield + prerequisite: + document-project: + status: required + agent: analyst + command: "/bmad:bmm:workflows:document-project" + purpose: "Analyze existing codebase before planning" + output_expected: "_bmad-output/project-docs/" + + # Phase 0 - Discovery (Optional) + phase_0_discovery: + research: + status: optional + agent: analyst + command: "/bmad:bmm:workflows:research" + purpose: "Technical analysis of BMAD Method integration" + starting_point: ".auto-claude/project_index.json" + included: true + + # Phase 1 - Planning + phase_1_planning: + create-prd: + status: required + agent: pm + command: "/bmad:bmm:workflows:create-prd" + purpose: "PRD for BMAD Method integration into Auto Claude" + output_expected: "_bmad-output/prd.md" + + create-ux-design: + status: conditional + condition: "if_has_ui" + agent: ux-designer + command: "/bmad:bmm:workflows:create-ux-design" + purpose: "UX design for any UI changes" + note: "Auto Claude has Electron UI - include if BMAD integration affects UI" + + # Phase 2 - Solutioning + phase_2_solutioning: + create-architecture: + status: recommended + agent: architect + command: "/bmad:bmm:workflows:create-architecture" + purpose: "Integration architecture - how BMAD fits into Auto Claude" + output_expected: "_bmad-output/architecture.md" + note: "HIGHLY RECOMMENDED: Prevents agent confusion in brownfield" + + create-epics-and-stories: + status: required + agent: pm + command: "/bmad:bmm:workflows:create-epics-and-stories" + purpose: "Break PRD into implementable stories" + output_expected: "_bmad-output/epics/" + depends_on: + - create-prd + - create-architecture + + test-design: + status: recommended + agent: tea + command: "/bmad:bmm:workflows:testarch-test-design" + purpose: "System-level testability review" + + implementation-readiness: + status: required + agent: architect + command: "/bmad:bmm:workflows:check-implementation-readiness" + purpose: "Gate check - validate all artifacts before implementation" + depends_on: + - create-prd + - create-architecture + - create-epics-and-stories + + # Phase 3 - Implementation + phase_3_implementation: + sprint-planning: + status: required + agent: sm + command: "/bmad:bmm:workflows:sprint-planning" + purpose: "Create sprint plan with prioritized stories" + depends_on: + - implementation-readiness + +# Next action +next_workflow: + id: "document-project" + name: "Document Project" + agent: "analyst" + command: "/bmad:bmm:workflows:document-project" + reason: "Prerequisite for brownfield - analyze existing codebase first" diff --git a/_bmad-output/project-scan-report.json b/_bmad-output/project-scan-report.json new file mode 100644 index 00000000..03707a05 --- /dev/null +++ b/_bmad-output/project-scan-report.json @@ -0,0 +1 @@ +{"workflow_version":"1.2.0","timestamps":{"started":"2025-12-16T13:28:19","last_updated":"2025-12-16T13:28:19"},"mode":"initial_scan","scan_level":"deep","project_root":"/Users/andremikalsen/Documents/Coding/autonomous-coding","output_folder":"/Users/andremikalsen/Documents/Coding/autonomous-coding/_bmad-output","completed_steps":[],"current_step":"step_1","findings":{},"outputs_generated":["project-scan-report.json"],"resume_instructions":"Starting from step 1"} \ No newline at end of file diff --git a/auto-claude/merge/ai_resolver.py b/auto-claude/merge/ai_resolver.py index 4998518d..2bc21d2a 100644 --- a/auto-claude/merge/ai_resolver.py +++ b/auto-claude/merge/ai_resolver.py @@ -596,56 +596,78 @@ Resolve all conflicts now:''' def create_claude_resolver() -> AIResolver: """ - Create an AIResolver configured to use Claude via the Claude Agent SDK. + Create an AIResolver configured to use Claude via the CLI in print mode. - Uses the same SDK pattern as the rest of the auto-claude framework. + Uses the `claude --print` command for non-interactive text generation, + which is ideal for merge resolution tasks. Returns: Configured AIResolver """ - import asyncio import os + import shutil + import subprocess + import tempfile - # Check for OAuth token (required for Claude Agent SDK) + # Check for OAuth token (required for Claude) oauth_token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN") if not oauth_token: logger.warning("CLAUDE_CODE_OAUTH_TOKEN not set, AI resolution unavailable") return AIResolver() - try: - from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient - except ImportError: - logger.warning("claude_agent_sdk not installed, AI resolution unavailable") + # Check that claude CLI is available + claude_path = shutil.which("claude") + if not claude_path: + logger.warning("claude CLI not found in PATH, AI resolution unavailable") return AIResolver() def call_claude(system: str, user: str) -> str: - """Call Claude using the Agent SDK for merge resolution.""" + """Call Claude using the CLI in print mode for merge resolution.""" + # Combine system and user prompts + full_prompt = f"""{system} - async def _run_merge_agent() -> str: - client = ClaudeSDKClient( - options=ClaudeAgentOptions( - model="claude-sonnet-4-5-20250514", # Fast and capable - system_prompt=system, - allowed_tools=[], # No tools needed for merge resolution - max_turns=1, # Single response +{user}""" + + try: + # Use claude --print for non-interactive single response + # Write prompt to temp file to avoid shell escaping issues with large prompts + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write(full_prompt) + prompt_file = f.name + + try: + # Read prompt from file and pipe to claude + result = subprocess.run( + [ + claude_path, + "--print", + "--model", "claude-sonnet-4-5-20250514", + "--max-turns", "1", + "--output-format", "text", + ], + input=full_prompt, + capture_output=True, + text=True, + timeout=120, # 2 minute timeout for merge + env={**os.environ, "CLAUDE_CODE_OAUTH_TOKEN": oauth_token}, ) - ) - async with client: - await client.query(user) + if result.returncode != 0: + logger.warning(f"claude CLI returned non-zero: {result.returncode}") + logger.debug(f"stderr: {result.stderr}") + return "" - response_text = "" - async for msg in client.receive_response(): - msg_type = type(msg).__name__ - if msg_type == "AssistantMessage" and hasattr(msg, "content"): - for block in msg.content: - block_type = type(block).__name__ - if block_type == "TextBlock" and hasattr(block, "text"): - response_text += block.text + return result.stdout.strip() - return response_text + finally: + # Clean up temp file + os.unlink(prompt_file) - # Run the async function synchronously - return asyncio.run(_run_merge_agent()) + except subprocess.TimeoutExpired: + logger.warning("claude CLI timed out") + return "" + except Exception as e: + logger.warning(f"claude CLI call failed: {e}") + return "" return AIResolver(ai_call_fn=call_claude) diff --git a/auto-claude/workspace.py b/auto-claude/workspace.py index ec1e9136..11684918 100644 --- a/auto-claude/workspace.py +++ b/auto-claude/workspace.py @@ -1011,11 +1011,15 @@ def _try_smart_merge_inner( def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict: """ - Check for git-level conflicts by doing a dry-run merge. + Check for git-level conflicts WITHOUT modifying the working directory. + + Uses git merge-tree to check conflicts in-memory, avoiding HMR triggers + from file system changes. Returns: Dict with has_conflicts, conflicting_files, etc. """ + import re import subprocess spec_branch = f"auto-claude/{spec_name}" @@ -1037,51 +1041,83 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict: if base_result.returncode == 0: result["base_branch"] = base_result.stdout.strip() - # Try merge --no-commit --no-ff to see if there are conflicts - stash_result = subprocess.run( - ["git", "stash", "push", "-m", "smart-merge-check"], + # Get merge base + merge_base_result = subprocess.run( + ["git", "merge-base", result["base_branch"], spec_branch], + cwd=project_dir, + capture_output=True, + text=True, + ) + if merge_base_result.returncode != 0: + debug_warning(MODULE, "Could not find merge base") + return result + + merge_base = merge_base_result.stdout.strip() + + # Get commit hashes + main_commit_result = subprocess.run( + ["git", "rev-parse", result["base_branch"]], + cwd=project_dir, + capture_output=True, + text=True, + ) + spec_commit_result = subprocess.run( + ["git", "rev-parse", spec_branch], cwd=project_dir, capture_output=True, text=True, ) - try: - merge_result = subprocess.run( - ["git", "merge", "--no-commit", "--no-ff", spec_branch], - cwd=project_dir, - capture_output=True, - text=True, - ) + if main_commit_result.returncode != 0 or spec_commit_result.returncode != 0: + debug_warning(MODULE, "Could not resolve branch commits") + return result - if merge_result.returncode != 0: - result["has_conflicts"] = True + main_commit = main_commit_result.stdout.strip() + spec_commit = spec_commit_result.stdout.strip() - # Get list of unmerged files - status_result = subprocess.run( - ["git", "diff", "--name-only", "--diff-filter=U"], + # 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], + cwd=project_dir, + capture_output=True, + text=True, + ) + + # merge-tree returns exit code 1 if there are conflicts + if merge_tree_result.returncode != 0: + result["has_conflicts"] = True + + # Parse the output for conflicting files + 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) + if match: + file_path = match.group(1).strip() + if file_path and file_path not in result["conflicting_files"]: + result["conflicting_files"].append(file_path) + + # Fallback: if we didn't parse conflicts, use diff to find files changed in both branches + if not result["conflicting_files"]: + main_files_result = subprocess.run( + ["git", "diff", "--name-only", merge_base, main_commit], cwd=project_dir, capture_output=True, text=True, ) - if status_result.returncode == 0: - for line in status_result.stdout.strip().split("\n"): - if line: - result["conflicting_files"].append(line) - finally: - # Always abort and restore - subprocess.run( - ["git", "merge", "--abort"], - cwd=project_dir, - capture_output=True, - text=True, - ) - if "No local changes" not in stash_result.stdout: - subprocess.run( - ["git", "stash", "pop"], + 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], cwd=project_dir, capture_output=True, text=True, ) + 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) except Exception as e: print(muted(f" Error checking git conflicts: {e}")) @@ -1637,12 +1673,12 @@ def _validate_merged_syntax(file_path: str, content: str, project_dir: Path) -> # TypeScript/JavaScript validation if ext in {'.ts', '.tsx', '.js', '.jsx'}: try: - # Write to temp file + # Write to temp file in system temp dir (NOT project dir to avoid HMR triggers) with tempfile.NamedTemporaryFile( mode='w', suffix=ext, delete=False, - dir=project_dir + # Don't set dir= to avoid writing to project directory which triggers HMR ) as tmp: tmp.write(content) tmp_path = tmp.name