diff --git a/scripts/epic-chain.sh b/scripts/epic-chain.sh index 1a8cf1eaa..1087d2115 100755 --- a/scripts/epic-chain.sh +++ b/scripts/epic-chain.sh @@ -803,7 +803,7 @@ REPORT_GENERATED: $CHAIN_REPORT_FILE" log "Invoking report generator..." # Execute report generation - report_result=$(claude --dangerously-skip-permissions -p "$report_prompt" 2>&1) || true + report_result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$report_prompt" 2>&1) || true echo "$report_result" >> "$LOG_FILE" diff --git a/scripts/epic-execute-lib/design-phase.sh b/scripts/epic-execute-lib/design-phase.sh index 8a2b31cc2..33fab83fb 100644 --- a/scripts/epic-execute-lib/design-phase.sh +++ b/scripts/epic-execute-lib/design-phase.sh @@ -143,7 +143,7 @@ DESIGN COMPLETE: $story_id" fi local result - result=$(claude --dangerously-skip-permissions -p "$design_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$design_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" diff --git a/scripts/epic-execute-lib/regression-gate.sh b/scripts/epic-execute-lib/regression-gate.sh index 4bddb9661..af8b7ebe8 100644 --- a/scripts/epic-execute-lib/regression-gate.sh +++ b/scripts/epic-execute-lib/regression-gate.sh @@ -126,9 +126,9 @@ init_regression_baseline() { # Check if there's a test:json script for better parsing if grep -q '"test:json"' "$PROJECT_ROOT/package.json" 2>/dev/null; then - test_output=$(cd "$PROJECT_ROOT" && npm run test:json 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm run test:json) || true else - test_output=$(cd "$PROJECT_ROOT" && npm test 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm test) || true fi BASELINE_PASSING_TESTS=$(extract_test_count "$test_output") @@ -152,14 +152,14 @@ init_regression_baseline() { elif [ -f "$PROJECT_ROOT/Cargo.toml" ]; then # Rust project log "Capturing baseline test count (Rust)..." - test_output=$(cd "$PROJECT_ROOT" && cargo test 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" cargo test) || true BASELINE_PASSING_TESTS=$(extract_test_count "$test_output") log "Baseline passing tests: $BASELINE_PASSING_TESTS" elif [ -f "$PROJECT_ROOT/go.mod" ]; then # Go project log "Capturing baseline test count (Go)..." - test_output=$(cd "$PROJECT_ROOT" && go test ./... -v 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" go test ./... -v) || true BASELINE_PASSING_TESTS=$(extract_test_count "$test_output") log "Baseline passing tests: $BASELINE_PASSING_TESTS" @@ -167,7 +167,7 @@ init_regression_baseline() { # Python project if command -v pytest >/dev/null 2>&1; then log "Capturing baseline test count (Python)..." - test_output=$(cd "$PROJECT_ROOT" && pytest -v 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" pytest -v) || true BASELINE_PASSING_TESTS=$(extract_test_count "$test_output") log "Baseline passing tests: $BASELINE_PASSING_TESTS" fi @@ -199,23 +199,23 @@ execute_regression_gate() { if [ -f "$PROJECT_ROOT/package.json" ]; then # Check if there's a test:json script for better parsing if grep -q '"test:json"' "$PROJECT_ROOT/package.json" 2>/dev/null; then - test_output=$(cd "$PROJECT_ROOT" && npm run test:json 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm run test:json) || true else - test_output=$(cd "$PROJECT_ROOT" && npm test 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm test) || true fi current_tests=$(extract_test_count "$test_output") elif [ -f "$PROJECT_ROOT/Cargo.toml" ]; then - test_output=$(cd "$PROJECT_ROOT" && cargo test 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" cargo test) || true current_tests=$(extract_test_count "$test_output") elif [ -f "$PROJECT_ROOT/go.mod" ]; then - test_output=$(cd "$PROJECT_ROOT" && go test ./... -v 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" go test ./... -v) || true current_tests=$(extract_test_count "$test_output") elif [ -f "$PROJECT_ROOT/requirements.txt" ] || [ -f "$PROJECT_ROOT/pyproject.toml" ]; then if command -v pytest >/dev/null 2>&1; then - test_output=$(cd "$PROJECT_ROOT" && pytest -v 2>&1) || true + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" pytest -v) || true current_tests=$(extract_test_count "$test_output") fi fi diff --git a/scripts/epic-execute-lib/tdd-flow.sh b/scripts/epic-execute-lib/tdd-flow.sh index bd041f728..903cffab2 100644 --- a/scripts/epic-execute-lib/tdd-flow.sh +++ b/scripts/epic-execute-lib/tdd-flow.sh @@ -169,7 +169,7 @@ After outputting the spec block: fi local result - result=$(claude --dangerously-skip-permissions -p "$spec_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$spec_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" @@ -315,7 +315,7 @@ After implementing the tests: fi local result - result=$(claude --dangerously-skip-permissions -p "$impl_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$impl_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" diff --git a/scripts/epic-execute-lib/utils.sh b/scripts/epic-execute-lib/utils.sh index 6dadf3faa..61f8cd0fc 100644 --- a/scripts/epic-execute-lib/utils.sh +++ b/scripts/epic-execute-lib/utils.sh @@ -12,6 +12,41 @@ # Usage: Sourced by epic-execute.sh # +# ============================================================================= +# Portable Timeout Wrapper +# ============================================================================= + +# Run a command with a timeout (portable, no coreutils needed) +# Usage: run_with_timeout +# Returns: command output via stdout; exit code 0 on success, 1 on timeout +run_with_timeout() { + local timeout_secs="$1"; shift + local output_file + output_file=$(mktemp /tmp/bmad-timeout-XXXXXX) + + ( "$@" > "$output_file" 2>&1 ) & + local cmd_pid=$! + + ( sleep "$timeout_secs" && kill -TERM "$cmd_pid" 2>/dev/null && sleep 2 && kill -9 "$cmd_pid" 2>/dev/null ) & + local watchdog_pid=$! + + wait "$cmd_pid" 2>/dev/null + local exit_code=$? + + # Kill watchdog if command finished in time + kill "$watchdog_pid" 2>/dev/null + wait "$watchdog_pid" 2>/dev/null + + cat "$output_file" + rm -f "$output_file" + + # 143 = SIGTERM, 137 = SIGKILL (timed out) + if [ $exit_code -eq 143 ] || [ $exit_code -eq 137 ]; then + return 1 + fi + return $exit_code +} + # ============================================================================= # M1: Retry Logic with Exponential Backoff # ============================================================================= @@ -102,7 +137,7 @@ execute_claude_with_retry() { # Wrapper function for retry _claude_invoke() { - timeout "$timeout" claude --dangerously-skip-permissions -p "$1" 2>&1 + timeout "$timeout" env -u CLAUDECODE claude --dangerously-skip-permissions -p "$1" 2>&1 local code=$? if [ $code -eq 124 ]; then echo "TIMEOUT: Claude invocation timed out after ${timeout}s" @@ -591,7 +626,7 @@ execute_claude_verbose() { # Execute with output tee'd to both terminal and log file local result - result=$(timeout "$timeout" claude --dangerously-skip-permissions -p "$prompt" 2>&1 | tee -a "$LOG_FILE") + result=$(timeout "$timeout" env -u CLAUDECODE claude --dangerously-skip-permissions -p "$prompt" 2>&1 | tee -a "$LOG_FILE") local exit_code=$? if [ $exit_code -eq 124 ]; then @@ -605,7 +640,7 @@ execute_claude_verbose() { else # Non-verbose mode: capture output silently local result - result=$(timeout "$timeout" claude --dangerously-skip-permissions -p "$prompt" 2>&1) + result=$(timeout "$timeout" env -u CLAUDECODE claude --dangerously-skip-permissions -p "$prompt" 2>&1) local exit_code=$? # Log to file only diff --git a/scripts/epic-execute.sh b/scripts/epic-execute.sh index 42adea224..b7c0b8a86 100755 --- a/scripts/epic-execute.sh +++ b/scripts/epic-execute.sh @@ -249,6 +249,32 @@ save_log_to_repo() { fi } +# Flush the current log to the repo after each story completes/fails. +# Unlike save_log_to_repo (which runs once at exit), this overwrites +# the same file incrementally so the repo always has the latest progress. +flush_log_to_repo() { + # Skip if no log file or it's empty + if [ ! -f "$LOG_FILE" ] || [ ! -s "$LOG_FILE" ]; then + return 0 + fi + + # Create logs directory if needed + if [ -n "$LOGS_DIR" ]; then + mkdir -p "$LOGS_DIR" 2>/dev/null || true + else + return 0 + fi + + # Use a stable filename so each flush overwrites the previous snapshot + if [ -n "$EPIC_ID" ]; then + local flush_file="$LOGS_DIR/epic-${EPIC_ID}-latest.log" + else + return 0 + fi + + cp "$LOG_FILE" "$flush_file" 2>/dev/null || true +} + # ============================================================================= # Git Safety Functions # ============================================================================= @@ -1394,7 +1420,7 @@ Do NOT use 'git add -A' or 'git add .' - only stage files you created or modifie # Execute in isolated context local result - result=$(claude --dangerously-skip-permissions -p "$dev_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$dev_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" @@ -1534,7 +1560,7 @@ Stage any fixes with explicit file paths: git add ..." # Execute in isolated context local result - result=$(claude --dangerously-skip-permissions -p "$review_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$review_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" @@ -1782,7 +1808,7 @@ Address all review findings now. This is attempt $attempt_num of 3." return 0 fi - result=$(claude --dangerously-skip-permissions -f "$temp_prompt_file" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -f "$temp_prompt_file" 2>&1) || true rm -f "$temp_prompt_file" else if [ "$DRY_RUN" = true ]; then @@ -1791,7 +1817,7 @@ Address all review findings now. This is attempt $attempt_num of 3." fi # Execute in isolated context - result=$(claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true fi echo "$result" >> "$LOG_FILE" @@ -1930,7 +1956,7 @@ $build_output if grep -q '"test"' "$PROJECT_ROOT/package.json" 2>/dev/null; then log "Running tests..." local test_output - test_output=$(cd "$PROJECT_ROOT" && npm test 2>&1) || { + test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm test) || { local exit_code=$? # Check if there are NEW failures (not just pre-existing baseline failures) @@ -2248,7 +2274,7 @@ Stage any fixes with: git add ..." fi local result - result=$(claude --dangerously-skip-permissions -p "$arch_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$arch_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" @@ -2325,7 +2351,7 @@ Stage any fixes with: git add ..." fi local result - result=$(claude --dangerously-skip-permissions -p "$quality_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$quality_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" @@ -2460,7 +2486,7 @@ Analyze traceability now. Read story files on-demand as needed." fi local result - result=$(claude --dangerously-skip-permissions -p "$trace_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$trace_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" @@ -2538,7 +2564,7 @@ Generate missing tests now." fi local result - result=$(claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" @@ -2892,7 +2918,7 @@ Generate the UAT document now. Read story files on-demand as needed." fi local result - result=$(claude --dangerously-skip-permissions -p "$uat_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$uat_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" @@ -2988,6 +3014,8 @@ for story_file in "${STORIES[@]}"; do if type save_checkpoint >/dev/null 2>&1; then save_checkpoint "$STORY_INDEX" "$story_id" "$COMPLETED" "$FAILED" "$SKIPPED" fi + # Flush log to repo after story failure + flush_log_to_repo continue fi else @@ -3003,6 +3031,8 @@ for story_file in "${STORIES[@]}"; do if type save_checkpoint >/dev/null 2>&1; then save_checkpoint "$STORY_INDEX" "$story_id" "$COMPLETED" "$FAILED" "$SKIPPED" fi + # Flush log to repo after story failure + flush_log_to_repo continue fi fi @@ -3030,6 +3060,9 @@ for story_file in "${STORIES[@]}"; do if type save_checkpoint >/dev/null 2>&1; then save_checkpoint "$STORY_INDEX" "$story_id" "$COMPLETED" "$FAILED" "$SKIPPED" fi + + # Flush log to repo after each completed story + flush_log_to_repo done # ============================================================================= diff --git a/scripts/uat-validate.sh b/scripts/uat-validate.sh index 1181e1d6d..6f492e854 100755 --- a/scripts/uat-validate.sh +++ b/scripts/uat-validate.sh @@ -859,7 +859,7 @@ HUMAN_ACTION_NEEDED: {yes/no}" # Execute in isolated context local result - result=$(claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true + result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE"