#!/bin/bash # # BMAD Epic Execute - JSON Output Module # # Provides functions for structured JSON output parsing to replace # fragile regex-based completion signal detection. # # Usage: Sourced by epic-execute.sh # # ============================================================================= # JSON Output Variables # ============================================================================= # Whether to use legacy text-based parsing instead of JSON USE_LEGACY_OUTPUT=false # Last extracted JSON result (for reuse within a phase) LAST_JSON_RESULT="" # ============================================================================= # JSON Output Functions # ============================================================================= # Extract JSON result block from Claude output # Looks for ```json ... ``` or ```result ... ``` blocks # Uses awk for more robust extraction (handles multiple blocks, nested backticks) # Arguments: # $1 - Full Claude output # Returns: JSON string or empty if not found extract_json_result() { local output="$1" # Reset last result LAST_JSON_RESULT="" local json_block="" # Method 1: Extract last ```json block using awk (handles multiple blocks) # This is more robust than sed for complex outputs json_block=$(echo "$output" | awk ' /```json/ { capture=1; content=""; next } /```/ && capture { last=content; capture=0; next } capture { content = content (content ? "\n" : "") $0 } END { print last } ') # Method 2: Fallback to ```result block if [ -z "$json_block" ]; then json_block=$(echo "$output" | awk ' /```result/ { capture=1; content=""; next } /```/ && capture { last=content; capture=0; next } capture { content = content (content ? "\n" : "") $0 } END { print last } ') fi # Method 3: Legacy sed approach (simpler cases) if [ -z "$json_block" ]; then json_block=$(echo "$output" | sed -n '/```json/,/```/p' | sed '1d;$d') fi # Method 4: Find standalone JSON object with status field if [ -z "$json_block" ]; then # Look for JSON object pattern {"status": ...} - capture full object json_block=$(echo "$output" | grep -oE '\{[^{}]*"status"[^{}]*\}' | tail -1) fi # Validate JSON if jq is available if [ -n "$json_block" ]; then if command -v jq >/dev/null 2>&1; then if echo "$json_block" | jq . >/dev/null 2>&1; then LAST_JSON_RESULT="$json_block" echo "$json_block" return 0 else # JSON invalid - try to extract just the status object local simple_json simple_json=$(echo "$json_block" | jq -c '{status: .status, story_id: .story_id, summary: .summary}' 2>/dev/null || echo "") if [ -n "$simple_json" ]; then LAST_JSON_RESULT="$simple_json" echo "$simple_json" return 0 fi fi else # jq not available, return raw block LAST_JSON_RESULT="$json_block" echo "$json_block" return 0 fi fi echo "" return 1 } # Get the status field from a JSON result # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) # Returns: Status string (COMPLETE, BLOCKED, FAILED, PASSED, etc.) get_result_status() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -r '.status // empty' else # Fallback: basic pattern matching echo "$json" | grep -oE '"status":\s*"[^"]+"' | sed 's/.*"\([^"]*\)"$/\1/' fi } # Get the story_id field from a JSON result # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) get_result_story_id() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -r '.story_id // empty' else echo "$json" | grep -oE '"story_id":\s*"[^"]+"' | sed 's/.*"\([^"]*\)"$/\1/' fi } # Get the feature_type field from a JSON result (design phase) # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) # Returns: frontend | backend | fullstack (or empty if not present) get_result_feature_type() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -r '.feature_type // empty' else echo "$json" | grep -oE '"feature_type":\s*"[^"]+"' | sed 's/.*"\([^"]*\)"$/\1/' fi } # Get the summary field from a JSON result # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) get_result_summary() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -r '.summary // empty' else echo "$json" | grep -oE '"summary":\s*"[^"]+"' | sed 's/.*"\([^"]*\)"$/\1/' fi } # Get the files_changed array from a JSON result # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) # Returns: Newline-separated list of file paths get_result_files() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -r '.files_changed[]? // empty' else # Fallback: basic pattern matching (limited) echo "$json" | grep -oE '"files_changed":\s*\[[^\]]*\]' | grep -oE '"[^"]+\.[a-z]+"' | tr -d '"' fi } # Get the concerns array from a JSON result # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) # Returns: Newline-separated list of concerns get_result_concerns() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -r '.concerns[]? // empty' else echo "" fi } # Get the issues array from a JSON result (for review/fix phases) # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) # Returns: JSON array of issues or empty get_result_issues() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -c '.issues // []' else echo "[]" fi } # Get the tests_added count from a JSON result # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) # Returns: Number of tests added get_result_tests_added() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "0" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -r '.tests_added // 0' else echo "$json" | grep -oE '"tests_added":\s*[0-9]+' | grep -oE '[0-9]+' || echo "0" fi } # Get the decisions array from a JSON result # Arguments: # $1 - JSON string (optional, uses LAST_JSON_RESULT if not provided) # Returns: JSON array of decisions get_result_decisions() { local json="${1:-$LAST_JSON_RESULT}" if [ -z "$json" ]; then echo "[]" return 1 fi if command -v jq >/dev/null 2>&1; then echo "$json" | jq -c '.decisions // []' else echo "[]" fi } # Check phase completion with JSON parsing and text fallback # Arguments: # $1 - Full Claude output # $2 - Phase type (dev, review, fix, arch, test_quality, trace, uat) # $3 - Story ID (for legacy text matching) # Returns: 0 if complete/passed, 1 if failed/blocked, 2 if unclear check_phase_completion() { local output="$1" local phase_type="$2" local story_id="$3" # Try JSON parsing first (unless legacy mode) if [ "$USE_LEGACY_OUTPUT" != true ]; then local json_result json_result=$(extract_json_result "$output") if [ -n "$json_result" ]; then local status status=$(get_result_status "$json_result") # Normalize status to uppercase for comparison status=$(echo "$status" | tr '[:lower:]' '[:upper:]') case "$status" in COMPLETE|PASSED|COMPLIANT|APPROVED|SUCCESS|DONE|OK) return 0 ;; BLOCKED|FAILED|VIOLATIONS|ERROR|INCOMPLETE|REJECTED) return 1 ;; CONCERNS) # Concerns typically don't block for test_quality and trace if [ "$phase_type" = "test_quality" ] || [ "$phase_type" = "trace" ]; then return 0 fi return 1 ;; esac fi fi # Try fuzzy matching from utils module if available (M3 improvement) if type check_phase_completion_fuzzy >/dev/null 2>&1; then check_phase_completion_fuzzy "$output" "$phase_type" "$story_id" local fuzzy_result=$? if [ $fuzzy_result -ne 2 ]; then return $fuzzy_result fi fi # Fallback to legacy text-based parsing case "$phase_type" in design) if echo "$output" | grep -q "DESIGN COMPLETE"; then return 0 elif echo "$output" | grep -q "DESIGN BLOCKED"; then return 1 fi ;; dev) if echo "$output" | grep -q "IMPLEMENTATION COMPLETE"; then return 0 elif echo "$output" | grep -q "IMPLEMENTATION BLOCKED"; then return 1 fi ;; review) if echo "$output" | grep -q "REVIEW PASSED"; then return 0 elif echo "$output" | grep -q "REVIEW FAILED"; then return 1 fi ;; fix) if echo "$output" | grep -q "FIX COMPLETE"; then return 0 elif echo "$output" | grep -q "FIX INCOMPLETE"; then return 1 fi ;; arch) if echo "$output" | grep -q "ARCH COMPLIANT"; then return 0 elif echo "$output" | grep -q "ARCH VIOLATIONS"; then return 1 fi ;; test_quality) if echo "$output" | grep -q "TEST QUALITY APPROVED"; then return 0 elif echo "$output" | grep -q "TEST QUALITY FAILED"; then return 1 elif echo "$output" | grep -q "TEST QUALITY CONCERNS"; then # Concerns don't block return 0 fi ;; trace) if echo "$output" | grep -q "TRACEABILITY PASS"; then return 0 elif echo "$output" | grep -q "TRACEABILITY FAIL"; then return 1 elif echo "$output" | grep -q "TRACEABILITY CONCERNS"; then return 0 fi ;; uat) if echo "$output" | grep -q "UAT GENERATED"; then return 0 fi ;; test_gen) if echo "$output" | grep -q "TEST GENERATION COMPLETE"; then return 0 elif echo "$output" | grep -q "TEST GENERATION PARTIAL"; then return 1 fi ;; esac # Unclear result return 2 } # Build JSON output instruction block for prompts # Arguments: # $1 - Phase type (dev, review, fix, arch, test_quality, trace, uat) # $2 - Story ID # Returns: Instruction text for prompts build_json_output_instructions() { local phase_type="$1" local story_id="$2" cat << 'EOF' ## Output Format After completing your task, output a JSON result block: ```json { "status": "COMPLETE" | "BLOCKED" | "FAILED" | "PASSED" | "VIOLATIONS" | "CONCERNS", "story_id": "", "summary": "", "files_changed": ["", ""], "tests_added": , "decisions": [ {"what": "", "why": ""} ], "issues": [ {"severity": "HIGH|MEDIUM|LOW", "description": "", "location": ""} ], "concerns": [""] } ``` ### Status Values by Phase EOF case "$phase_type" in dev) cat << EOF - **COMPLETE**: Implementation finished successfully - **BLOCKED**: Cannot proceed due to missing dependencies or unclear requirements Then ALSO output the legacy signal for backward compatibility: - Success: \`IMPLEMENTATION COMPLETE: $story_id\` - Blocked: \`IMPLEMENTATION BLOCKED: $story_id - [reason]\` EOF ;; review) cat << EOF - **PASSED**: Code review passed (all issues fixed or acceptable) - **FAILED**: Critical issues remain that need developer attention Then ALSO output the legacy signal for backward compatibility: - Pass: \`REVIEW PASSED: $story_id\` - Fail: \`REVIEW FAILED: $story_id - [reason]\` EOF ;; fix) cat << EOF - **COMPLETE**: All issues from review have been fixed - **FAILED**: Unable to fix one or more issues Then ALSO output the legacy signal for backward compatibility: - Complete: \`FIX COMPLETE: $story_id - Fixed N issues\` - Incomplete: \`FIX INCOMPLETE: $story_id - [reason]\` EOF ;; arch) cat << EOF - **COMPLIANT**: No architecture violations (or all fixed) - **VIOLATIONS**: Architecture violations that need attention Then ALSO output the legacy signal for backward compatibility: - Compliant: \`ARCH COMPLIANT: $story_id\` - Violations: \`ARCH VIOLATIONS: $story_id - [summary]\` EOF ;; test_quality) cat << EOF - **APPROVED**: Test quality meets standards (score >= 70) - **CONCERNS**: Minor quality issues (score 60-69) - **FAILED**: Test quality below acceptable threshold (score < 60) Then ALSO output the legacy signal for backward compatibility: - Approved: \`TEST QUALITY APPROVED: $story_id - Score: N/100\` - Concerns: \`TEST QUALITY CONCERNS: $story_id - Score: N/100\` - Failed: \`TEST QUALITY FAILED: $story_id - Score: N/100\` EOF ;; trace) cat << EOF - **PASSED**: Traceability requirements met (P0=100%, P1>=90%) - **CONCERNS**: Minor gaps (P1 80-89%) - **FAILED**: Critical traceability gaps Then ALSO output the legacy signal for backward compatibility: - Pass: \`TRACEABILITY PASS: Epic-$story_id - P0: N%, P1: M%\` - Fail: \`TRACEABILITY FAIL: Epic-$story_id - X critical gaps\` EOF ;; uat) cat << EOF - **COMPLETE**: UAT document generated successfully Then ALSO output the legacy signal: \`UAT GENERATED: \` EOF ;; esac }