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.
This commit is contained in:
AndyMik90
2025-12-16 13:30:09 +01:00
parent 901e83ac61
commit 7f6456facc
4 changed files with 243 additions and 63 deletions
+121
View File
@@ -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"
+1
View File
@@ -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"}
+52 -30
View File
@@ -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)
+69 -33
View File
@@ -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