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:
@@ -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"
|
||||
@@ -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"}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user