#!/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 # 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="" # Try to extract JSON from code block (```json ... ```) local json_block json_block=$(echo "$output" | sed -n '/```json/,/```/p' | sed '1d;$d') # If no ```json block, try ```result block if [ -z "$json_block" ]; then json_block=$(echo "$output" | sed -n '/```result/,/```/p' | sed '1d;$d') fi # If still no block, try to find raw JSON object at end of output if [ -z "$json_block" ]; then # Look for JSON object pattern {"status": ...} json_block=$(echo "$output" | grep -oE '\{"status":[^}]+\}' | tail -1) fi # Validate JSON if jq is available if [ -n "$json_block" ] && 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 fi elif [ -n "$json_block" ]; then # jq not available, return raw block LAST_JSON_RESULT="$json_block" echo "$json_block" return 0 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 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") case "$status" in COMPLETE|PASSED|COMPLIANT|APPROVED) return 0 ;; BLOCKED|FAILED|VIOLATIONS|CONCERNS) return 1 ;; esac fi fi # Fallback to legacy text-based parsing case "$phase_type" in 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 }